diff --git a/.devcontainer/Dockerfile.dev b/.devcontainer/Dockerfile.dev new file mode 100644 index 000000000..1bc7183e3 --- /dev/null +++ b/.devcontainer/Dockerfile.dev @@ -0,0 +1 @@ +FROM ghcr.io/universityradioyork/myradio/dev-base:latest diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..d715f5cdc --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,59 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.203.0/containers/docker-existing-docker-compose +// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml. +{ + "name": "MyRadio + Postgres, Memcached, Mailhog", + + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": [ + "../docker-compose.yml", + "docker-compose.yml" + ], + + "service": "myradio", + + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + "workspaceFolder": "/var/www/myradio", + + // Set *default* container specific settings.json values on container create. + "settings": { + "sqltools.connections": [{ + "name": "Container database", + "driver": "PostgreSQL", + "server": "postgres", + "previewLimit": 50, + "port": 5432, + "database": "myradio", + "username": "myradio", + "password": "myradio" + }] + }, + + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "bmewburn.vscode-intelephense-client", + "felixfbecker.php-debug", + "mtxr.sqltools", + "mtxr.sqltools-driver-pg", + "whatwedo.twig" + ], + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [7080, 8025], + + // "initializeCommand": "" + + // Uncomment the next line if you want start specific services in your Docker Compose config. + // "runServices": [], + + // Uncomment the next line if you want to keep your containers running after VS Code shuts down. + // "shutdownAction": "none", + + // Uncomment the next line to run commands after the container is created - for example installing curl. + "postCreateCommand": "COMPOSER_VENDOR_DIR=/workspaces/MyRadio/src/vendor composer install && apachectl restart", + + // Uncomment to connect as a non-root user if you've added one. See https://aka.ms/vscode-remote/containers/non-root. + // "remoteUser": "vscode" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 000000000..dd03ef687 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,25 @@ +version: '3' +services: + # Update this to the name of the service you want to work with in your docker-compose.yml file + myradio: + # Use a pre-built base image to speed up startup + build: + context: . + dockerfile: .devcontainer/Dockerfile.dev + + # If you want add a non-root user to your Dockerfile, you can use the "remoteUser" + # property in devcontainer.json to cause VS Code its sub-processes (terminals, tasks, + # debugging) to execute as the user. Uncomment the next line if you want the entire + # container to run as this user instead. Note that, on Linux, you may need to + # ensure the UID and GID of the container user you create matches your local user. + # See https://aka.ms/vscode-remote/containers/non-root for details. + # + # user: vscode + + init: true + + volumes: + # Update this to wherever you want VS Code to mount the folder of your project + - .:/workspaces/MyRadio:cached + - ./sample_configs/codespaces-server-name.conf:/etc/apache2/conf-enabled/server-name.conf + - ./sample_configs/codespaces-apache.conf:/etc/apache2/sites-available/myradio.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..2bb1c8ce5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +src/vendor/ +node_modules/ \ No newline at end of file diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 000000000..15a3ecc1c --- /dev/null +++ b/.eslintignore @@ -0,0 +1,4 @@ +*.min.js +src/vendor/* +src/Public/js/vendor/**/*.js +src/PublicAPI/rtfm/* diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 000000000..84b83b270 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,16 @@ +{ + "root": true, + "env": { + "browser": true, + "es6": true, + "jquery": true + }, + "extends": "eslint:recommended", + "rules": { + "indent": ["error", 2], + "linebreak-style": ["error", "unix"], + "quotes": ["error", "double"], + "semi": ["error", "always"], + "no-console": "off" + } +} diff --git a/.github/workflows/dev-base-image.yml b/.github/workflows/dev-base-image.yml new file mode 100644 index 000000000..51994fa02 --- /dev/null +++ b/.github/workflows/dev-base-image.yml @@ -0,0 +1,50 @@ +name: Update development base image + +on: + push: + branches: [ master ] + paths: + - sample_configs/* + - composer.* + - .github/* + - .devcontainer/* + workflow_dispatch: + inputs: + ref: + description: Branch, tag, or commit SHA to build against + required: false + default: master + docker-tag: + description: Tag to add to the Docker image + required: false + default: latest + +jobs: + + build: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.ref || 'master' }} + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker Login + uses: docker/login-action@v1.10.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + platforms: linux/amd64,linux/arm64 + push: true + tags: ghcr.io/universityradioyork/myradio/dev-base:${{ github.event.inputs.docker-tag || 'latest' }} diff --git a/.gitignore b/.gitignore index e3b49c40d..4c6a974d7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,18 @@ -/nbproject *.LCK* -MyRadio_Config.local.php +MyRadio_Config.local.php* +util/checkpassword.conf *.sw[a-z] sftp-*.json +.sync-config.cson +.tags* +Thumbs.db +cache.properties +build/ +.phpintel +.vagrant +src/vendor +composer.lock +node_modules/ +src/Public/img/stats_training_*.svg +.idea/ +.DS_Store \ No newline at end of file diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 000000000..e669bd5d1 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,66 @@ +sudo: required +dist: trusty # Use notancient unbuntu +language: php +php: + - '7.1' + - '7.2' + - '7.3' + - '7.4' + - 'nightly' # why not + +matrix: + allow_failures: + - php: 'nightly' + +cache: + directories: + - src/vendor + - $HOME/.cache/composer/files + - node_modules/ + +addons: + postgresql: '9.5' +services: + - postgresql +before_install: + - nvm install --lts # Updated Node required for eslint +install: + - composer install + - npm install eslint + - sudo apt-get update + - sudo apt-get install apache2 libapache2-mod-fastcgi +before_script: + # missing read perms on $HOME, which apache24 requires + - chmod 755 $HOME + # enable php-fpm + - phpenv config-add sample_configs/travis-php.ini + - sudo cp ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.conf{.default,} + - sudo cp ~/.phpenv/versions/$(phpenv version-name)/etc/php-fpm.d/www.conf{.default,} + - sudo a2enmod rewrite actions fastcgi alias + - echo "cgi.fix_pathinfo = 1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + - ~/.phpenv/versions/$(phpenv version-name)/sbin/php-fpm + # configure apache virtual hosts + - sudo cp -f sample_configs/travis-fpm.conf /etc/apache2/sites-available/myradio-travis.conf + - sudo sed -e "s?%TRAVIS_BUILD_DIR%?$(pwd)?g" --in-place /etc/apache2/sites-available/myradio-travis.conf + - sudo a2dissite 000-default + - sudo a2ensite myradio-travis + - sudo service apache2 restart + - sudo apachectl -S + - sudo cat /etc/apache2/sites-enabled/* + # setup postgres + - psql -U postgres < sample_configs/postgres.sql + - PGPASSWORD=myradio psql -U myradio myradio < schema/base.sql + - for f in schema/patches/*.sql; do PGPASSWORD=myradio psql -U myradio myradio < $f; done + - PGPASSWORD=myradio psql -U myradio myradio < sample_configs/travis-auth.sql + # configure MyRadio + - cp sample_configs/travis-config.php src/MyRadio_Config.local.php +script: + - src/vendor/bin/phpcs --standard=PSR2 --ignore="*.min.*,*/vendor/*,*/PublicAPI/rtfm/*,*/Public/js/*" --exclude=Squiz.Classes.ValidClassName,PSR1.Files.SideEffects src/ -p -s + - node_modules/.bin/eslint . + - curl -v http://localhost/api/v2/config/publicconfig?api_key=travis-test-key + - src/vendor/bin/codecept run --debug --env travis +after_script: + - sudo cat $(pwd)/apache-access.log + - sudo cat $(pwd)/apache-error.log + - sudo cat ~/.phpenv/versions/$(phpenv version-name)/var/log/php-fpm.log + - sudo ls -l ~/.phpenv/versions/$(phpenv version-name)/var/log/ diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 000000000..542060d33 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,14 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Listen for Xdebug", + "type": "php", + "request": "launch", + "port": 9003 + } + ] +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..835f4167f --- /dev/null +++ b/Dockerfile @@ -0,0 +1,59 @@ +FROM php:8.2-apache + +RUN apt-get update && apt-get install -y libpq-dev libpng-dev libjpeg-dev libldap-dev unzip \ + libcurl4-openssl-dev libxslt-dev git libz-dev libzip-dev libmemcached-dev \ + postgresql-client jq msmtp-mta ffmpeg + +RUN docker-php-ext-install pgsql pdo_pgsql gd ldap curl xsl zip + +RUN pecl install memcached && \ + echo extension=memcached.so >> /usr/local/etc/php/conf.d/memcached.ini + +RUN pecl install xdebug-3.3.1 && docker-php-ext-enable xdebug \ + && echo 'zend_extension="/usr/local/lib/php/extensions/no-debug-non-zts-20220829/xdebug.so"' >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ + && echo 'xdebug.client_port=9003' >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ + && echo 'xdebug.mode=develop,debug' >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ + && echo 'xdebug.start_with_request=yes' >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \ + && echo 'xdebug.client_host=localhost' >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini + +RUN echo 'error_reporting=E_ALL' >> /usr/local/etc/php/conf.d/error-reporting.ini + +RUN echo "memory_limit=512M" >> /usr/local/etc/php/conf.d/uploads.ini +RUN echo "upload_max_filesize=512M" >> /usr/local/etc/php/conf.d/uploads.ini +RUN echo "post_max_size=512M" >> /usr/local/etc/php/conf.d/uploads.ini + +RUN echo sendmail_path = "/usr/bin/msmtp -t --host mail --port 1025 --from myradio@ury.dev" > /usr/local/etc/php/conf.d/sendmail.ini + +# Self-signed certificate +RUN openssl req -nodes -new -subj "/C=GB/ST=North Yorkshire/L=York/O=University Radio York/OU=Localhost/CN=localhost" > myradio.csr && \ + openssl rsa -in privkey.pem -out myradio.key && \ + openssl x509 -in myradio.csr -out myradio.crt -req -signkey myradio.key -days 999 && \ + cp myradio.crt /etc/apache2/myradio.crt && \ + cp myradio.key /etc/apache2/myradio.key + +RUN a2enmod rewrite ssl + +# Fixes for running on ARM +RUN echo "Mutex posixsem" >> /etc/apache2/apache2.conf + +RUN echo "127.0.0.0/8" >> /etc/apache2/trusted-proxies.txt + +COPY sample_configs/apache.conf /etc/apache2/sites-available/myradio.conf +RUN a2enmod remoteip && a2dissite 000-default && a2ensite myradio && apachectl -S + +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer + +RUN mkdir -p /var/www/myradio && chown -R www-data:www-data /var/www/myradio && \ + mkdir -p /var/log/myradio && chown -R www-data:www-data /var/log/myradio + +WORKDIR /var/www/myradio +COPY composer.* /var/www/myradio/ +RUN COMPOSER_VENDOR_DIR=/var/www/myradio/src/vendor composer install --no-security-blocking + +COPY schema schema +COPY src src + +COPY sample_configs/docker-config.php src/MyRadio_Config.local.php +RUN chown www-data:www-data /var/www/myradio/src/MyRadio_Config.local.php && chmod 664 /var/www/myradio/src/MyRadio_Config.local.php + +CMD ["apache2-foreground"] diff --git a/Dockerfile.prod b/Dockerfile.prod new file mode 100644 index 000000000..83af47e75 --- /dev/null +++ b/Dockerfile.prod @@ -0,0 +1,50 @@ +FROM php:8.2-apache + +RUN apt-get update && apt-get install -y libpq-dev libpng-dev libjpeg-dev libldap-dev unzip \ + libcurl4-openssl-dev libxslt-dev git libz-dev libzip-dev libmemcached-dev \ + postgresql-client jq msmtp-mta ffmpeg + +RUN docker-php-ext-install pgsql pdo_pgsql gd ldap curl xsl zip + +RUN pecl install memcached && \ + echo extension=memcached.so >> /usr/local/etc/php/conf.d/memcached.ini + +RUN echo 'error_reporting=E_ALL' >> /usr/local/etc/php/conf.d/error-reporting.ini + +RUN echo "memory_limit=512M" >> /usr/local/etc/php/conf.d/uploads.ini +RUN echo "upload_max_filesize=512M" >> /usr/local/etc/php/conf.d/uploads.ini +RUN echo "post_max_size=512M" >> /usr/local/etc/php/conf.d/uploads.ini + +RUN echo sendmail_path = "/usr/bin/msmtp -t --host ury.york.ac.uk --port 587 --from noreply@ury.org.uk" > /usr/local/etc/php/conf.d/sendmail.ini + +# Self-signed certificate +RUN openssl req -nodes -new -subj "/C=GB/ST=North Yorkshire/L=York/O=University Radio York/OU=Localhost/CN=localhost" > myradio.csr && \ + openssl rsa -in privkey.pem -out myradio.key && \ + openssl x509 -in myradio.csr -out myradio.crt -req -signkey myradio.key -days 999 && \ + cp myradio.crt /etc/apache2/myradio.crt && \ + cp myradio.key /etc/apache2/myradio.key + +RUN a2enmod rewrite ssl + +RUN echo "144.32.64.160/27" >> /etc/apache2/trusted-proxies.txt + +COPY sample_configs/apache.conf /etc/apache2/sites-available/myradio.conf +RUN a2enmod remoteip && a2dissite 000-default && a2ensite myradio && apachectl -S + +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer + +RUN mkdir -p /var/www/myradio && chown -R www-data:www-data /var/www/myradio && \ + mkdir -p /var/log/myradio && chown -R www-data:www-data /var/log/myradio && \ + ln -s /dev/stderr /var/log/myradio/errors.log + +WORKDIR /var/www/myradio +COPY composer.* /var/www/myradio/ +RUN COMPOSER_VENDOR_DIR=/var/www/myradio/src/vendor composer install --no-security-blocking + +COPY schema schema +COPY src src + +COPY sample_configs/docker-config.php src/MyRadio_Config.local.php +RUN chown www-data:www-data /var/www/myradio/src/MyRadio_Config.local.php && chmod 664 /var/www/myradio/src/MyRadio_Config.local.php + +CMD ["apache2-foreground"] diff --git a/README.md b/README.md index 206a101fd..1d3135208 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,9 @@ their own community or student radio venture. MyRadio is part of a suite of upcoming public projects, including: - iTones, our liquidsoap sustainer system -- loggerng, our python audio logging and retriving system +- loggerng, our python audio logging and retrieving system - Bootstrapping scripts for setting up and configuring all the dependencies -MyRadio uses Git-Flow as a development workflow: https://github.com/nvie/gitflow/ +To install myradio locally, see the [Install Guide](docs/install.md) + +MyRadio uses [GitHub Flow](https://guides.github.com/overviews/flow/) as a development workflow: diff --git a/Vagrantfile b/Vagrantfile new file mode 100644 index 000000000..c2d2116b2 --- /dev/null +++ b/Vagrantfile @@ -0,0 +1,78 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# All Vagrant configuration is done below. The "2" in Vagrant.configure +# configures the configuration version (we support older styles for +# backwards compatibility). Please don't change it unless you know what +# you're doing. +Vagrant.configure(2) do |config| + # The most common configuration options are documented and commented below. + # For a complete reference, please see the online documentation at + # https://docs.vagrantup.com. + + # Every Vagrant development environment requires a box. You can search for + # boxes at https://atlas.hashicorp.com/search. + config.vm.box = "bento/ubuntu-20.04" + + # Disable automatic box update checking. If you disable this, then + # boxes will only be checked for updates when the user runs + # `vagrant box outdated`. This is not recommended. + # config.vm.box_check_update = false + + # Create a forwarded port mapping which allows access to a specific port + # within the machine from a port on the host machine. In the example below, + # accessing "localhost:8080" will access port 80 on the guest machine. + config.vm.network "forwarded_port", guest: 443, host: 4443 + config.vm.network "forwarded_port", guest: 80, host: 7080 + + # Create a private network, which allows host-only access to the machine + # using a specific IP. + # config.vm.network "private_network", ip: "192.168.33.10" + + # Create a public network, which generally matched to bridged network. + # Bridged networks make the machine appear as another physical device on + # your network. + # config.vm.network "public_network" + + # Share an additional folder to the guest VM. The first argument is + # the path on the host to the actual folder. The second argument is + # the path on the guest to mount the folder. And the optional third + # argument is a set of non-required options. + # config.vm.synced_folder "./", "/vagrant_data" + + # Set www-data so MyRadio/Apache2 is happy. + config.vm.synced_folder ".", "/vagrant", :mount_options => ['dmode=775', 'fmode=775'] + + # Provider-specific configuration so you can fine-tune various + # backing providers for Vagrant. These expose provider-specific options. + # Example for VirtualBox: + # + # config.vm.provider "virtualbox" do |vb| + # # Display the VirtualBox GUI when booting the machine + # vb.gui = true + # + # # Customize the amount of memory on the VM: + # vb.memory = "1024" + # end + # + # View the documentation for the provider you are using for more + # information on available options. + + # Define a Vagrant Push strategy for pushing to Atlas. Other push strategies + # such as FTP and Heroku are also available. See the documentation at + # https://docs.vagrantup.com/v2/push/atlas.html for more information. + # config.push.define "atlas" do |push| + # push.app = "YOUR_ATLAS_USERNAME/YOUR_APPLICATION_NAME" + # end + + # Enable provisioning with a shell script. Additional provisioners such as + # Puppet, Chef, Ansible, Salt, and Docker are also available. Please see the + # documentation for more information about their specific syntax and use. + config.vm.provision :shell, :inline => "sed -i 's#http://us.archive.ubuntu.com/ubuntu/#mirror://mirrors.ubuntu.com/mirrors.txt#g' /etc/apt/sources.list" + config.vm.provision :shell, path: "scripts/bootstrap.sh" + config.trigger.after :up do |trigger| + trigger.name = "Apache2 Restart" + trigger.info = "Restarting Apache2, it doesn't seem to start correctly." + trigger.run_remote = {inline: "service apache2 reload"} + end +end diff --git a/build.xml b/build.xml index 79b6bb8f2..76f433fd4 100644 --- a/build.xml +++ b/build.xml @@ -1,29 +1,14 @@ - - - - - - - - - - - - - - - - + + + @@ -34,94 +19,84 @@ + + + + + + + + - - + + - - - - - - - - - - + + - - - - - - - - - - + + + + + - - - - - - - - - - - + + - + + - - - - - + + + + + + + - + + + diff --git a/build/phpmd.xml b/build/phpmd.xml index 902319305..a32dff83b 100755 --- a/build/phpmd.xml +++ b/build/phpmd.xml @@ -15,4 +15,4 @@ - \ No newline at end of file + diff --git a/codeception.yml b/codeception.yml new file mode 100644 index 000000000..f0446cc8a --- /dev/null +++ b/codeception.yml @@ -0,0 +1,10 @@ +paths: + tests: tests + output: tests/_output + data: tests/_data + support: tests/_support + envs: tests/_envs +actor_suffix: Tester +extensions: + enabled: + - Codeception\Extension\RunFailed diff --git a/composer.json b/composer.json new file mode 100644 index 000000000..b2c93b115 --- /dev/null +++ b/composer.json @@ -0,0 +1,30 @@ +{ + "require": { + "php": ">=7.4", + "james-heinrich/getid3": "~1.9", + "ezyang/htmlpurifier": "~4.10", + "google/recaptcha": "~1.1", + "soundasleep/html2text": "~0.5", + "twig/twig": "~2.4", + "ext-xmlwriter": "*", + "webonyx/graphql-php": "^15.0.0", + "ext-json": "*", + "spatie/icalendar-generator": "^2.1", + "simshaun/recurr": "^4.0", + "geoip2/geoip2": "^2.10.0", + "spatie/icalendar-generator": "^2.1", + "ext-pgsql": "*", + "rbdwllr/reallysimplejwt": "4.0.3" + }, + "require-dev": { + "squizlabs/php_codesniffer": "~3.5", + "pdepend/pdepend": "~2.5", + "phpmd/phpmd": "~2.6", + "theseer/phpdox": "~0.11", + "codeception/codeception": "~4.2", + "flow/jsonpath": "~0.4" + }, + "config": { + "vendor-dir": "src/vendor" + } +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..571f24926 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,44 @@ +version: '3' +services: + postgres: + image: postgres:alpine + environment: + POSTGRES_DB: myradio + POSTGRES_USER: myradio + POSTGRES_PASSWORD: myradio + volumes: + - db-data:/var/lib/postgresql/data + + memcached: + image: memcached:alpine + + mail: + image: mailhog/mailhog:latest + ports: + - 8025:8025 + + myradio: + build: . + depends_on: + - postgres + - memcached + ports: + - 7080:80 + - 4443:443 + volumes: + - ./schema:/var/www/myradio/schema + - ./src:/var/www/myradio/src:rw + # daemon: + # build: . + # depends_on: + # - postgres + # - memcached + # ports: + # - 7080:80 + # - 4443:443 + # volumes: + # - ./schema:/var/www/schema + # - ./src:/var/www/myradio + # command: ['/usr/local/bin/php', '/var/www/myradio/src/Controllers/daemon.php'] +volumes: + db-data: diff --git a/docs/database.md b/docs/database.md new file mode 100644 index 000000000..8da9fb6a3 --- /dev/null +++ b/docs/database.md @@ -0,0 +1,17 @@ +# Pre-Myradio Database +## Building +When myradio is installed, a blank postgres database called "myradio" is made by [bootstrap.sh](../scripts/bootstrap.sh); the [setup process](../src/Controllers/Setup) checks this database to see what is required for myradio to run properly. +The first page that runs is [dbserver.php](../src/Controllers/Setup/dbserver.php), which takes a database hostname and credentials and simply tries to connect. +On failure, it just asks again until you give valid inputs but, on success, it moves to [dbschema.php](../src/Controllers/Setup/dbschema.php). + +This next page tries to find the current state of the database by checking the value of 'myradio.schema.version' and comparing it to a hardcoded constant 'MYRADIO_CURRENT_SCHEMA_VERSION' (defined in [root.php](../src/Controllers/root.php)). +This constant declares the number of [patches](../schema/patches) to be added on top of [base.sql](../schema/base.sql), while the "version" number in the database is the number of patches that have already been applied. +By simply comparing these numbers and patching as appropriate, the page either builds the database from nothing or upgrades an existing one. + +On success, this moves on to [dbdata.php](../src/Controllers/Setup/dbdata.php), which simply reads from the database to setup the site. +By this stage, the actual database setup is complete. + +## Resetting +In the [scripts folder](../scripts) there bash scripts to reset the database, for testing and modifying the database setup. +[reset-db.sh](../scripts/reset-db.sh) is built for the testing system, so has a few lines specific to testing, while [reset-db-v2.sh](../scripts/reset-db-v2.sh) simply drops and recreates the main database. + diff --git a/docs/install.md b/docs/install.md new file mode 100644 index 000000000..512703154 --- /dev/null +++ b/docs/install.md @@ -0,0 +1,159 @@ +# Installing Myradio + +There are a few different ways to install myradio + +## GitHub Codespaces + +If you have access to the GitHub Codespaces beta (if you don't, [sign up here](https://github.com/features/codespaces/signup)), +MyRadio is pre-configured for it, which let you start a version of +Visual Studio Code running in the cloud, alongside a MyRadio instance. + +To do this, go to [Codespaces](https://github.com/UniversityRadioYork/MyRadio/codespaces), +and hit New Codespace. After a minute or so you'll be dropped into a Visual Studio +Code window, running remotely on a GitHub server. Switch to the Ports tab on the bottom (or, +if it doesn't show up, press Ctrl+Shift+P and select `View: Toggle Ports`), +and click on the Local Address next to port 7080 to open a MyRadio browser tab. Then, +follow the Post-Installation steps below from Database onwards - the database hostname is `postgres` and +the database password is `myradio`. + +To debug the code, press F5, or Ctrl+Shift+P and select `Start Debugging`. +You can place breakpoints anywhere in the PHP code by clicking to the left of the line number, and +execution will be paused when that line is reached. + +The codespace includes Mailhog, which will trap any emails sent by MyRadio. To see them, +click the Local Address next to port 8025 in the Ports panel. + +## Docker Install +If you have Docker on your system, use Docker Compose to set up an environment. +Simply run `docker compose up -d`, and visit "https://localhost:4443/myradio/". + +## Vagrant Install +MyRadio comes with a Vagrantfile based on Ubuntu 19.10. +If you have [Vagrant](https://www.vagrantup.com) installed and want to get +developing or playing right away, just run `vagrant up` and a few minutes +later [you'll have a working server](https://localhost:4443/myradio/). + +When you're done run `vagrant halt` to end the process. + +Make sure you have both "vagrant" and "virtualbox" installed and configured. +If this fail, try reinstalling virtualbox and THEN vagrant. + +Note: The Vagrant bootstrap script gives the myradio user CREATEDB permissions +so be sure to never run this in a production environment, or remove the +permission before doing so. + +## Uncontained Install +Install Apache2, PHP, Composer and PostgreSQL on your prefered Unix-based distro. +Or Windows, if you're into that. +MyRadio has been tested with Ubuntu and FreeBSD. + +cd to your MyRadio installation and run `composer install` + +Edit your Apache config as follows +(where /usr/local/www/myradio is your checkout of this repository): + +``` +Alias /myradio /usr/local/www/MyRadio/src/Public + + + Require all granted + AllowOverride None + + +Alias /api /usr/local/www/MyRadio/src/PublicAPI + + Require all granted + AllowOverride None + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ /api/index.php [QSA,L] + +``` + +Restart Apache2, go to http://hostname/myradio + +To make a new postgresql server, run the following after: +``` +pg_createcluster [YOUR_POSTGRES_VERSION] myradio +su postgres +psql +CREATE USER myradio WITH password '[A_STRONG_PASSWORD]'; +CREATE DATABASE myradio WITH OWNER=myradio; +``` + +# Post-Installation + +## Myradio Setup +CONNECT: + - Open up "https://localhost:4443/myradio/" in a browser + - [Use Chrome as this often fails to run on Firefox] + - It will say "connection not private" so press "advanced" and then "proceed" + +DATABASE: + - On the intro screen press "Click here to continue" + - Enter the database details (see Default Credentials) and press Next + - Press "run task", wait a few seconds and then press "run task" again. + - [This method is a workaround for a slight bug in how we build the database] + +USER: + - [Here you can make config changes but the defaults are autofilled] + - Press "complete starting set", scroll and press "save and continue" + - Input any first and last name, an email (NOT an @york.ac.uk email) and a password + - [If you enter an @york.ac.uk email you will not be able to login at all] + - Login using the email and password you just enterted + +## Default Credentials +Database: (when building the database, these credentials are needed) + - Hostname: `postgres` if running in Docker, `localhost` otherwise + - Database: myradio + - Username: myradio + - Password: myradio + +Vagrant VM: (if you need to ssh into the virtual machine) + - Username: vagrant + - Password: vagrant + +## Tests +MyRadio uses [Codeception](http://codeception.com/quickstart) for its test suite. + +[This was written with a Vagrant install in mind - has not been tested on Docker] + +To run the tests, call `src/vendor/bin/codecept run` from the root directory. +By default this assumes that the API is running at http://localhost:7080/api/v2, +as per the Vagrant instance's defaults. It can also use port 80 for the tests, +by running the tests with `--env travis` appended to it. + +A script at `./scripts/reset-db.sh` is provided that creates a Config file and +database structure such that the tests can be run (essentially just runs setup +for you). This script operates on the database directly, so needs to be ran on +the Vagrant instance rather than the host, if that's necesssary. The script +blanks the `myradio_test` database each time it is ran, so it can be used to +reset the database and config file, should this prove necessary. + +Summary: +* `composer install` +* `vagrant up` +* `vagrant ssh -- /vagrant/scripts/reset-db.sh` +* `src/vendor/bin/codecept run` + +The vagrant initialisation script also runs `composer install`, but that is run +on the virtual machine which also installs the PHP extensions required for +Codeception. These extensions may be missing locally, so running composer will +confirm that they are present. + +## Next Steps +Once you've got through the setup wizard, the next thing that's most useful to +you is most likely creating a show. + +To do this, you first need to: +- Create a Term (Show Scheduler -> Manage Terms) +- Create a Show (List My Shows -> Create a Show) +- Apply for a Season of your new Show (List My Shows -> New Season) +- Schedule the Season (Shows Scheduler) + +### A note on Seasons and Terms +MyRadio splits Shows into "Seasons". Any Season is applied to in relation to a +"Term", which is a 10-week space of time. This is because The University of +York has 10 week terms, if you didn't know. + diff --git a/myradio_daemon b/myradio_daemon index a7cdd06e1..bd1f50360 100755 --- a/myradio_daemon +++ b/myradio_daemon @@ -19,20 +19,15 @@ name=myradio_daemon rcvar=myradio_daemon_enable -load_rc_config myradio_daemon_enable +load_rc_config $name : ${myradio_daemon_enable:="NO"} -command=/usr/local/www/myury/src/Controllers/daemon.php -command_interpreter=`which php` -command_args='process >/var/log/ury-org-uk/myradio_daemon.log 2>&1 &' -start_precmd=find_pidfile -stop_precmd=find_pidfile - -find_pidfile() -{ - pidfile='/var/run/myradio_daemon.pid' -} +command=/usr/local/www/myradio/src/Controllers/daemon.php +command_interpreter=/usr/local/bin/php +command_args="process >>/var/log/myradio/daemon.log 2>&1 &" +pidfile="/var/run/${name}.pid" +# Necessary for podcasts (ffmpeg) export PATH=/usr/local/bin:/usr/local/sbin:$PATH run_rc_command "$1" diff --git a/phpdox.xml b/phpdox.xml index c0a00cdc5..f2997e3a1 100755 --- a/phpdox.xml +++ b/phpdox.xml @@ -1,9 +1,17 @@ - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + diff --git a/phpunit.xml b/phpunit.xml index 572595b2e..b822bc496 100755 --- a/phpunit.xml +++ b/phpunit.xml @@ -5,7 +5,7 @@ backupStaticAttributes="false" strict="true" verbose="true"> - + tests/unit/ diff --git a/sample_configs/apache.conf b/sample_configs/apache.conf new file mode 100644 index 000000000..896f0cf37 --- /dev/null +++ b/sample_configs/apache.conf @@ -0,0 +1,73 @@ +# Used to enable local testing. You should of course disable HTTP API access in the real world. + + DocumentRoot /var/www + + RemoteIPTrustedProxyList /etc/apache2/trusted-proxies.txt + RemoteIPHeader X-Real-IP + + Alias /api /var/www/myradio/src/PublicAPI + + Require all granted + AllowOverride None + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ /api/index.php [QSA,L] + + + Alias /myradio /var/www/myradio/src/Public + + Require all granted + AllowOverride None + RewriteEngine On + + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^([^/]+)/([^/]+)/? /myradio/index.php?module=$1&action=$2 [QSA,L] + + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^([^/]+)/? /myradio/index.php?module=$1 [QSA,L] + + + + + + SSLEngine on + SSLCertificateFile /etc/apache2/myradio.crt + SSLCertificateKeyFile /etc/apache2/myradio.key + + ServerAdmin webmaster@localhost + DocumentRoot /var/www + + RemoteIPTrustedProxyList /etc/apache2/trusted-proxies.txt + RemoteIPHeader X-Real-IP + + Alias /myradio /var/www/myradio/src/Public + + Require all granted + AllowOverride None + RewriteEngine On + + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^([^/]+)/([^/]+)/? /myradio/index.php?module=$1&action=$2 [QSA,L] + + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^([^/]+)/? /myradio/index.php?module=$1 [QSA,L] + + + Alias /api /var/www/myradio/src/PublicAPI + + Require all granted + AllowOverride None + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ /api/index.php [QSA,L] + + + php_value post_max_size 20M + php_value upload_max_filesize 20M + diff --git a/sample_configs/codespaces-apache.conf b/sample_configs/codespaces-apache.conf new file mode 100644 index 000000000..f092bb3d1 --- /dev/null +++ b/sample_configs/codespaces-apache.conf @@ -0,0 +1,38 @@ +# Used to enable local testing. You should of course disable HTTP API access in the real world. + + DocumentRoot /var/www + + ErrorLog /var/log/myradio/error.log + + RedirectMatch ^/$ /myradio + + Alias /api /workspaces/MyRadio/src/PublicAPI + + Require all granted + AllowOverride None + php_flag display_errors on + php_flag log_errors on + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ /api/index.php [QSA,L] + + + Alias /myradio /workspaces/MyRadio/src/Public + + Require all granted + AllowOverride None + RewriteEngine On + + php_flag display_errors on + php_flag log_errors on + + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^([^/]+)/([^/]+)/? /myradio/index.php?module=$1&action=$2 [QSA,L] + + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^([^/]+)/? /myradio/index.php?module=$1 [QSA,L] + + diff --git a/sample_configs/codespaces-server-name.conf b/sample_configs/codespaces-server-name.conf new file mode 100644 index 000000000..b9774ea26 --- /dev/null +++ b/sample_configs/codespaces-server-name.conf @@ -0,0 +1 @@ +ServerName ${CODESPACE_NAME}.githubpreview.dev diff --git a/sample_configs/docker-config.php b/sample_configs/docker-config.php new file mode 100644 index 000000000..418b532ac --- /dev/null +++ b/sample_configs/docker-config.php @@ -0,0 +1,19 @@ + + ServerName localhost + DocumentRoot %TRAVIS_BUILD_DIR%/src/Public + + Alias /api %TRAVIS_BUILD_DIR%/src/PublicAPI + + Require all granted + AllowOverride None + RewriteEngine On + RewriteCond %{REQUEST_FILENAME} !-d + RewriteCond %{REQUEST_FILENAME} !-f + RewriteRule ^ /api/index.php [QSA,L] + + + # Wire up Apache to use Travis CI's php-fpm. + + AddHandler php5-fcgi .php + Action php5-fcgi /php5-fcgi + Alias /php5-fcgi /usr/lib/cgi-bin/php5-fcgi + FastCgiExternalServer /usr/lib/cgi-bin/php5-fcgi -host 127.0.0.1:9000 -pass-header Authorization + + Require all granted + + + + ErrorLog "%TRAVIS_BUILD_DIR%/apache-error.log" + CustomLog "%TRAVIS_BUILD_DIR%/apache-access.log" combined + + diff --git a/sample_configs/travis-php.ini b/sample_configs/travis-php.ini new file mode 100644 index 000000000..56693a7b9 --- /dev/null +++ b/sample_configs/travis-php.ini @@ -0,0 +1,4 @@ +; PHP "feature". See https://bugs.php.net/bug.php?id=66763 +always_populate_raw_post_data=-1 +error_reporting=E_ALL +log_errors=1 diff --git a/schema/api.graphql b/schema/api.graphql new file mode 100644 index 000000000..1c326e09c --- /dev/null +++ b/schema/api.graphql @@ -0,0 +1,469 @@ +""" +Bind an object field to a class or method. This shouldn't be used externally and is only used within the schema defition. +Use cases: +1. Inside Query (and eventually Mutation) (needs both class and method) +2. On a field that can't be resolved by the name munging rules (needs method) +""" +directive @bind( + """ + The class that we're binding to. It should be a fully qualified class name, prefixed with a backslash (\\). + """ + class: String, + """ + The method that we're binding to + """ + method: String, + + callingConvention: CallingConvention +) on FIELD_DEFINITION | OBJECT + +enum CallingConvention { + FirstArgCurrentUser, + FirstArgCurrentObject +} + +enum AuthHook { + """ + Only valid on MyRadio_Shows/Seasons/Timeslots. + Grants access if the current user has "View Any Show" or is credited on the show. + """ + ViewShow, + """ + Only valid on MyRadio_User fields. + Grants access if we are looking at the current user, or they have "View Any Profile". + """ + ViewMember, + """ + Only valid on MyRadio_User fields. + Grants access if we are looking at the current user, or they have "View Any Profile", *or* they are an officer. + """ + ViewOfficer +} + +""" +Override authorisation requirements for a field. This shouldn't be used externally and is only used within the schema defition. + +If set on an object, overrides all authentication on that object, unless a field also has @auth. +""" +directive @auth( + """ + The AUTH_XXX constants that will grant access to this method. Note that this uses OR logic, not AND, so any of them + will grant access. + """ + constants: [String!], + """ + Use a custom authentication hook to authenticate this field. + """ + hook: AuthHook +) on FIELD_DEFINITION | OBJECT +directive @meta(key: String!) on FIELD_DEFINITION + +enum Coercion { + "If the value is false, replace it with null." + FalseToNull +} +directive @coerce(hooks: [Coercion]!) on FIELD_DEFINITION + +""" +An RFC3339 date string, such as 2007-12-03 +""" +scalar Date +""" +An RFC3339 time string, such as 10:15:30Z. +""" +scalar Time +""" +An RFC3339 date-time string, such as 2007-12-03T10:15:30Z. +""" +scalar DateTime + +""" +A time duration, such as 01:00:00 +""" +scalar Duration + +""" +Equivalent to a string for all intents and purposes, but used to signal that it may contain arbitrary HTML. +""" +scalar HTMLString + +interface Node { + id: ID! +} + +interface MyRadioObject { + itemId: Int! +} + +type User implements Node & MyRadioObject { + id: ID! @bind(method: "getID") + itemId: Int! @bind(method: "getID") + fname: String! @bind(method: "getFName") + sname: String! @bind(method: "getSName") + + # Public information + profilePhoto: Photo @auth(constants: []) + bio: HTMLString + publicEmail: String + url: String + + # Public information + shows(current_term_only: Boolean): [Show!] @auth(constants: []) + officerships(includeMemberships: Boolean): [UserOfficership!] @auth(constants: []) + + timeline: [UserTimelineEntry!] @auth(constants: []) + + # Semi-public information + phone: String @auth(hook: ViewOfficer) + + # Not public information + allTraining: [UserTrainingStatus] @auth(hook: ViewMember) + accountLocked: Boolean @auth(hook: ViewMember) + college: String @auth(hook: ViewMember) # TODO: consider enum + email: String @auth(hook: ViewMember) + eduroam: String @auth(hook: ViewMember) + localAlias: String @auth(hook: ViewMember) + localName: String @auth(hook: ViewMember) + lastLogin: DateTime @auth(hook: ViewMember) + isCurrentlyPaid: Boolean @auth(hook: ViewMember) + allEmails: [EmailDestination] @auth(hook: ViewMember) +} + +type UserTimelineEntry { + photo: String + message: String + timestamp: DateTime @coerce(hooks: [FalseToNull]) +} + +type Photo implements Node & MyRadioObject{ + id: ID! + itemId: Int! @bind(method: "getID") + dateAdded: DateTime! + format: String! + owner: User! + url: String! +} + +type Officership { + User: User! + from_date: Date! + till_date: Date +} + +type TeamOfficership { + User: User! + from_date: Date! + till_date: Date + position: Officer! +} + +type Team implements Node & MyRadioObject { + id: ID! + itemId: Int! @bind(method: "getID") + name: String! + alias: String + description: String + status: String # TODO enum + currentHolders: [TeamOfficership] + history: [TeamOfficership] +} + +enum OfficerStatus { + Current, + Historic +} + +enum OfficerType { + AssistantHeadOfTeam, + HeadOfTeam, + TeamMember, + Other +} + +type Officer implements Node & MyRadioObject { + id: ID! + itemId: Int! @bind(method: "getID") + team: Team + name: String! + alias: String + description: String + status: OfficerStatus + type: OfficerType + history: [Officership] +} + +type UserOfficership implements Node & MyRadioObject { + id: ID! + itemId: Int! @bind(method: "getID") + user: User! @auth(constants: []) + officer: Officer! @auth(constants: []) + fromDate: Date! + tillDate: Date +} + +type TrainingStatus implements Node & MyRadioObject { + id: ID! + itemId: Int! @bind(method: "getID") + title: String! + detail: String! + depends: TrainingStatus + awarder: TrainingStatus +} + +type UserTrainingStatus implements Node & MyRadioObject @auth(hook: ViewMember) { + id: ID! @bind(method: "getUserTrainingStatusID") + itemId: Int! @bind(method: "getUserTrainingStatusID") + trainingStatusId: ID! @bind(method: "getID") + trainingStatusItemId: Int! @bind(method: "getID") + + title: String! + detail: String! + depends: TrainingStatus + awarder: TrainingStatus + + awardedTo: User! + awardedBy: User! + awardedTime: DateTime! + + revokedBy: User + revokedTime: DateTime @coerce(hooks: [FalseToNull]) +} + +type Album implements Node & MyRadioObject { + id: ID! + itemId: Int! @bind(method: "getID") + + title: String! + artist: String! + tracks: [Track] +} + +type Track implements Node & MyRadioObject { + id: ID! + itemId: Int! @bind(method: "getID") + + artist: String! + album: Album! + title: String! + intro: Int + outro: Int + clean: Boolean + digitised: Boolean! + length: String #Duration + genre: String # TODO enum + digitisedBy: User + lastEditedTime: DateTime + lastEditedUser: User @bind(method: "getLastEditedMemberID") +} + +type ShowSubtype implements Node & MyRadioObject { + id: ID! + itemId: Int! @bind(method: "getID") + name: String! + class: String! + description: String +} + +type ShowCredit { + type: String + User: User +} + +type CreditType { + value: Int! + text: String! +} + +type Genre { + value: Int! + text: String! +} + +type TrackNotRec { + title: String! + artist: String! + album: String! +} + +union TracklistTrack = Track | TrackNotRec + +type TracklistItem implements Node & MyRadioObject { + id: ID! + itemId: Int! @bind(method: "getID") + track: TracklistTrack @auth(constants: []) + startTime: DateTime! +} + +type NowPlayingTrack @auth(constants: []) { + track: TracklistTrack! + start_time: DateTime +} + +type Show implements Node & MyRadioObject @auth(hook: ViewShow) { + id: ID! + itemId: Int! @bind(method: "getID") + title: String! @meta(key: "title") + description: HTMLString! @meta(key: "description") + subtype: ShowSubtype! + photo: String @bind(method: "getShowPhoto") + credits: [ShowCredit] + allSeasons: [Season] +} + +type Season implements Node & MyRadioObject @auth(hook: ViewShow) { + id: ID! + itemId: Int! @bind(method: "getID") + title: String! @meta(key: "title") + description: HTMLString! @meta(key: "description") + credits: [ShowCredit] + show: Show! + subtype: ShowSubtype! + seasonNumber: Int! + allTimeslots: [Timeslot] + firstTime: DateTime @coerce(hooks: [FalseToNull]) +} + +type Message { + read: Boolean! + time: DateTime! + id: ID! + type: Int # TODO: make this an enum + title: String + body: String + source: String + location: [String] +} + +type Timeslot implements Node & MyRadioObject @auth(hook: ViewShow) { + id: ID! + itemId: Int! @bind(method: "getID") + title: String! @meta(key: "title") + description: HTMLString! @meta(key: "description") + season: Season! + timeslotNumber: Int! + startTime: DateTime! + endTime: DateTime! + duration: Duration! + photo: String + messages: [Message] + webpage: String! + + tracklist: [TracklistItem!] @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_TracklistItem", method: "getTracklistForTimeslot", callingConvention: FirstArgCurrentObject) + uploadState: String @meta(key: "upload_state") +} + +type Quote implements Node { + id: ID! + itemId: Int! @bind(method: "getID") + source: User! @auth(constants: []) + date: Date! + text: HTMLString! +} + +type MailingList implements Node & MyRadioObject @auth(constants: []) { + id: ID! + itemId: Int! @bind(method: "getID") + + name: String! + isPublic: Boolean + address: String + + areWeAMember: Boolean! @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_List", method: "isMember", callingConvention: FirstArgCurrentUser) + haveWeOptedOutOfAuto: Boolean! @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_List", method: "hasOptedOutOfAuto", callingConvention: FirstArgCurrentUser) +} + + +type Alias implements Node & MyRadioObject { + id: ID! + itemId: Int! @bind(method: "getID") + + source: String + # TODO: destination - there's no good way to type "string" destinations +} + +union EmailDestinationType = User | Officer | Team | MailingList + +type EmailDestination { + source: String! + reason: String! # TODO enum + destination: EmailDestinationType! @auth(constants: []) + alias: Alias +} + +type MemberSearchResult { + memberid: Int! + fname: String! + sname: String! + eduroam: String + local_alias: String +} + +type FindShowByTitleResult { # TODO: icky + show_id: Int! + title: String! +} + +type CurrentAndNext_PseudoTimeslot { + title: String! + desc: String! + photo: String! + start_time: DateTime + "This will be a string containing either a Unix timestamp, or \"The End of Time\"." + end_time: String! +} + +union CurrentAndNextObject = Timeslot | CurrentAndNext_PseudoTimeslot + +type CurrentAndNext { + current: CurrentAndNextObject! + next: [CurrentAndNextObject!] +} + +type Query { + node(id: ID): Node + + me: User @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_User", method: "getCurrentUser") + + allQuotes: [Quote!] @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Quote", method: "getAll") + + show(itemid: Int!): Show @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Show", method: "getInstance") + season(itemid: Int!): Season @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Season", method: "getInstance") + timeslot(itemid: Int!): Timeslot @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Timeslot", method: "getInstance") + + allShows(current_term_only: Boolean): [Show] @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Show", method: "getAllShows") + + allSubtypes: [ShowSubtype!] @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_ShowSubtype", method: "getAll") @auth(constants: []) + + currentTimeslot: Timeslot @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Timeslot", method: "getCurrentTimeslot") + nextTimeslot: Timeslot @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Timeslot", method: "getNextTimeslot") + nineDaySchedule(weekno: Int!, year: Int): Timeslot @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Timeslot", method: "get9DaySchedule") + currentAndNext(time: Int, n: Int, filter: [Int]): CurrentAndNext @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Timeslot", method: "getCurrentAndNextObjects") @auth(constants: []) + previousTimeslots(time: Int, n: Int, filter: [Int]): [Timeslot] @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Timeslot", method: "getPreviousTimeslots") + + nowPlaying( + "Which source letters (not IDs!) to accept tracklisting from. Defaults to [b, m, o, w, a, s, j]" + sources: [String!], + "Should whatever Jukebox is playing be included even when it's not on air. Silly unless 'j' is passed in sources." + allowOffAir: Boolean + ): NowPlayingTrack @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Track", method: "getNowPlaying") @auth(constants: []) + + isTerm: Boolean @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Scheduler", method: "isTerm") @auth(constants: []) + + user(itemid: Int!): User @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_User", method: "getInstance") + + officer(itemid: Int!): Officer @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Officer", method: "getInstance") + + team(itemid: Int!): Team @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Team", method: "getInstance") + + allTrainingStatuses: [TrainingStatus!] @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_TrainingStatus", method: "getAll") + + allMailingLists: [MailingList!] @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_List", method: "getAllLists") + mailingListByName(name: String!): MailingList @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_List", method: "getByName") + + allGenres: [Genre!] @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Scheduler", method: "getGenres") @auth(constants: []) + allCreditTypes: [CreditType!] @bind(class: "\\MyRadio\\ServiceAPI\\MyRadio_Scheduler", method: "getCreditTypes") @auth(constants: []) +} + +schema { + query: Query +} diff --git a/schema/api.json b/schema/api.json new file mode 100644 index 000000000..9ebd09a0e --- /dev/null +++ b/schema/api.json @@ -0,0 +1,161 @@ +{ + "classes": { + "\\MyRadio\\ServiceAPI\\MyRadio_Track": "track", + "\\MyRadio\\ServiceAPI\\MyRadio_Show": "show", + "\\MyRadio\\ServiceAPI\\MyRadio_Season": "season", + "\\MyRadio\\ServiceAPI\\MyRadio_Timeslot": "timeslot", + "\\MyRadio\\ServiceAPI\\MyRadio_Album": "album", + "\\MyRadio\\ServiceAPI\\MyRadio_Demo": "demo", + "\\MyRadio\\ServiceAPI\\MyRadio_List": "list", + "\\MyRadio\\ServiceAPI\\MyRadio_Photo": "photo", + "\\MyRadio\\ServiceAPI\\MyRadio_Podcast": "podcast", + "\\MyRadio\\ServiceAPI\\MyRadio_Scheduler": "scheduler", + "\\MyRadio\\ServiceAPI\\MyRadio_TrackCorrection": "trackCorrection", + "\\MyRadio\\ServiceAPI\\MyRadio_TrainingStatus": "training", + "\\MyRadio\\ServiceAPI\\MyRadio_UserTrainingStatus": "userTraining", + "\\MyRadio\\ServiceAPI\\MyRadio_Selector": "selector", + "\\MyRadio\\ServiceAPI\\MyRadio_Alias": "alias", + "\\MyRadio\\ServiceAPI\\MyRadio_Officer": "officer", + "\\MyRadio\\ServiceAPI\\MyRadio_Team": "team", + "\\MyRadio\\ServiceAPI\\MyRadio_TracklistItem": "tracklistItem", + "\\MyRadio\\ServiceAPI\\MyRadio_User": "user", + "\\MyRadio\\ServiceAPI\\MyRadio_Banner": "banner", + "\\MyRadio\\ServiceAPI\\MyRadio_BannerCampaign": "bannerCampaign", + "\\MyRadio\\ServiceAPI\\MyRadio_Artist": "artist", + "\\MyRadio\\ServiceAPI\\MyRadio_Webcam": "webcam", + "\\MyRadio\\ServiceAPI\\MyRadio_ShowSubtype": "showSubtype", + "\\MyRadio\\ServiceAPI\\MyRadio_Event": "event", + "\\MyRadio\\ServiceAPI\\MyRadio_ShortURL": "shortUrl", + "\\MyRadio\\iTones\\iTones_Utils": "iTones", + "\\MyRadio\\iTones\\iTones_Playlist": "playlist", + "\\MyRadio\\MyRadio\\AuthUtils": "auth", + "\\MyRadio\\MyRadio\\CoreUtils": "utils", + "\\MyRadio\\MyRadio\\MyRadioNews": "news", + "\\MyRadio\\NIPSWeb\\NIPSWeb_ManagedUserPlaylist": "nipswebUserPlaylist", + "\\MyRadio\\NIPSWeb\\NIPSWeb_ManagedPlaylist": "nipswebPlaylist", + "\\MyRadio\\NIPSWeb\\NIPSWeb_TimeslotItem": "timeslotItem", + "\\MyRadio\\NIPSWeb\\NIPSWeb_ManagedItem": "nipswebItem", + "\\MyRadio\\Config": "config", + "\\MyRadio\\ServiceAPI\\Profile": "profile", + "\\MyRadio\\ServiceAPI\\MyRadio_Term": "term" + }, + "specs": { + "quote": { + "required": ["text", "source", "date"], + "properties": { + "text": { + "type": "string" + }, + "source": { + "type": "string" + }, + "date": { + "type": "integer" + } + } + }, + "show": { + "required": ["title", "description", "credits"], + "properties": { + "show_id": { + "type": "integer", + "readOnly": true + }, + "title": { + "type": "string" + }, + "description": { + "type": "string" + }, + "credits": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["type", "memberid"], + "properties": { + "type": { + "type": "integer" + }, + "memberid": { + "type": "integer" + }, + "User": { + "readOnly": true + } + } + } + }, + "show_type_id": { + "type": "integer", + "default": 1 + }, + "seasons": { + "type": "integer", + "description": "The number of seasons attached to this show.", + "readOnly": true + }, + "tag": { + "type": "array", + "items": { + "type": "string" + } + }, + "upload_state": { + "type": "string", + "description": "If mixclouder is available, setting this to 'Requested' will make broadcasts of this show available on Mixcloud after broadcast. It will change to a progress status and finally to Mixcloud ID of the ondemand." + }, + "image": { + "type": "string", + "readOnly": true + } + } + }, + "user": { + "required": ["fname", "sname"], + "properties": { + "fname": { + "type": "string" + }, + "sname": { + "type": "string" + }, + "eduroam": { + "type": "string" + }, + "sex": { + "type": "string", + "enum": ["m", "f", "o"], + "default": "o" + }, + "collegeid": { + "type": "integer", + "description": "Default configurable" + }, + "email": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "receive_email": { + "type": "boolean", + "default": true + }, + "paid": { + "type": "number", + "default": 0.00 + }, + "provided_pass": { + "type": "string" + } + }, + "anyOf": [{ + "required": ["eduroam"] + }, + { + "required": ["email"] + }] + } + } +} diff --git a/schema/base.sql b/schema/base.sql new file mode 100644 index 000000000..ee39f2799 --- /dev/null +++ b/schema/base.sql @@ -0,0 +1,7136 @@ +CREATE SCHEMA bapsplanner; +COMMENT ON SCHEMA bapsplanner IS 'Used by Show Planner.'; +CREATE SCHEMA jukebox; +COMMENT ON SCHEMA jukebox IS 'Used by jukebox auto-music-player.'; +CREATE SCHEMA mail; +COMMENT ON SCHEMA mail IS 'Mailing lists.'; +CREATE SCHEMA metadata; +COMMENT ON SCHEMA metadata IS 'Metadata systems.'; +CREATE SCHEMA music; +COMMENT ON SCHEMA music IS 'Charts and possibly other stuff too.'; +CREATE SCHEMA myury; +COMMENT ON SCHEMA myury IS 'Schema for new/migrated data for the Members Internal replacement'; +CREATE SCHEMA people; +COMMENT ON SCHEMA people IS 'Tables for the LeRouge Extensible Roles (Or User Groups).'; +CREATE SCHEMA schedule; +COMMENT ON SCHEMA schedule IS 'Schema for the MyRadio schedule.'; +CREATE SCHEMA sis2; +COMMENT ON SCHEMA sis2 IS 'Used by Studio Infomation Service.'; +CREATE SCHEMA tracklist; +COMMENT ON SCHEMA tracklist IS 'Provides a schema that logs played out tracks for PPL track returns'; +CREATE SCHEMA uryplayer; +COMMENT ON SCHEMA uryplayer IS 'URY Player'; +CREATE SCHEMA webcam; +CREATE SCHEMA website; +COMMENT ON SCHEMA website IS 'Collection of data relating to the operation of the public-facing website.'; +CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; +CREATE FUNCTION bapstotracklist() RETURNS TRIGGER AS $$ +DECLARE + audid INTEGER; +BEGIN + IF ((TG_OP = 'UPDATE') + AND ((SELECT COUNT(*) FROM (SELECT sel.action FROM selector sel + WHERE sel.action >= 4 AND sel.action <= 11 + ORDER BY sel.TIME DESC LIMIT 1) AS seltop + INNER JOIN tracklist.selbaps bsel ON (seltop.action = bsel.selaction) + WHERE bsel.bapsloc = NEW."serverid" AND (NEW."timeplayed" >= (SELECT sel.TIME FROM selector sel + ORDER BY sel.TIME DESC + LIMIT 1))) = 1) + AND ((SELECT COUNT(*) FROM baps_audio ba WHERE ba.audioid = NEW."audioid" AND ba.trackid > 0) = 1) + AND ((NEW."timestopped" - NEW."timeplayed" > '00:00:30')) + AND ((SELECT COUNT(*) FROM tracklist.tracklist WHERE bapsaudioid = NEW."audiologid") = 0)) + THEN + INSERT INTO tracklist.tracklist (SOURCE, timestart, timestop, timeslotid, bapsaudioid) + VALUES ('b', NEW."timeplayed", NEW."timestopped", (SELECT show_season_timeslot_id FROM schedule.show_season_timeslot + WHERE start_time <= NOW() + AND (start_time + duration) >= NOW() + AND show_season_id != 0 + ORDER BY show_season_timeslot_id ASC + LIMIT 1), + NEW."audiologid") + RETURNING audiologid INTO audid; + INSERT INTO tracklist.track_rec + VALUES ("audid", (SELECT rec.recordid FROM rec_track rec + INNER JOIN baps_audio ba USING (trackid) + WHERE ba.audioid = NEW."audioid"), + (SELECT trackid FROM baps_audio WHERE audioid = NEW."audioid")); + END IF; + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE FUNCTION clear_item_func() RETURNS trigger AS $$ +BEGIN + IF OLD.textitemid IS NOT NULL + THEN + DELETE FROM baps_textitem WHERE textitemid = OLD.textitemid; + END IF; + IF OLD.libraryitemid IS NOT NULL + THEN + DELETE FROM baps_libraryitem WHERE libraryitemid = OLD.libraryitemid; + END IF; + IF OLD.fileitemid IS NOT NULL + THEN + DELETE FROM baps_fileitem WHERE fileitemid = OLD.fileitemid; + END IF; + RETURN OLD; +END; +$$ LANGUAGE plpgsql; + +CREATE FUNCTION process_gammu_text() RETURNS trigger AS $$ +BEGIN + IF (TG_OP = 'INSERT') + THEN + IF ((SELECT show_season_timeslot_id FROM schedule.show_season_timeslot + WHERE start_time <= NOW() AND (start_time + duration) >= NOW() + ORDER BY show_season_timeslot_id ASC + LIMIT 1) IS NOT NULL) + THEN + INSERT INTO sis2.messages (commtypeid, timeslotid, sender, subject, content, statusid) + VALUES (2, (SELECT show_season_timeslot_id FROM schedule.show_season_timeslot + WHERE start_time <= NOW() AND (start_time + duration) >= NOW() + ORDER BY show_season_timeslot_id ASC + LIMIT 1), + NEW."SenderNumber", NEW."TextDecoded", NEW."TextDecoded", 1); + RETURN NEW; + ELSE + INSERT INTO sis2.messages (commtypeid, timeslotid, sender, subject, content, statusid) + VALUES (2, 118540, NEW."SenderNumber", NEW."TextDecoded", NEW."TextDecoded", 1); + RETURN NEW; + END IF; + END IF; + RETURN NULL; +END; +$$ LANGUAGE plpgsql; + +CREATE FUNCTION set_shelfcode_func() RETURNS trigger AS $$ +DECLARE + myshelfnumber integer DEFAULT 0; + recordrow RECORD; +BEGIN + IF ((NEW.media = '7' OR NEW.media = '2') AND NEW.format = 'a') + THEN + FOR recordrow IN (SELECT * FROM rec_record + WHERE media = NEW.media AND (format = '7' OR format = '2') AND shelfletter = NEW.shelfletter + ORDER BY shelfnumber) + LOOP + IF (recordrow.shelfnumber > myshelfnumber + 1) + THEN + EXIT; + END IF; + myshelfnumber = myshelfnumber + 1; + END LOOP; + ELSE + FOR recordrow IN (SELECT * FROM rec_record + WHERE media = NEW.media AND format = NEW.format AND shelfletter = NEW.shelfletter + ORDER BY shelfnumber) + LOOP + IF (recordrow.shelfnumber > myshelfnumber + 1) + THEN + EXIT; + END IF; + myshelfnumber = myshelfnumber + 1; + END LOOP; + END IF; + NEW.shelfnumber = myshelfnumber + 1; +RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE FUNCTION update_timestamp() RETURNS trigger AS $$ +BEGIN + NEW."UpdatedInDB" := LOCALTIMESTAMP(0); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE SEQUENCE bapsplanner.auto_playlists_autoplaylistid_seq + START WITH 3 + INCREMENT BY 1 + MINVALUE 3 + NO MAXVALUE + CACHE 1; +CREATE TABLE bapsplanner.auto_playlists ( + auto_playlist_id integer DEFAULT nextval('bapsplanner.auto_playlists_autoplaylistid_seq'::regclass) NOT NULL, + name character varying(30) NOT NULL, + query text +); +CREATE SEQUENCE bapsplanner.client_ids_client_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +CREATE TABLE bapsplanner.client_ids ( + client_id integer DEFAULT nextval('bapsplanner.client_ids_client_id_seq'::regclass) NOT NULL, + show_season_timeslot_id integer, + session_id character varying(64) +); +CREATE SEQUENCE bapsplanner.managed_items_manageditemid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +CREATE TABLE bapsplanner.managed_items ( + manageditemid integer DEFAULT nextval('bapsplanner.managed_items_manageditemid_seq'::regclass) NOT NULL, + managedplaylistid integer NOT NULL, + title character varying NOT NULL, + length time without time zone, + bpm smallint, + expirydate date, + memberid integer +); +CREATE SEQUENCE bapsplanner.managed_items_managedplaylistid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE bapsplanner.managed_items_managedplaylistid_seq OWNED BY bapsplanner.managed_items.managedplaylistid; +CREATE TABLE bapsplanner.managed_playlists ( + managedplaylistid integer NOT NULL, + name character varying, + folder character varying, + item_ttl integer +); +COMMENT ON COLUMN bapsplanner.managed_playlists.item_ttl IS 'The default period of time an item in this playlist will live for, in seconds. A value of NULL will not expire.'; +CREATE SEQUENCE bapsplanner.managed_playlists_managedplaylistid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE bapsplanner.managed_playlists_managedplaylistid_seq OWNED BY bapsplanner.managed_playlists.managedplaylistid; +CREATE TABLE bapsplanner.managed_user_items ( + manageditemid integer DEFAULT nextval('bapsplanner.managed_items_manageditemid_seq'::regclass) NOT NULL, + managedplaylistid character varying(35) NOT NULL, + title character varying NOT NULL, + length time without time zone, + bpm smallint +); +CREATE TABLE bapsplanner.secure_play_token ( + sessionid character varying(32) NOT NULL, + memberid integer NOT NULL, + "timestamp" timestamp without time zone DEFAULT now() NOT NULL, + trackid integer NOT NULL +); +COMMENT ON TABLE bapsplanner.secure_play_token IS 'Stores ''tokens'' that allow a user to play a file - once the file is played the token is removed preventing downloads or sharing.'; +CREATE TABLE bapsplanner.timeslot_items ( + timeslot_item_id integer NOT NULL, + timeslot_id integer NOT NULL, + channel_id smallint NOT NULL, + weight integer NOT NULL, + cue integer NOT NULL DEFAULT 0, + rec_track_id integer, + managed_item_id integer, + user_item_id integer, + legacy_aux_id integer +); +CREATE SEQUENCE bapsplanner.timeslot_items_timeslot_item_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE bapsplanner.timeslot_items_timeslot_item_id_seq OWNED BY bapsplanner.timeslot_items.timeslot_item_id; +SET search_path = jukebox, pg_catalog; +CREATE SEQUENCE playlist_availability_playlist_availability_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +CREATE TABLE playlist_availability ( + memberid integer, + approvedid integer, + effective_from timestamp with time zone NOT NULL, + effective_to timestamp with time zone, + playlist_availability_id integer DEFAULT nextval('jukebox.playlist_availability_playlist_availability_id_seq'::regclass) NOT NULL, + weight integer NOT NULL, + playlistid character varying NOT NULL +); +ALTER SEQUENCE playlist_availability_playlist_availability_id_seq OWNED BY playlist_availability.playlist_availability_id; +CREATE TABLE playlist_entries ( + playlistid character varying(15) NOT NULL, + trackid integer NOT NULL, + revision_added integer NOT NULL, + revision_removed integer, + entryid integer NOT NULL +); +CREATE SEQUENCE playlist_entries_entryid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE playlist_entries_entryid_seq OWNED BY playlist_entries.entryid; +CREATE TABLE playlist_revisions ( + playlistid character varying(15) NOT NULL, + revisionid integer NOT NULL, + "timestamp" timestamp without time zone DEFAULT now() NOT NULL, + author integer NOT NULL, + notes text +); +CREATE SEQUENCE playlist_timeslot_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +CREATE TABLE playlist_timeslot ( + id integer DEFAULT nextval('playlist_timeslot_id_seq'::regclass) NOT NULL, + memberid integer NOT NULL, + approvedid integer, + day smallint NOT NULL, + start_time time without time zone NOT NULL, + end_time time without time zone NOT NULL, + playlist_availability_id integer NOT NULL +); +COMMENT ON COLUMN playlist_timeslot.day IS '1-7 (1=Monday)'; +CREATE TABLE playlists ( + playlistid character varying(15) NOT NULL, + title character varying(50) NOT NULL, + image character varying(50) DEFAULT 'music_note.png'::character varying, + description character varying(250), + lock integer, + locktime integer, + weight integer DEFAULT 0 NOT NULL, + exported boolean DEFAULT true NOT NULL, + weightx integer DEFAULT 0 NOT NULL +); +COMMENT ON COLUMN playlists.weight IS 'Relative to the other playlists, how often should a track from this be played on the jukebox? If 0, never play it.'; +COMMENT ON COLUMN playlists.weightx IS 'Weightings for post-watershed jukebox.'; +CREATE TABLE request ( + request_id integer NOT NULL, + memberid integer NOT NULL, + date timestamp with time zone NOT NULL, + queue character varying NOT NULL, + trackid integer NOT NULL +); +CREATE SEQUENCE request_request_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE request_request_id_seq OWNED BY request.request_id; +CREATE TABLE silence_log ( + silenceid integer NOT NULL, + starttime timestamp without time zone DEFAULT now() NOT NULL, + stoptime timestamp without time zone, + handledby integer +); +COMMENT ON TABLE silence_log IS 'Stores a log of silence events'; +CREATE VIEW silence AS + SELECT silence_log.starttime FROM silence_log WHERE (silence_log.stoptime = NULL::timestamp without time zone) ORDER BY silence_log.silenceid LIMIT 1; +COMMENT ON VIEW silence IS 'Gets start time of a current silence event. Should check if time is > 1 second'; +CREATE SEQUENCE silence_log_silenceid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE silence_log_silenceid_seq OWNED BY silence_log.silenceid; +CREATE TABLE track_blacklist ( + trackid integer NOT NULL +); +COMMENT ON TABLE track_blacklist IS 'A list of Tracks that should, under no circumstances, be played on the jukebox.'; +SET search_path = mail, pg_catalog; +CREATE TABLE alias ( + alias_id integer NOT NULL, + source character varying NOT NULL +); +CREATE SEQUENCE alias_alias_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE alias_alias_id_seq OWNED BY alias.alias_id; +CREATE TABLE alias_list ( + alias_id integer NOT NULL, + destination integer NOT NULL +); +CREATE TABLE alias_member ( + alias_id integer NOT NULL, + destination integer NOT NULL +); +CREATE TABLE alias_officer ( + alias_id integer NOT NULL, + destination integer NOT NULL +); +CREATE SEQUENCE alias_source_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +CREATE TABLE alias_text ( + alias_id integer NOT NULL, + destination character varying NOT NULL +); +CREATE TABLE email ( + email_id integer NOT NULL, + sender integer, + subject text NOT NULL, + body text NOT NULL, + "timestamp" timestamp without time zone DEFAULT now() +); +CREATE TABLE email_recipient_list ( + email_id integer NOT NULL, + listid integer NOT NULL, + sent boolean DEFAULT false NOT NULL +); +CREATE TABLE email_recipient_member ( + email_id integer NOT NULL, + memberid integer NOT NULL, + sent boolean DEFAULT false NOT NULL +); +CREATE SEQUENCE emails_email_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE emails_email_id_seq OWNED BY email.email_id; +SET search_path = metadata, pg_catalog; +CREATE TABLE metadata_key ( + metadata_key_id integer NOT NULL, + name character varying NOT NULL, + allow_multiple boolean DEFAULT false, + description text DEFAULT ''::text NOT NULL, + cache_duration integer DEFAULT 300 NOT NULL, + plural character varying(255), + searchable boolean DEFAULT false NOT NULL +); +COMMENT ON TABLE metadata_key IS 'Stores possible types of textual metadatum. Used by all three _metadata tables'; +COMMENT ON COLUMN metadata_key.metadata_key_id IS 'A unique identifier for each metadata type'; +COMMENT ON COLUMN metadata_key.name IS 'A human-readable name for the metadata key'; +COMMENT ON COLUMN metadata_key.description IS 'A short description of the semantics/meaning of this key, and where it is applicable.'; +CREATE SEQUENCE metadata_key_metadata_key_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE metadata_key_metadata_key_id_seq OWNED BY metadata_key.metadata_key_id; +CREATE TABLE package ( + name character varying(50) NOT NULL, + description text NOT NULL, + package_id integer NOT NULL, + weight integer +); +CREATE TABLE package_image_metadata ( + memberid integer, + approvedid integer, + effective_from timestamp with time zone, + effective_to timestamp with time zone, + metadata_key_id integer NOT NULL, + metadata_value character varying(100) NOT NULL, + element_id integer NOT NULL, + package_image_metadata_id integer NOT NULL, + package_id integer +); +CREATE SEQUENCE package_image_metadata_package_image_metadata_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE package_image_metadata_package_image_metadata_id_seq OWNED BY package_image_metadata.package_image_metadata_id; +CREATE SEQUENCE package_package_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE package_package_id_seq OWNED BY package.package_id; +CREATE TABLE package_text_metadata ( + memberid integer, + approvedid integer, + effective_from timestamp with time zone, + effective_to timestamp with time zone, + metadata_key_id integer NOT NULL, + metadata_value text NOT NULL, + element_id integer NOT NULL, + package_text_metadata_id integer NOT NULL, + package_id integer +); +CREATE SEQUENCE package_text_metadata_package_text_metadata_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE package_text_metadata_package_text_metadata_id_seq OWNED BY package_text_metadata.package_text_metadata_id; +SET search_path = music, pg_catalog; +CREATE TABLE chart_release ( + submitted timestamp with time zone, + chart_release_id integer NOT NULL, + chart_type_id integer NOT NULL +); +CREATE SEQUENCE chart_release_chart_release_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE chart_release_chart_release_id_seq OWNED BY chart_release.chart_release_id; +CREATE TABLE chart_row ( + chart_row_id integer NOT NULL, + chart_release_id integer NOT NULL, + "position" smallint NOT NULL, + track character varying(255), + artist character varying(255), + trackid integer, + CONSTRAINT chart_row_position_check CHECK (("position" >= 0)) +); +CREATE SEQUENCE chart_row_chart_row_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE chart_row_chart_row_id_seq OWNED BY chart_row.chart_row_id; +CREATE TABLE chart_type ( + name character varying(50) NOT NULL, + description text NOT NULL, + chart_type_id integer NOT NULL +); +CREATE SEQUENCE chart_type_chart_type_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE chart_type_chart_type_id_seq OWNED BY chart_type.chart_type_id; +SET search_path = myury, pg_catalog; +CREATE TABLE act_permission ( + actpermissionid integer NOT NULL, + serviceid integer NOT NULL, + moduleid integer, + actionid integer, + typeid integer +); +COMMENT ON TABLE act_permission IS 'Specifies what permissions are required in order to use a feature. This is an *OR* type permission system - any of these matching will grant access. +A NULL in the module or action field matches any module or action. +A NULL permissionid means that no permissions are required to use that Service/Module/Action combination. +NULL permissions on wildcards will be ignored.'; +CREATE SEQUENCE act_permission_actpermissionid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE act_permission_actpermissionid_seq OWNED BY act_permission.actpermissionid; +CREATE TABLE actions ( + actionid integer NOT NULL, + moduleid integer, + name character varying, + enabled boolean DEFAULT true NOT NULL, + custom_uri character varying +); +COMMENT ON TABLE actions IS 'Stores Actions within managed MyRadio Service Modules'; +CREATE SEQUENCE actions_actionid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE actions_actionid_seq OWNED BY actions.actionid; +CREATE TABLE api_class_map ( + api_map_id integer NOT NULL, + class_name character varying NOT NULL, + api_name character varying NOT NULL +); +COMMENT ON TABLE api_class_map IS 'Maps MyRadio Internal classes to the names exposed to the MyRadio API. For example, MyRadio_Track would map to Track. If a class is not mapped, it is not available at all to the API.'; +CREATE SEQUENCE api_class_map_api_map_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE api_class_map_api_map_id_seq OWNED BY api_class_map.api_map_id; + +CREATE TABLE api_key ( + key_string character varying NOT NULL, + description character varying NOT NULL, + revoked boolean DEFAULT false NOT NULL +); +COMMENT ON TABLE api_key IS 'Access keys for the MyRadiopi'; +INSERT INTO api_key (key_string, description) VALUES ('IUrnsb8AMkjqDRdfXvOMe3DqHLW8HJ1RNBPNJq3H1FQpiwQDs7Ufoxmsf5xZE9XEbQErRO97DG4xfyVAO7LuS2dOiVNZYoxkk4fEhDt8wR4sLXbghidtM5rLHcgkzO10', 'Swagger Documentation Key'); +CREATE TABLE api_key_auth ( + key_string character varying NOT NULL, + typeid integer NOT NULL +); +COMMENT ON TABLE api_key_auth IS 'Stores what API capabilities each key has.'; +CREATE TABLE api_method_auth ( + api_method_auth_id integer NOT NULL, + class_name character varying NOT NULL, + method_name character varying, + typeid integer +); +COMMENT ON TABLE api_method_auth IS 'Assigns permissions to API calls. If a Class or Object Method does not have a permission here, it is accessible only to Keys with "AUTH_APISUDO". +Other than the above exception, the permissions structure is identical to the standard myury action permission system.'; +CREATE SEQUENCE api_method_auth_api_method_auth_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE api_method_auth_api_method_auth_id_seq OWNED BY api_method_auth.api_method_auth_id; + +CREATE TABLE award_categories ( + awardid integer NOT NULL, + name character varying NOT NULL +); +CREATE SEQUENCE award_categories_awardid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE award_categories_awardid_seq OWNED BY award_categories.awardid; +CREATE TABLE award_member ( + awardmemberid integer NOT NULL, + awardid integer NOT NULL, + memberid integer NOT NULL, + awarded timestamp without time zone DEFAULT now() NOT NULL, + awardedby integer NOT NULL +); +CREATE SEQUENCE award_member_awardmemberid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE award_member_awardmemberid_seq OWNED BY award_member.awardmemberid; +CREATE TABLE modules ( + moduleid integer NOT NULL, + serviceid integer, + name character varying, + enabled boolean DEFAULT true NOT NULL +); +COMMENT ON TABLE modules IS 'Stores Modules within MyRadio managed Services'; +CREATE SEQUENCE modules_moduleid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE modules_moduleid_seq OWNED BY modules.moduleid; +CREATE TABLE password_reset_token ( + token character varying NOT NULL, + expires timestamp without time zone NOT NULL, + used timestamp without time zone, + memberid integer NOT NULL +); +COMMENT ON TABLE password_reset_token IS 'Tokens used for sending password reset emails.'; +CREATE TABLE photos ( + photoid integer NOT NULL, + owner integer, + date_added timestamp without time zone DEFAULT now() NOT NULL, + format character varying DEFAULT 'png'::character varying NOT NULL +); +COMMENT ON COLUMN photos.format IS 'png, jpeg etc - should be the file extension.'; +CREATE SEQUENCE photos_photoid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE photos_photoid_seq OWNED BY photos.photoid; +CREATE TABLE services ( + serviceid integer NOT NULL, + name character varying, + enabled boolean DEFAULT true NOT NULL +); +COMMENT ON TABLE services IS 'Lists all Services managed by MyRadio'; +CREATE SEQUENCE services_serviceid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE services_serviceid_seq OWNED BY services.serviceid; +CREATE TABLE services_versions ( + serviceversionid integer NOT NULL, + serviceid integer NOT NULL, + version character varying NOT NULL, + path character varying NOT NULL, + is_default boolean DEFAULT false NOT NULL, + proxy_static boolean DEFAULT false +); +COMMENT ON COLUMN services_versions.proxy_static IS 'If true, Twig will be given a base url that proxies all static resources through a JS script to ensure the right version of the file is served.'; +CREATE TABLE services_versions_member ( + memberid integer NOT NULL, + serviceversionid integer NOT NULL +); +CREATE SEQUENCE services_versions_serviceversionid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE services_versions_serviceversionid_seq OWNED BY services_versions.serviceversionid; + +CREATE TABLE api_key_log ( + api_log_id integer NOT NULL, + key_string character varying NOT NULL, + "timestamp" timestamp without time zone DEFAULT now() NOT NULL, + remote_ip inet NOT NULL, + request_path character varying, + request_params json +); +COMMENT ON TABLE api_key_log IS 'Stores a record of API Requests by an API Key'; +CREATE SEQUENCE api_key_log_api_log_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE api_key_log_api_log_id_seq OWNED BY api_key_log.api_log_id; +SET search_path = people, pg_catalog; +CREATE TABLE credit_type ( + credit_type_id integer NOT NULL, + name character varying(255) NOT NULL, + plural character varying(255) NOT NULL, + is_in_byline boolean DEFAULT false NOT NULL +); +COMMENT ON TABLE credit_type IS 'Types of credit (associations between URY people and items such as shows or podcasts they have taken a role in creating).'; +COMMENT ON COLUMN credit_type.plural IS 'A human-readable plural form of the show credit name, for example "presenters" or "producers".'; +COMMENT ON COLUMN credit_type.is_in_byline IS 'If true, people credited with this credit type will appear in "with XYZ and ABC" by-lines for the show.'; +CREATE TABLE group_root_role ( + group_root_role_id integer NOT NULL, + role_id_id integer NOT NULL, + group_type_id integer NOT NULL, + group_leader_id integer +); +COMMENT ON COLUMN group_root_role.group_leader_id IS 'An optional reference to a role to be considered the ''leader'' role within the group defined by this root.'; +CREATE SEQUENCE group_root_role_group_root_role_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE group_root_role_group_root_role_id_seq OWNED BY group_root_role.group_root_role_id; +CREATE TABLE group_type ( + group_type_id integer NOT NULL, + name character varying(20) NOT NULL +); +CREATE SEQUENCE group_type_group_type_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE group_type_group_type_id_seq OWNED BY group_type.group_type_id; +CREATE TABLE metadata ( + roleid integer NOT NULL, + key text NOT NULL, + value text NOT NULL +); +COMMENT ON TABLE metadata IS 'Key-value store for textual metadata associated with rules. +As of writing the metadata schema is: +''Collective-Name'' - human readable name for the group of everyone in this role, example ''Station Managers''. This is used as the team name when the role is a grouproot. +''Individual-Name'' - human readable name for an individual in this role, example ''Station Manager''. +''Acronym'' - self-explanatory, example ''SM''. +''Constitution-Section'' - section of the URY constitution defining the role, example: ''4.2.1''. +''Description'' - short description of the role, example: ''Manages the station''.'; +COMMENT ON COLUMN metadata.roleid IS 'The unique ID of the role this metadatum concerns.'; +COMMENT ON COLUMN metadata.key IS 'The key of the metadatum; should fit the site schema.'; +COMMENT ON COLUMN metadata.value IS 'The value of the metadatum.'; +CREATE TABLE role ( + role_id integer NOT NULL, + alias character varying(100) NOT NULL, + visibilitylevel integer DEFAULT 1 NOT NULL, + isactive boolean DEFAULT true NOT NULL, + ordering integer DEFAULT 1 NOT NULL +); +COMMENT ON TABLE role IS 'Role definitions for LeRouge'; +COMMENT ON COLUMN role.role_id IS 'The unique ID of this role.'; +COMMENT ON COLUMN role.alias IS 'The internal alias name of this role. (Human readable name is considered metadata)'; +COMMENT ON COLUMN role.visibilitylevel IS 'The visibility level of this role; see roles.visibilities.'; +COMMENT ON COLUMN role.isactive IS 'If false, this role should be ignored completely during any role actions.'; +COMMENT ON COLUMN role.ordering IS 'Coefficient used to determine how high up in lists this role appears (the lower, the more senior). For officer positions this should reflect the constitutional pecking order; for teams, it just defines the order of teams in any ordered team lists.'; +CREATE TABLE role_inheritance ( + child_id integer NOT NULL, + parent_id integer NOT NULL, + role_inheritance_id integer NOT NULL +); +COMMENT ON TABLE role_inheritance IS 'Pairs of roles and their immediate parents, used to create the role inheritance graph.'; +COMMENT ON COLUMN role_inheritance.child_id IS 'The unique ID of the child row.'; +COMMENT ON COLUMN role_inheritance.parent_id IS 'The unique ID of the parent row.'; +COMMENT ON COLUMN role_inheritance.role_inheritance_id IS 'The unique ID of this inheritance binding.'; +CREATE SEQUENCE role_inheritance_role_inheritance_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE role_inheritance_role_inheritance_id_seq OWNED BY role_inheritance.role_inheritance_id; +CREATE TABLE role_text_metadata ( + metadata_key_id integer NOT NULL, + role_id integer NOT NULL, + metadata_value text, + effective_from timestamp with time zone, + memberid integer NOT NULL, + approvedid integer, + role_text_metadata_id integer NOT NULL, + effective_to timestamp with time zone +); +COMMENT ON COLUMN role_text_metadata.role_id IS 'The ID of the role this metadatum concerns.'; +COMMENT ON COLUMN role_text_metadata.role_text_metadata_id IS 'The unique numeric ID of this metadatum.'; +CREATE SEQUENCE role_metadata_role_metadata_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE role_metadata_role_metadata_id_seq OWNED BY role_text_metadata.role_text_metadata_id; +CREATE TABLE role_visibility ( + role_visibility_id integer NOT NULL, + name character varying(100) NOT NULL, + description text +); +COMMENT ON TABLE role_visibility IS 'Enumeration of types of visibility and their human readable names and descriptions; used to ensure at the database level that incorrect visibilities cannot be used.'; +COMMENT ON COLUMN role_visibility.role_visibility_id IS 'The unique ID of this visibility level.'; +COMMENT ON COLUMN role_visibility.name IS 'A human-readable short name for the visibility level.'; +COMMENT ON COLUMN role_visibility.description IS 'Optional description for the visibility level.'; +CREATE SEQUENCE roles_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE roles_id_seq OWNED BY role.role_id; +CREATE SEQUENCE "schedule.showcredittype_id_seq" + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE "schedule.showcredittype_id_seq" OWNED BY credit_type.credit_type_id; +CREATE SEQUENCE types_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE types_id_seq OWNED BY role_visibility.role_visibility_id; +SET search_path = public, pg_catalog; +CREATE TABLE auth ( + memberid integer NOT NULL, + lookupid integer NOT NULL, + starttime timestamp with time zone DEFAULT now() NOT NULL, + endtime timestamp with time zone, + CONSTRAINT auth_check CHECK (((endtime IS NULL) OR (starttime < endtime))) +); +COMMENT ON TABLE auth IS 'Grants users temporary permissions on the back end.'; +COMMENT ON COLUMN auth.lookupid IS 'Permission granded to the user'; +COMMENT ON COLUMN auth.endtime IS 'Permission runs out now; NULL = permenant.'; +CREATE TABLE auth_group ( + id integer NOT NULL, + name character varying(80) NOT NULL +); +CREATE SEQUENCE auth_group_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE auth_group_id_seq OWNED BY auth_group.id; +CREATE TABLE auth_officer ( + officerid integer NOT NULL, + lookupid integer NOT NULL +); +COMMENT ON TABLE auth_officer IS 'Grants permanent back end permissions to people currently holding officer posts.'; +CREATE TABLE auth_subnet ( + typeid integer NOT NULL, + subnet cidr NOT NULL +); +COMMENT ON TABLE auth_subnet IS 'Allow users logged in from specific clients machines access to additional resources'; +CREATE TABLE auth_trainingstatus ( + typeid integer NOT NULL, + presenterstatusid integer NOT NULL +); +COMMENT ON TABLE auth_trainingstatus IS 'Permissions granted to users with the given training status'; +CREATE TABLE auth_user ( + id integer NOT NULL, + username character varying(30) NOT NULL, + first_name character varying(30) NOT NULL, + last_name character varying(30) NOT NULL, + email character varying(75) NOT NULL, + password character varying(128) NOT NULL, + is_staff boolean NOT NULL, + is_active boolean NOT NULL, + is_superuser boolean NOT NULL, + last_login timestamp with time zone NOT NULL, + date_joined timestamp with time zone NOT NULL +); +CREATE TABLE auth_user_groups ( + id integer NOT NULL, + user_id integer NOT NULL, + group_id integer NOT NULL +); +CREATE SEQUENCE auth_user_groups_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE auth_user_groups_id_seq OWNED BY auth_user_groups.id; +CREATE SEQUENCE auth_user_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE auth_user_id_seq OWNED BY auth_user.id; +CREATE SEQUENCE banner_category_categoryid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +CREATE TABLE baps_audio ( + audioid integer NOT NULL, + trackid integer, + filename character varying(256) NOT NULL +); +CREATE SEQUENCE baps_audio_audioid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE baps_audio_audioid_seq OWNED BY baps_audio.audioid; +CREATE TABLE baps_audiolog ( + audiologid integer NOT NULL, + serverid integer NOT NULL, + audioid integer NOT NULL, + timeplayed timestamp without time zone DEFAULT now() NOT NULL, + channel integer, + timestopped timestamp without time zone +); +CREATE SEQUENCE baps_audiolog_audiologid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE baps_audiolog_audiologid_seq OWNED BY baps_audiolog.audiologid; +CREATE TABLE baps_filefolder ( + filefolderid integer NOT NULL, + workgroup character varying(40) NOT NULL, + server character varying(40) NOT NULL, + share character varying(40) NOT NULL, + username character varying(40), + password character varying(40), + public boolean DEFAULT false NOT NULL, + description character varying(255) NOT NULL, + owner integer NOT NULL +); +CREATE SEQUENCE baps_filefolder_filefolderid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE baps_filefolder_filefolderid_seq OWNED BY baps_filefolder.filefolderid; +CREATE TABLE baps_fileitem ( + fileitemid integer NOT NULL, + filename character varying(511) NOT NULL +); +CREATE SEQUENCE baps_fileitem_fileitemid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE baps_fileitem_fileitemid_seq OWNED BY baps_fileitem.fileitemid; +CREATE TABLE baps_item ( + itemid integer NOT NULL, + listingid integer NOT NULL, + name1 character varying(255) NOT NULL, + "position" integer NOT NULL, + textitemid integer, + libraryitemid integer, + fileitemid integer, + name2 character varying(255), + CONSTRAINT baps_item_check CHECK ((((((textitemid IS NULL) AND (libraryitemid IS NULL)) AND (fileitemid IS NOT NULL)) OR (((textitemid IS NOT NULL) AND (libraryitemid IS NULL)) AND (fileitemid IS NULL))) OR (((textitemid IS NULL) AND (libraryitemid IS NOT NULL)) AND (fileitemid IS NULL)))) +); +CREATE SEQUENCE baps_item_itemid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE baps_item_itemid_seq OWNED BY baps_item.itemid; +CREATE TABLE baps_libraryitem ( + libraryitemid integer NOT NULL, + recordid integer NOT NULL, + trackid integer NOT NULL +); +CREATE SEQUENCE baps_libraryitem_libraryitemid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE baps_libraryitem_libraryitemid_seq OWNED BY baps_libraryitem.libraryitemid; +CREATE TABLE baps_listing ( + listingid integer NOT NULL, + showid integer NOT NULL, + name character varying(255) NOT NULL, + channel integer DEFAULT 0 NOT NULL +); +CREATE SEQUENCE baps_listing_listingid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE baps_listing_listingid_seq OWNED BY baps_listing.listingid; +CREATE SEQUENCE baps_personal_collection_unique_id + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + MAXVALUE 999999 + CACHE 1000; +CREATE TABLE rec_track ( + number smallint NOT NULL, + title text NOT NULL, + artist text NOT NULL, + length time without time zone DEFAULT '00:00:00'::time without time zone NOT NULL, + genre character(1) DEFAULT 'o'::bpchar NOT NULL, + intro time without time zone DEFAULT '00:00:00'::time without time zone NOT NULL, + outro time without time zone DEFAULT '00:00:00'::time without time zone NOT NULL, + clean character(1) DEFAULT 'u'::bpchar NOT NULL, + trackid integer DEFAULT nextval(('"rec_track_trackid_seq"'::text)::regclass) NOT NULL, + recordid integer NOT NULL, + digitised boolean DEFAULT false NOT NULL, + digitisedby integer, + duration integer, + lastfm_verified boolean DEFAULT false, + last_edited_memberid integer, + last_edited_time timestamp with time zone +); +COMMENT ON COLUMN rec_track.duration IS 'Duration of track in seconds'; +COMMENT ON COLUMN rec_track.lastfm_verified IS 'Whether or not the metadata of this track has been verified using the LastFM API'; +CREATE VIEW baps_ppl_log AS + SELECT baps_audiolog.timeplayed, baps_audiolog.timestopped, rec_track.title, rec_track.artist, rec_track.length AS trackduration FROM ((baps_audiolog JOIN baps_audio ON ((baps_audiolog.audioid = baps_audio.audioid))) JOIN rec_track ON ((rec_track.trackid = baps_audio.trackid))) WHERE ((baps_audiolog.timeplayed >= '2012-01-01 00:00:00'::timestamp without time zone) AND (baps_audiolog.timestopped <= '2012-02-01 00:00:00'::timestamp without time zone)); +COMMENT ON VIEW baps_ppl_log IS 'Sample query for PPL'; +CREATE TABLE baps_server ( + serverid integer NOT NULL, + servername character varying(50) NOT NULL +); +CREATE SEQUENCE baps_server_serverid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE baps_server_serverid_seq OWNED BY baps_server.serverid; +CREATE TABLE baps_show ( + showid integer NOT NULL, + userid integer NOT NULL, + name character varying(255) NOT NULL, + broadcastdate timestamp without time zone NOT NULL, + externallinkid integer, + viewable boolean DEFAULT false NOT NULL +); +CREATE SEQUENCE baps_show_showid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE baps_show_showid_seq OWNED BY baps_show.showid; +CREATE TABLE baps_textitem ( + textitemid integer NOT NULL, + textinfo text NOT NULL +); +CREATE SEQUENCE baps_textitem_textitemid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE baps_textitem_textitemid_seq OWNED BY baps_textitem.textitemid; +CREATE TABLE baps_user ( + userid integer NOT NULL, + username character varying(40) NOT NULL, + admin boolean DEFAULT false NOT NULL, + quotausage integer DEFAULT 0 NOT NULL, + quotalimit integer DEFAULT 734003200 NOT NULL, + shared boolean DEFAULT false NOT NULL, + signed boolean DEFAULT false NOT NULL, + userdescription character varying(100) +); +CREATE TABLE baps_user_external ( + userexternalid integer NOT NULL, + userid integer NOT NULL, + externalid integer NOT NULL +); +CREATE SEQUENCE baps_user_external_userexternalid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE baps_user_external_userexternalid_seq OWNED BY baps_user_external.userexternalid; +CREATE TABLE baps_user_filefolder ( + userfilefolderid integer NOT NULL, + userid integer NOT NULL, + filefolderid integer NOT NULL +); +CREATE SEQUENCE baps_user_filefolder_userfilefolderid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE baps_user_filefolder_userfilefolderid_seq OWNED BY baps_user_filefolder.userfilefolderid; +CREATE SEQUENCE baps_user_userid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE baps_user_userid_seq OWNED BY baps_user.userid; +CREATE TABLE chart ( + chartweek integer NOT NULL, + lastweek text NOT NULL, + title text NOT NULL, + artist text NOT NULL, + "position" integer NOT NULL +); +COMMENT ON TABLE chart IS 'ury chart rundowns'; +COMMENT ON COLUMN chart.chartweek IS 'chart release timestamp'; +CREATE TABLE selector ( + selid integer NOT NULL, + "time" timestamp with time zone DEFAULT now() NOT NULL, + action integer NOT NULL, + setby integer NOT NULL, + CONSTRAINT setbyinrange CHECK (((setby >= 0) AND (setby <= 3))) +); +CREATE VIEW current_studio AS + SELECT selector.action FROM selector WHERE (selector.action = ANY (ARRAY[4, 5, 6])) ORDER BY selector."time" DESC LIMIT 1; +COMMENT ON VIEW current_studio IS 'Gets the studio that is currently on air +Note: Studio 1 = 4, Studio 2 = 5, Jukebox = 6'; +CREATE TABLE gammu ( + "Version" numeric +); +COMMENT ON TABLE gammu IS 'Do not delete - Gammu will break'; +CREATE TABLE l_action ( + typeid integer DEFAULT nextval(('"l_action_typeid_seq"'::text)::regclass) NOT NULL, + descr character varying(255) NOT NULL, + phpconstant character varying(100) NOT NULL, + CONSTRAINT l_action_phpconstant_check CHECK (((phpconstant)::text = upper((phpconstant)::text))) +); +COMMENT ON TABLE l_action IS 'Enumerates back end permissions'; +COMMENT ON COLUMN l_action.typeid IS 'Surrogate Key'; +COMMENT ON COLUMN l_action.descr IS 'What the user sees the permission being called.'; +COMMENT ON COLUMN l_action.phpconstant IS 'The name in /members/inc/constants.php'; +CREATE SEQUENCE l_action_typeid_seq + START WITH 200 + INCREMENT BY 1 + MINVALUE 200 + MAXVALUE 10000 + CACHE 1; +CREATE TABLE l_college ( + collegeid integer DEFAULT nextval(('"l_college_collegeid_seq"'::text)::regclass) NOT NULL, + descr character varying(255) NOT NULL +); +CREATE SEQUENCE l_college_collegeid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + MAXVALUE 2147483647 + CACHE 1; +CREATE TABLE l_musicinterest ( + typeid integer DEFAULT nextval(('"l_musicinterest_typeid_seq"'::text)::regclass) NOT NULL, + descr character varying(255) NOT NULL +); +CREATE SEQUENCE l_musicinterest_typeid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + MAXVALUE 2147483647 + CACHE 1; +CREATE TABLE l_newsfeed ( + feedid integer NOT NULL, + feedname character varying(30) NOT NULL +); +INSERT INTO l_newsfeed (feedid, feedname) VALUES (1, 'Members News'); +INSERT INTO l_newsfeed (feedid, feedname) VALUES (2, 'Tech News'); +INSERT INTO l_newsfeed (feedid, feedname) VALUES (3, 'Breaking News'); +INSERT INTO l_newsfeed (feedid, feedname) VALUES (4, 'Presenter Information'); +COMMENT ON TABLE l_newsfeed IS 'Lookup table for internal news feeds'; +CREATE SEQUENCE l_newsfeeds_feedid_seq + START WITH 5 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE l_newsfeeds_feedid_seq OWNED BY l_newsfeed.feedid; +CREATE TABLE l_presenterstatus ( + presenterstatusid integer NOT NULL, + descr character varying(40) NOT NULL, + ordering integer, + depends integer, + can_award integer, + detail character varying +); +COMMENT ON COLUMN l_presenterstatus.can_award IS 'Members with this training status can award other members with this training status.'; +CREATE SEQUENCE l_presenterstatus_presenterstatusid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE l_presenterstatus_presenterstatusid_seq OWNED BY l_presenterstatus.presenterstatusid; +CREATE TABLE l_status ( + statusid character(1) NOT NULL, + descr character varying(255) NOT NULL +); +CREATE TABLE l_subnet ( + subnet cidr NOT NULL, + iscollege boolean NOT NULL, + description character varying NOT NULL +); +COMMENT ON TABLE l_subnet IS 'An informed guess (via Gavin) about which subnet covers which colleges.'; +CREATE SEQUENCE l_uryinterest_typeid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + MAXVALUE 2147483647 + CACHE 1; +CREATE TABLE mail_alias_text ( + aliasid integer NOT NULL, + name character varying NOT NULL, + dest character varying NOT NULL, + CONSTRAINT validaliasname CHECK (((name)::text ~ '^([a-zA-Z0-9]|-|_)+(.([a-zA-Z0-9]|-|_)+)*$'::text)) +); +COMMENT ON TABLE mail_alias_text IS 'DEPRECATED. See mail.alias and mail.alias_text for replacement.'; +CREATE SEQUENCE mail_aliasid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE mail_aliasid_seq OWNED BY mail_alias_text.aliasid; +CREATE TABLE mail_alias_list ( + aliasid integer DEFAULT nextval('mail_aliasid_seq'::regclass) NOT NULL, + name character varying NOT NULL, + listid integer NOT NULL, + CONSTRAINT validaliasname CHECK (((name)::text ~ '^([a-zA-Z0-9]|-|_)+(.([a-zA-Z0-9]|-|_)+)*$'::text)) +); +COMMENT ON TABLE mail_alias_list IS 'DEPRECATED. See mail.alias and mail.alias_list for replacement.'; +CREATE TABLE mail_alias_member ( + aliasid integer DEFAULT nextval('mail_aliasid_seq'::regclass) NOT NULL, + name character varying NOT NULL, + memberid integer NOT NULL, + CONSTRAINT validaliasname CHECK (((name)::text ~ '^([a-zA-Z0-9]|-|_)+(.([a-zA-Z0-9]|-|_)+)*$'::text)) +); +COMMENT ON TABLE mail_alias_member IS 'DEPRECATED. See mail.alias and mail.alias_member for replacement.'; +CREATE TABLE mail_alias_officer ( + aliasid integer DEFAULT nextval('mail_aliasid_seq'::regclass) NOT NULL, + name character varying NOT NULL, + officerid integer NOT NULL, + CONSTRAINT validaliasname CHECK (((name)::text ~ '^([a-zA-Z0-9]|-|_)+(.([a-zA-Z0-9]|-|_)+)*$'::text)) +); +COMMENT ON TABLE mail_alias_officer IS 'DEPRECATED. See mail.alias and mail.alias_officer for replacement.'; +CREATE TABLE mail_list ( + listid integer NOT NULL, + listname character varying NOT NULL, + defn text, + toexim boolean DEFAULT true NOT NULL, + listaddress character varying, + subscribable boolean DEFAULT true NOT NULL, + CONSTRAINT notnulliftoexim CHECK (((toexim AND (listaddress IS NOT NULL)) OR (NOT toexim))), + CONSTRAINT subscript_or_sql CHECK (((subscribable AND (defn IS NULL)) OR ((NOT subscribable) AND (defn IS NOT NULL)))), + CONSTRAINT validlistaddress CHECK (((listaddress)::text ~ '^([a-zA-Z0-9]|-|_)+(.([a-zA-Z0-9]|-|_)+)*$'::text)) +); +COMMENT ON TABLE mail_list IS 'Definitions of mailing lists'; +COMMENT ON COLUMN mail_list.listid IS 'Surrogate Key'; +COMMENT ON COLUMN mail_list.listname IS 'Name of the list'; +COMMENT ON COLUMN mail_list.defn IS 'A SQL string that returns fname, sname and email address.'; +COMMENT ON COLUMN mail_list.toexim IS 'Whether to create a mail alias on the email server for this list.'; +COMMENT ON COLUMN mail_list.listaddress IS 'If the list is exported, this is the list''s email address.'; +COMMENT ON COLUMN mail_list.subscribable IS 'Whether members can (un)subscribe freely.'; +CREATE SEQUENCE mail_list_listid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE mail_list_listid_seq OWNED BY mail_list.listid; +CREATE TABLE mail_subscription ( + memberid integer NOT NULL, + listid integer NOT NULL +); +COMMENT ON TABLE mail_subscription IS 'If a list is subscribable, then all members here are subscribed. If a list is not, then all members here are opted out.'; +CREATE TABLE member ( + memberid integer DEFAULT nextval(('"member_memberid_seq"'::text)::regclass) NOT NULL, + fname character varying(255) NOT NULL, + sname character varying(255) NOT NULL, + college integer NOT NULL, + phone character varying(255), + email character varying(255), + receive_email boolean DEFAULT true NOT NULL, + local_name character varying(100), + local_alias character varying(32), + account_locked boolean DEFAULT false NOT NULL, + last_login timestamp with time zone, + endofcourse timestamp with time zone, + eduroam character varying, + usesmtppassword boolean DEFAULT false NOT NULL, + joined timestamp without time zone DEFAULT now() NOT NULL, + require_password_change boolean DEFAULT false NOT NULL, + profile_photo integer, + bio text, + auth_provider character varying, + contract_signed boolean DEFAULT false NOT NULL +); +COMMENT ON COLUMN member.email IS 'If set, this is the user''s contact address. Otherwise, use the eduroam field.'; +COMMENT ON COLUMN member.local_name IS 'This column represents the part of the user''s URY email address before the @. When null, the user does not have a URY email account.'; +COMMENT ON COLUMN member.endofcourse IS 'This data is inaccurate/useless!'; +COMMENT ON COLUMN member.eduroam IS 'An eduroam username (E.G. abc123)'; +COMMENT ON COLUMN member.require_password_change IS 'If true, the user is required to change their password on next login.'; +COMMENT ON COLUMN member.auth_provider IS 'The MyRadioAuthenticator implementation to use when validating this user. If NULL, try all in turn, and depending on client implementation either ask them to set one or keep it as it is.'; +CREATE SEQUENCE member_memberid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + MAXVALUE 2147483647 + CACHE 1; +CREATE TABLE member_news_feed ( + membernewsfeedid integer NOT NULL, + newsentryid integer NOT NULL, + memberid integer NOT NULL, + seen timestamp without time zone DEFAULT now() NOT NULL +); +COMMENT ON TABLE member_news_feed IS 'Stores when members have seen news feed events'; +CREATE SEQUENCE member_news_feed_membernewsfeedid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE member_news_feed_membernewsfeedid_seq OWNED BY member_news_feed.membernewsfeedid; +CREATE SEQUENCE member_office_member_office_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + MAXVALUE 2147483647 + CACHE 1; +CREATE TABLE member_officer ( + member_officerid integer DEFAULT nextval(('"member_office_member_office_seq"'::text)::regclass) NOT NULL, + officerid integer NOT NULL, + memberid integer NOT NULL, + from_date date NOT NULL, + till_date date +); +CREATE TABLE member_pass ( + memberid integer NOT NULL, + password character varying +); +COMMENT ON TABLE member_pass IS 'User password. Access only to be granted to Shibbobleh, Dovecot Users. Exim authenticates via IMAP.'; +CREATE TABLE member_presenterstatus ( + memberid integer NOT NULL, + presenterstatusid integer NOT NULL, + completeddate timestamp with time zone DEFAULT now() NOT NULL, + confirmedby integer NOT NULL, + memberpresenterstatusid integer NOT NULL, + revokedtime timestamp without time zone, + revokedby integer +); +CREATE SEQUENCE member_presenterstatus_memberpresenterstatusid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE member_presenterstatus_memberpresenterstatusid_seq OWNED BY member_presenterstatus.memberpresenterstatusid; +CREATE TABLE member_year ( + memberid integer NOT NULL, + year smallint NOT NULL, + paid numeric(4,2) DEFAULT 0 NOT NULL +); +CREATE TABLE net_switchport ( + portid integer NOT NULL, + vlanid integer, + mac macaddr NOT NULL, + up boolean NOT NULL +); +COMMENT ON COLUMN net_switchport.portid IS 'Port number from front panel'; +COMMENT ON COLUMN net_switchport.vlanid IS 'Untagged vlan for this port'; +CREATE TABLE net_switchport_tags ( + portid integer NOT NULL, + vlanid integer NOT NULL +); +CREATE TABLE net_vlan ( + vlanid integer NOT NULL, + vlanname character varying NOT NULL +); +COMMENT ON COLUMN net_vlan.vlanid IS 'VLAN Number (not VLAN index)'; +CREATE TABLE news_feed ( + newsentryid integer NOT NULL, + feedid integer, + memberid integer, + "timestamp" timestamp without time zone DEFAULT now() NOT NULL, + content text, + revoked boolean DEFAULT false NOT NULL +); +COMMENT ON TABLE news_feed IS 'News Feed entries'; +CREATE SEQUENCE news_feed_newsentryid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE news_feed_newsentryid_seq OWNED BY news_feed.newsentryid; +CREATE TABLE nipsweb_migrate ( + memberid integer NOT NULL, + migrated boolean DEFAULT false, + enforced boolean DEFAULT false +); +COMMENT ON TABLE nipsweb_migrate IS 'Used to migrate users from BAPSWeb to NIPSWeb.'; +CREATE TABLE officer ( + officerid integer DEFAULT nextval(('"officer_officerid_seq"'::text)::regclass) NOT NULL, + officer_name character varying(255) NOT NULL, + officer_alias character varying(255), + teamid integer, + ordering smallint, + descr character varying(255), + status character(1) DEFAULT 'c'::bpchar NOT NULL, + type character(1) DEFAULT 'o'::bpchar, + CONSTRAINT validalias CHECK (((officer_alias)::text ~ '^([a-zA-Z0-9]|-|_)+(.([a-zA-Z0-9]|-|_)+)*$'::text)) +); +COMMENT ON COLUMN officer.type IS '(O)fficer, (A)ssistant Head of Team, (H)ead of Team'; +CREATE SEQUENCE officer_officerid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + MAXVALUE 2147483647 + CACHE 1; +CREATE TABLE rec_cleanlookup ( + clean_code character(1) NOT NULL, + clean_descr text NOT NULL +); +CREATE TABLE rec_formatlookup ( + format_code character(1) NOT NULL, + format_descr text NOT NULL +); +CREATE TABLE rec_genrelookup ( + genre_code character(1) NOT NULL, + genre_descr text NOT NULL +); +CREATE TABLE rec_itunes ( + trackid integer NOT NULL, + link text, + preview text, + image text, + identifier text +); +COMMENT ON TABLE rec_itunes IS 'itunes affiliation program'; +CREATE TABLE rec_labelqueue ( + recordid integer, + queueid integer DEFAULT nextval(('"rec_labelqueue_queueid_seq"'::text)::regclass) NOT NULL, + printed boolean DEFAULT false +); +CREATE SEQUENCE rec_labelqueue_queueid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + MAXVALUE 2147483647 + CACHE 1; +CREATE TABLE rec_locationlookup ( + location_code character(1) NOT NULL, + location_descr text NOT NULL +); +CREATE TABLE rec_lookup ( + code_type text NOT NULL, + code character(1) NOT NULL, + description text NOT NULL +); +COMMENT ON TABLE rec_lookup IS 'Table containing human-readable names for various codes used in the record library. (Replaces rec_Xlookup for API lookup simplicity.)'; +COMMENT ON COLUMN rec_lookup.code_type IS 'The type of code - this replaces the names used in the previous tables.'; +COMMENT ON COLUMN rec_lookup.code IS 'The single-character code used in the record library.'; +COMMENT ON COLUMN rec_lookup.description IS 'The human-readable description of the code.'; +CREATE SEQUENCE rec_lookup_description_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE rec_lookup_description_seq OWNED BY rec_lookup.description; +CREATE TABLE rec_medialookup ( + media_code character(1) NOT NULL, + media_descr text NOT NULL +); +CREATE TABLE rec_record ( + title text NOT NULL, + artist text NOT NULL, + status character(1) DEFAULT 'o'::bpchar NOT NULL, + media character(1) NOT NULL, + format character(1) NOT NULL, + recordlabel text NOT NULL, + dateadded timestamp with time zone DEFAULT now() NOT NULL, + datereleased date, + shelfnumber smallint NOT NULL, + shelfletter character(1) NOT NULL, + recordid integer DEFAULT nextval(('"rec_record_recordid_seq"'::text)::regclass) NOT NULL, + memberid_add integer NOT NULL, + memberid_lastedit integer, + datetime_lastedit timestamp with time zone, + cdid character varying(8), + location text, + promoterid integer +); +CREATE SEQUENCE rec_record_recordid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + MAXVALUE 2147483647 + CACHE 1; +CREATE TABLE rec_statuslookup ( + status_code character(1) NOT NULL, + status_descr text NOT NULL +); +CREATE SEQUENCE rec_track_trackid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + MAXVALUE 2147483647 + CACHE 1; +CREATE TABLE rec_trackcorrection ( + correctionid integer NOT NULL, + trackid integer NOT NULL, + proposed_title character varying NOT NULL, + proposed_artist character varying NOT NULL, + proposed_album_name character varying NOT NULL, + state character(1) DEFAULT 'p'::bpchar NOT NULL, + reviewedby integer, + level integer DEFAULT (-1) NOT NULL +); +COMMENT ON COLUMN rec_trackcorrection.state IS '''a'' Applied, ''r'' Rejected or ''p'' Pending'; +CREATE SEQUENCE rec_trackcorrection_correctionid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE rec_trackcorrection_correctionid_seq OWNED BY rec_trackcorrection.correctionid; +CREATE TABLE recommended_listening ( + week integer NOT NULL, + title text NOT NULL, + artist text NOT NULL, + "position" integer NOT NULL +); +COMMENT ON TABLE recommended_listening IS 'Recommended listening lists.'; +COMMENT ON COLUMN recommended_listening.week IS 'The timestamp of the week whose list this entry is part of.'; +COMMENT ON COLUMN recommended_listening.title IS 'The title of the song that has been recommended.'; +COMMENT ON COLUMN recommended_listening.artist IS 'The artist of the song that has been recommended.'; +COMMENT ON COLUMN recommended_listening."position" IS 'The position of the item in the list.'; +SET search_path = schedule, pg_catalog; +CREATE TABLE show ( + show_id integer NOT NULL, + show_type_id integer DEFAULT 1 NOT NULL, + submitted timestamp with time zone, + memberid integer NOT NULL +); +COMMENT ON TABLE show IS 'This is a very minimal table that ties everything together. The reason it doesn’t contain so much is so that literally everything else can have multiple entries and multiple versions. This lets us know what happened with everything ever, as well as allowing, for allocated seasons and timeslots, overrides for one or two instances over the default.'; +COMMENT ON COLUMN show.show_id IS 'A unique identifier for each show'; +COMMENT ON COLUMN show.show_type_id IS 'A reference to the type of show'; +COMMENT ON COLUMN show.submitted IS 'When the application was submitted. If Null, the application has just been saved by the user (not yet implemented in MyRadio)'; +COMMENT ON COLUMN show.memberid IS 'The ID of the user who submitted the application'; +CREATE TABLE show_metadata ( + show_metadata_id integer NOT NULL, + metadata_key_id integer NOT NULL, + show_id integer NOT NULL, + metadata_value text, + effective_from timestamp with time zone, + memberid integer NOT NULL, + approvedid integer, + effective_to timestamp with time zone +); +COMMENT ON TABLE show_metadata IS 'Stores all text-based items of information associated with a show.'; +COMMENT ON COLUMN show_metadata.show_metadata_id IS 'A unique identifier for each text item entry'; +COMMENT ON COLUMN show_metadata.metadata_key_id IS 'The ID of the type of metadata stored in this table'; +COMMENT ON COLUMN show_metadata.show_id IS 'The ID of the show the metadata applies to'; +COMMENT ON COLUMN show_metadata.metadata_value IS 'The value of the metadata'; +COMMENT ON COLUMN show_metadata.effective_from IS 'The timestamp from which this version of the show metadata became active. A value of NULL means it is not yet active (e.g. pending review)'; +COMMENT ON COLUMN show_metadata.memberid IS 'The ID of the member who submitted the updated version of the show metadata'; +COMMENT ON COLUMN show_metadata.approvedid IS 'The ID of the member who approved the change to the show metadata item. A value of NULL means not yet approved or this item does not need to be approved.'; +COMMENT ON COLUMN show_metadata.effective_to IS 'The timestamp of the period at which this metadatum stops being effective. If NULL, the metadatum is effective indefinitely from effective_from.'; +SET search_path = public, pg_catalog; +SET search_path = schedule, pg_catalog; +CREATE TABLE show_season ( + show_season_id integer NOT NULL, + show_id integer NOT NULL, + termid integer NOT NULL, + submitted timestamp with time zone, + memberid integer +); +COMMENT ON TABLE show_season IS 'Shows are now divided into Seasons - these are what actually have requested times and individual shows linked to them.'; +COMMENT ON COLUMN show_season.show_season_id IS 'A unique identifier for each season'; +COMMENT ON COLUMN show_season.show_id IS 'The ID of the show this is a season for'; +COMMENT ON COLUMN show_season.termid IS 'The ID for the term this season should be scheduled for'; +COMMENT ON COLUMN show_season.submitted IS 'When the application was submitted. If Null, the application has just been saved by the user'; +COMMENT ON COLUMN show_season.memberid IS 'The ID of the user that submitted the application'; +CREATE TABLE show_season_timeslot ( + show_season_timeslot_id integer NOT NULL, + show_season_id integer NOT NULL, + start_time timestamp with time zone NOT NULL, + memberid integer NOT NULL, + approvedid integer NOT NULL, + duration interval NOT NULL +); +COMMENT ON COLUMN show_season_timeslot.duration IS 'The duration of the show, as a time interval.'; +CREATE VIEW view_timeslot_legacy AS + SELECT t1.show_id, t1.starttime, t1.endtime, t1.entryid, t2.summary, t1.timeslotid FROM ((SELECT show_season_timeslot.show_season_timeslot_id AS timeslotid, show_season_timeslot.start_time AS starttime, (show_season_timeslot.start_time + show_season_timeslot.duration) AS endtime, show_season.show_season_id AS entryid, show.show_id FROM show_season_timeslot, show_season, show WHERE ((show_season_timeslot.show_season_id = show_season.show_season_id) AND (show_season.show_id = show.show_id))) t1 LEFT JOIN (SELECT show_metadata.metadata_value AS summary, show_metadata.show_id FROM show_metadata WHERE (show_metadata.metadata_key_id = (SELECT metadata_key.metadata_key_id FROM metadata.metadata_key WHERE ((metadata_key.name)::text = 'title'::text) LIMIT 1))) t2 ON ((t1.show_id = t2.show_id))); +SET search_path = public, pg_catalog; +CREATE VIEW sched_timeslot AS + SELECT view_timeslot_legacy.show_id, view_timeslot_legacy.starttime, view_timeslot_legacy.endtime, view_timeslot_legacy.entryid, view_timeslot_legacy.summary, view_timeslot_legacy.timeslotid FROM schedule.view_timeslot_legacy; +CREATE TABLE selector_actions ( + action integer NOT NULL, + description character varying(50) NOT NULL +); +CREATE SEQUENCE selector_actions_action_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE selector_actions_action_seq OWNED BY selector_actions.action; +CREATE SEQUENCE selector_selid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE selector_selid_seq OWNED BY selector.selid; +CREATE TABLE sso_session ( + id character varying(32) NOT NULL, + data text, + "timestamp" timestamp without time zone NOT NULL +); +CREATE TABLE strm_useragent ( + useragentid integer NOT NULL, + useragent character varying(255) NOT NULL +); +CREATE SEQUENCE strm_client_clientid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE strm_client_clientid_seq OWNED BY strm_useragent.useragentid; +CREATE TABLE strm_log ( + logid integer NOT NULL, + streamid integer NOT NULL, + useragentid integer NOT NULL, + starttime timestamp with time zone NOT NULL, + endtime timestamp with time zone NOT NULL, + ipaddr inet NOT NULL +); +CREATE SEQUENCE strm_log_logid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE strm_log_logid_seq OWNED BY strm_log.logid; +CREATE TABLE strm_logfile ( + logfileid integer NOT NULL, + filename character varying(255) NOT NULL, + lastsize integer +); +CREATE SEQUENCE strm_logfile_logfileid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE strm_logfile_logfileid_seq OWNED BY strm_logfile.logfileid; +CREATE TABLE strm_stream ( + streamid integer NOT NULL, + streamname character varying(255) NOT NULL +); +CREATE SEQUENCE strm_stream_streamid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE strm_stream_streamid_seq OWNED BY strm_stream.streamid; +CREATE TABLE team ( + teamid integer DEFAULT nextval(('"team_teamid_seq"'::text)::regclass) NOT NULL, + team_name character varying(255) NOT NULL, + descr text, + local_group character varying(255), + local_alias character varying(255), + ordering smallint, + status character(1) DEFAULT 'c'::bpchar NOT NULL +); +CREATE SEQUENCE team_teamid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + MAXVALUE 2147483647 + CACHE 1; +CREATE TABLE terms ( + start timestamp with time zone NOT NULL, + descr character varying(10) NOT NULL, + finish timestamp with time zone NOT NULL, + termid integer NOT NULL +); +COMMENT ON TABLE terms IS 'URY university term database. Must be updated regularly. Starts and finishes should be midnight UTC on the Monday of Weeks 1 and 11 respectively, NOT MIDNIGHT LOCAL TIME.'; +COMMENT ON COLUMN terms.start IS 'Midnight UTC, Monday Week 1'; +COMMENT ON COLUMN terms.finish IS 'Midnight UTC, Monday Week 11'; +CREATE SEQUENCE terms_termid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE terms_termid_seq OWNED BY terms.termid; +SET search_path = schedule, pg_catalog; +CREATE TABLE block ( + block_id integer NOT NULL, + name character varying(255) DEFAULT ''::character varying NOT NULL, + tag character varying(100) DEFAULT 'default'::character varying NOT NULL, + priority integer DEFAULT 0 NOT NULL, + is_listable boolean DEFAULT false NOT NULL, + description text DEFAULT ''::text NOT NULL +); +COMMENT ON TABLE block IS 'Schedule blocks, which divide up a schedule (via matching rules covered by other tables) into groups of shows.'; +COMMENT ON COLUMN block.block_id IS 'The unique identifier of this schedule block.'; +COMMENT ON COLUMN block.name IS 'The human-readable and publicly displayed name of this block.'; +COMMENT ON COLUMN block.tag IS 'The machine-readable string identifier used, for example, as the prefix of the CSS classes used to colour this block.'; +COMMENT ON COLUMN block.priority IS 'The priority of this block when deciding which block shows fall into. A lower number indicates a higher priority.'; +COMMENT ON COLUMN block.is_listable IS 'If true, the block appears in lists of blocks, allowing people to find shows in that block.'; +COMMENT ON COLUMN block.description IS 'A human-readable description of the block.'; +CREATE SEQUENCE block_description_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE block_description_seq OWNED BY block.description; +CREATE TABLE block_range_rule ( + block_range_rule_id integer NOT NULL, + block_id integer NOT NULL, + start_time interval NOT NULL, + end_time interval NOT NULL +); +COMMENT ON TABLE block_range_rule IS 'Block rules that associate timeslots falling into given ranges with corresponding blocks. +This is the lowest priority rule type.'; +COMMENT ON COLUMN block_range_rule.block_range_rule_id IS 'The unique identifier of this block rule.'; +COMMENT ON COLUMN block_range_rule.block_id IS 'The identifier of the block this rule matches.'; +COMMENT ON COLUMN block_range_rule.start_time IS 'The start of the range, as an offset from midnight on the day concerned.'; +COMMENT ON COLUMN block_range_rule.end_time IS 'The end of this range, as an offset from midnight on the day concerned.'; +CREATE SEQUENCE block_range_rule_block_range_rule_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE block_range_rule_block_range_rule_id_seq OWNED BY block_range_rule.block_range_rule_id; +CREATE SEQUENCE block_show_rules_block_show_rule_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +CREATE TABLE block_show_rule ( + block_show_rule_id integer DEFAULT nextval('block_show_rules_block_show_rule_id_seq'::regclass) NOT NULL, + block_id integer NOT NULL, + show_id integer NOT NULL +); +COMMENT ON TABLE block_show_rule IS 'A block show rule matches all timeslots of a given show to a given block. +This rule scheme takes precedence over any other scheme except direct timeslot assignment.'; +COMMENT ON COLUMN block_show_rule.block_show_rule_id IS 'The unique ID of the matching rule.'; +COMMENT ON COLUMN block_show_rule.block_id IS 'The ID of the block this rule assigns a show to.'; +COMMENT ON COLUMN block_show_rule.show_id IS 'The ID of the show this rule assigns to a block.'; +CREATE SEQUENCE blocks_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE blocks_id_seq OWNED BY block.block_id; +CREATE TABLE genre ( + genre_id integer NOT NULL, + name character varying +); +CREATE SEQUENCE genre_genre_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE genre_genre_id_seq OWNED BY genre.genre_id; +CREATE TABLE location ( + location_id integer NOT NULL, + location_name character varying +); +COMMENT ON TABLE location IS 'Locations include Studio 1, Studio 2, Outside Broadcast, Production Orifice'; +COMMENT ON COLUMN location.location_name IS 'The textual name of the location'; +CREATE SEQUENCE location_location_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE location_location_id_seq OWNED BY location.location_id; +CREATE SEQUENCE season_metadata_season_metadata_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +CREATE TABLE season_metadata ( + season_metadata_id integer DEFAULT nextval('season_metadata_season_metadata_id_seq'::regclass) NOT NULL, + metadata_key_id integer NOT NULL, + show_season_id integer NOT NULL, + metadata_value text, + effective_from timestamp with time zone, + memberid integer NOT NULL, + approvedid integer, + effective_to timestamp with time zone +); +COMMENT ON COLUMN season_metadata.season_metadata_id IS 'A unique identifier for each text item entry'; +COMMENT ON COLUMN season_metadata.metadata_key_id IS 'The ID of the type of metadata stored in this table'; +COMMENT ON COLUMN season_metadata.show_season_id IS 'The ID of the season the metadata applies to'; +COMMENT ON COLUMN season_metadata.metadata_value IS 'The value of the metadata'; +COMMENT ON COLUMN season_metadata.effective_from IS 'The timestamp from which this version of the season metadata became active. A value of NULL means it is not yet active (e.g. pending review)'; +COMMENT ON COLUMN season_metadata.memberid IS 'The ID of the member who submitted the updated version of the season metadata'; +COMMENT ON COLUMN season_metadata.approvedid IS 'The ID of the member who approved the updated version of the season metadata. A value of NULL may mean it is either not approved (See effective_from) or that this metadata type does not need review'; +COMMENT ON COLUMN season_metadata.effective_to IS 'The timestamp of the period at which this metadatum stops being effective. If NULL, the metadatum is effective indefinitely from effective_from.'; +CREATE TABLE show_credit ( + show_credit_id integer NOT NULL, + show_id integer NOT NULL, + credit_type_id integer NOT NULL, + creditid integer NOT NULL, + effective_from timestamp with time zone, + effective_to timestamp with time zone, + memberid integer NOT NULL, + approvedid integer +); +COMMENT ON TABLE show_credit IS 'Associates members to shows'; +CREATE SEQUENCE show_credit_show_credit_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE show_credit_show_credit_id_seq OWNED BY show_credit.show_credit_id; +CREATE TABLE show_genre ( + show_genre_id integer NOT NULL, + show_id integer NOT NULL, + genre_id integer NOT NULL, + effective_from timestamp with time zone, + effective_to timestamp with time zone, + memberid integer NOT NULL, + approvedid integer +); +CREATE SEQUENCE show_genre_show_genre_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE show_genre_show_genre_id_seq OWNED BY show_genre.show_genre_id; +CREATE TABLE show_image_metadata ( + memberid integer, + approvedid integer, + effective_from timestamp with time zone, + effective_to timestamp with time zone, + metadata_key_id integer NOT NULL, + metadata_value text NOT NULL, + show_id integer, + show_image_metadata_id integer NOT NULL +); +CREATE SEQUENCE show_image_metadata_show_image_metadata_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE show_image_metadata_show_image_metadata_id_seq OWNED BY show_image_metadata.show_image_metadata_id; +CREATE TABLE show_location ( + show_location_id integer NOT NULL, + show_id integer NOT NULL, + location_id integer NOT NULL, + effective_from timestamp with time zone, + effective_to timestamp with time zone, + memberid integer NOT NULL, + approvedid integer +); +CREATE SEQUENCE show_location_show_location_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE show_location_show_location_id_seq OWNED BY show_location.show_location_id; +CREATE SEQUENCE show_metadata_show_metadata_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE show_metadata_show_metadata_id_seq OWNED BY show_metadata.show_metadata_id; +CREATE TABLE show_podcast_link ( + podcast_id integer NOT NULL, + show_id integer NOT NULL +); +CREATE TABLE show_season_requested_time ( + show_season_requested_time_id integer NOT NULL, + requested_day integer NOT NULL, + start_time integer NOT NULL, + preference smallint DEFAULT 1 NOT NULL, + duration interval NOT NULL, + show_season_id integer NOT NULL +); +COMMENT ON TABLE show_season_requested_time IS 'Stores a list of times requested for a season with their preference. When a season is applied for, users are expected to make at least three preferences'; +COMMENT ON COLUMN show_season_requested_time.show_season_requested_time_id IS 'A unique identifier for each show season requested time'; +COMMENT ON COLUMN show_season_requested_time.requested_day IS 'The day that the show starts on.'; +COMMENT ON COLUMN show_season_requested_time.start_time IS 'The requested time for the show to start. Stored as seconds since midnight.'; +COMMENT ON COLUMN show_season_requested_time.preference IS 'A lower preference number means it is more preferred.'; +CREATE SEQUENCE show_season_requested_time_show_season_requested_time_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE show_season_requested_time_show_season_requested_time_id_seq OWNED BY show_season_requested_time.show_season_requested_time_id; +CREATE TABLE show_season_requested_week ( + show_season_requested_week_id integer NOT NULL, + show_season_id integer NOT NULL, + week smallint NOT NULL +); +COMMENT ON TABLE show_season_requested_week IS 'Stores a list of weeks for which a season wishes to be scheduled. Uses values 1-10 for weeks within a standard term.'; +CREATE SEQUENCE show_season_requested_week_show_season_requested_week_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE show_season_requested_week_show_season_requested_week_id_seq OWNED BY show_season_requested_week.show_season_requested_week_id; +CREATE SEQUENCE show_season_show_season_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE show_season_show_season_id_seq OWNED BY show_season.show_season_id; +CREATE VIEW show_season_text_metadata AS + SELECT season_metadata.season_metadata_id, season_metadata.metadata_key_id, season_metadata.show_season_id, season_metadata.metadata_value, season_metadata.effective_from, season_metadata.memberid, season_metadata.approvedid, season_metadata.effective_to FROM season_metadata; +CREATE VIEW show_season_timeslot_credit AS + SELECT show_credit.show_credit_id AS show_season_timeslot_credit_id, show_season_timeslot.show_season_timeslot_id, show_credit.credit_type_id, show_credit.creditid, show_credit.effective_from, show_credit.effective_to, show_credit.memberid, show_credit.approvedid FROM ((show_credit NATURAL JOIN show_season) NATURAL JOIN show_season_timeslot); +COMMENT ON VIEW show_season_timeslot_credit IS 'Removed: + WHERE show_credit.effective_from IS NOT NULL AND show_credit.effective_from <= show_season_timeslot.start_time AND (show_credit.effective_to IS NULL OR show_credit.effective_to >= (show_season_timeslot.start_time + show_season_timeslot.duration));'; +CREATE SEQUENCE show_season_timeslot_show_season_timeslot_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE show_season_timeslot_show_season_timeslot_id_seq OWNED BY show_season_timeslot.show_season_timeslot_id; +CREATE TABLE timeslot_metadata ( + metadata_key_id integer NOT NULL, + show_season_timeslot_id integer NOT NULL, + metadata_value text, + effective_from timestamp with time zone, + memberid integer NOT NULL, + approvedid integer, + timeslot_metadata_id integer NOT NULL, + effective_to timestamp with time zone +); +COMMENT ON COLUMN timeslot_metadata.timeslot_metadata_id IS 'The unique numeric ID of this metadatum.'; +CREATE VIEW show_season_timeslot_text_metadata AS + SELECT timeslot_metadata.metadata_key_id, timeslot_metadata.show_season_timeslot_id, timeslot_metadata.metadata_value, timeslot_metadata.effective_from, timeslot_metadata.memberid, timeslot_metadata.approvedid, timeslot_metadata.timeslot_metadata_id, timeslot_metadata.effective_to FROM timeslot_metadata; +CREATE SEQUENCE show_show_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE show_show_id_seq OWNED BY show.show_id; +CREATE VIEW show_text_metadata AS + SELECT show_metadata.show_metadata_id, show_metadata.metadata_key_id, show_metadata.show_id, show_metadata.metadata_value, show_metadata.effective_from, show_metadata.memberid, show_metadata.approvedid, show_metadata.effective_to FROM show_metadata; +CREATE TABLE show_type ( + show_type_id integer NOT NULL, + name character varying, + public boolean DEFAULT true NOT NULL, + has_showdb_entry boolean DEFAULT true NOT NULL, + description text DEFAULT ''::text NOT NULL, + can_be_messaged boolean DEFAULT false NOT NULL, + is_collapsible boolean DEFAULT false +); +COMMENT ON TABLE show_type IS 'Stores the possible types of “showâ€. Only one of these will actually be a show, but all can be entered on the Scheduling system. This allows support for Demos and Training (even though training is in a lecture - it can be added as Outside Broadcast) as well as John Wakefield’s pre-recording in Studio 2 and other general Studio 2 goings on, since it doesn’t often get used for shows.'; +COMMENT ON COLUMN show_type.show_type_id IS 'A unique identifier for each Show type'; +COMMENT ON COLUMN show_type.name IS 'The name of the show type'; +COMMENT ON COLUMN show_type.public IS 'Whether or not this show type is visible on the public schedule'; +COMMENT ON COLUMN show_type.has_showdb_entry IS 'If true, shows of this type and their season/timeslot descendents will have a publicly accessible showdb entry.'; +COMMENT ON COLUMN show_type.description IS 'A human-readable description of the show type.'; +COMMENT ON COLUMN show_type.can_be_messaged IS 'If true, the show can be messaged via the website.'; +CREATE SEQUENCE show_type_show_type_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE show_type_show_type_id_seq OWNED BY show_type.show_type_id; +CREATE SEQUENCE timeslot_metadata_timeslot_metadata_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE timeslot_metadata_timeslot_metadata_id_seq OWNED BY timeslot_metadata.timeslot_metadata_id; +SET search_path = sis2, pg_catalog; +CREATE TABLE commtype ( + commtypeid integer NOT NULL, + descr character varying(16) NOT NULL +); +INSERT INTO commtype (commtypeid, descr) VALUES (1, 'Email'); +INSERT INTO commtype (commtypeid, descr) VALUES (2, 'SMS'); +INSERT INTO commtype (commtypeid, descr) VALUES (3, 'Website'); +INSERT INTO commtype (commtypeid, descr) VALUES (4, 'Request'); +INSERT INTO commtype (commtypeid, descr) VALUES (5, 'Mobile Site'); +CREATE SEQUENCE commtype_commtypeid_seq + START WITH 6 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE commtype_commtypeid_seq OWNED BY commtype.commtypeid; +CREATE TABLE config ( + setting character varying(100) NOT NULL, + value character varying(100) +); +CREATE TABLE member_options ( + memberid integer NOT NULL, + helptab boolean DEFAULT true +); +CREATE TABLE member_signin ( + member_signin_id integer NOT NULL, + memberid integer NOT NULL, + sign_time timestamp without time zone DEFAULT now() NOT NULL, + signerid integer NOT NULL, + show_season_timeslot_id integer NOT NULL +); +COMMENT ON TABLE member_signin IS 'Stores members signing into their shows'; +COMMENT ON COLUMN member_signin.member_signin_id IS 'Unique Identifier for this signin event'; +COMMENT ON COLUMN member_signin.memberid IS 'The ID of the user signed in'; +COMMENT ON COLUMN member_signin.sign_time IS 'The time the user was signed in'; +COMMENT ON COLUMN member_signin.signerid IS 'The ID of the user who signed the user in'; +COMMENT ON COLUMN member_signin.show_season_timeslot_id IS 'The Timeslot that the users are signing in to'; +CREATE SEQUENCE member_signin_member_signin_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE member_signin_member_signin_id_seq OWNED BY member_signin.member_signin_id; +CREATE TABLE messages ( + commid integer NOT NULL, + timeslotid integer NOT NULL, + commtypeid integer NOT NULL, + sender character varying(64), + date timestamp with time zone DEFAULT now() NOT NULL, + subject character varying, + content text, + statusid integer NOT NULL, + comm_source character varying(15) +); +COMMENT ON TABLE messages IS 'Stores SIS comm messages'; +CREATE SEQUENCE messages_commid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE messages_commid_seq OWNED BY messages.commid; +CREATE TABLE statustype ( + statusid integer NOT NULL, + descr character varying(8) +); +INSERT INTO statustype (statusid, descr) VALUES (1, 'Unread'); +INSERT INTO statustype (statusid, descr) VALUES (2, 'Read'); +INSERT INTO statustype (statusid, descr) VALUES (3, 'Deleted'); +INSERT INTO statustype (statusid, descr) VALUES (4, 'Junk'); +INSERT INTO statustype (statusid, descr) VALUES (5, 'Abusive'); +CREATE SEQUENCE statustype_statusid_seq + START WITH 6 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE statustype_statusid_seq OWNED BY statustype.statusid; +SET search_path = tracklist, pg_catalog; +CREATE TABLE selbaps ( + selaction integer NOT NULL, + bapsloc integer NOT NULL +); +COMMENT ON TABLE selbaps IS 'Marries selector actions with BAPS servers'; +CREATE TABLE source ( + sourceid character(1) NOT NULL, + source text NOT NULL +); +COMMENT ON TABLE source IS 'Lookup table for source of track'; +CREATE TABLE state ( + stateid character(1) NOT NULL, + state text NOT NULL +); +COMMENT ON TABLE state IS 'State lookup table'; +CREATE TABLE track_notrec ( + audiologid integer NOT NULL, + artist text NOT NULL, + album text, + label text, + trackno integer, + track text NOT NULL, + length timestamp without time zone +); +COMMENT ON TABLE track_notrec IS 'Retrieves tracks not entered in the database'; +CREATE TABLE track_rec ( + audiologid integer NOT NULL, + recordid integer NOT NULL, + trackid integer +); +COMMENT ON TABLE track_rec IS 'Retrieves tracks from central database'; +CREATE TABLE tracklist ( + source character(1) NOT NULL, + timestart timestamp without time zone DEFAULT now() NOT NULL, + timestop timestamp without time zone, + state character(1), + timeslotid integer, + audiologid integer NOT NULL, + bapsaudioid integer +); +COMMENT ON TABLE tracklist IS 'Main tracklisting table. Things spur off of this'; +CREATE SEQUENCE tracklist_audiologid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE tracklist_audiologid_seq OWNED BY tracklist.audiologid; +SET search_path = uryplayer, pg_catalog; +CREATE TABLE podcast ( + memberid integer, + approvedid integer, + submitted timestamp with time zone, + podcast_id integer NOT NULL, + file character varying(100), + suspended boolean DEFAULT false NOT NULL +); +CREATE TABLE podcast_credit ( + memberid integer, + approvedid integer, + effective_from timestamp with time zone, + effective_to timestamp with time zone, + credit_type_id integer NOT NULL, + creditid integer NOT NULL, + podcast_credit_id integer NOT NULL, + podcast_id integer NOT NULL +); +CREATE SEQUENCE podcast_credit_podcast_credit_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE podcast_credit_podcast_credit_id_seq OWNED BY podcast_credit.podcast_credit_id; +CREATE TABLE podcast_image_metadata ( + memberid integer, + approvedid integer, + effective_from timestamp with time zone, + effective_to timestamp with time zone, + metadata_key_id integer NOT NULL, + metadata_value character varying(100) NOT NULL, + podcast_image_metadata_id integer NOT NULL, + podcast_id integer +); +CREATE SEQUENCE podcast_image_metadata_podcast_image_metadata_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE podcast_image_metadata_podcast_image_metadata_id_seq OWNED BY podcast_image_metadata.podcast_image_metadata_id; +CREATE TABLE podcast_metadata ( + memberid integer, + approvedid integer, + effective_from timestamp with time zone, + effective_to timestamp with time zone, + metadata_key_id integer NOT NULL, + metadata_value text NOT NULL, + podcast_metadata_id integer NOT NULL, + podcast_id integer +); +CREATE SEQUENCE podcast_metadata_podcast_metadata_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE podcast_metadata_podcast_metadata_id_seq OWNED BY podcast_metadata.podcast_metadata_id; +CREATE TABLE podcast_package_entry ( + memberid integer, + approvedid integer, + effective_from timestamp with time zone, + effective_to timestamp with time zone, + package_id integer NOT NULL, + podcast_id integer NOT NULL, + podcast_package_entry_id integer NOT NULL +); +CREATE SEQUENCE podcast_package_entry_podcast_package_entry_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE podcast_package_entry_podcast_package_entry_id_seq OWNED BY podcast_package_entry.podcast_package_entry_id; +CREATE SEQUENCE podcast_podcast_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE podcast_podcast_id_seq OWNED BY podcast.podcast_id; +CREATE VIEW podcast_text_metadata AS + SELECT podcast_metadata.memberid, podcast_metadata.approvedid, podcast_metadata.effective_from, podcast_metadata.effective_to, podcast_metadata.metadata_key_id, podcast_metadata.metadata_value, podcast_metadata.podcast_metadata_id, podcast_metadata.podcast_id FROM podcast_metadata; +SET search_path = webcam, pg_catalog; +CREATE TABLE memberviews ( + memberid integer NOT NULL, + timer integer DEFAULT 0 NOT NULL +); +CREATE TABLE streams ( + streamid integer NOT NULL, + streamname character varying NOT NULL, + liveurl character varying NOT NULL, + staticurl character varying NOT NULL +); +COMMENT ON TABLE streams IS 'Stores a list of streams available on MyRadio'; +CREATE SEQUENCE streams_streamid_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE streams_streamid_seq OWNED BY streams.streamid; +SET search_path = website, pg_catalog; +CREATE TABLE banner ( + banner_id integer NOT NULL, + alt text NOT NULL, + image character varying(100) NOT NULL, + target character varying(200) NOT NULL, + banner_type_id integer NOT NULL, + photoid integer +); +COMMENT ON COLUMN banner.photoid IS 'The corresponding MyRadio Photo ID, if one exists.'; +CREATE SEQUENCE banner_banner_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE banner_banner_id_seq OWNED BY banner.banner_id; +CREATE TABLE banner_campaign ( + memberid integer, + approvedid integer, + effective_from timestamp with time zone NOT NULL, + effective_to timestamp with time zone, + banner_campaign_id integer NOT NULL, + banner_location_id integer NOT NULL, + banner_id integer NOT NULL +); +CREATE SEQUENCE banner_campaign_banner_campaign_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE banner_campaign_banner_campaign_id_seq OWNED BY banner_campaign.banner_campaign_id; +CREATE TABLE banner_location ( + name character varying(50) NOT NULL, + description text NOT NULL, + banner_location_id integer NOT NULL +); +CREATE SEQUENCE banner_location_banner_location_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE banner_location_banner_location_id_seq OWNED BY banner_location.banner_location_id; +CREATE TABLE banner_timeslot ( + id integer NOT NULL, + memberid integer, + approvedid integer, + "order" integer NOT NULL, + banner_campaign_id integer NOT NULL, + day smallint NOT NULL, + start_time time without time zone NOT NULL, + end_time time without time zone, + CONSTRAINT banner_timeslot_day_check CHECK ((day >= 0)), + CONSTRAINT banner_timeslot_order_check CHECK (("order" >= 0)) +); +COMMENT ON COLUMN banner_timeslot.start_time IS 'The time of day when the banner will start rotating.'; +COMMENT ON COLUMN banner_timeslot.end_time IS 'The time of day when the banner will stop rotating.'; +CREATE SEQUENCE banner_timeslot_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE banner_timeslot_id_seq OWNED BY banner_timeslot.id; +CREATE TABLE banner_type ( + name character varying(50) NOT NULL, + description text NOT NULL, + banner_type_id integer NOT NULL +); +CREATE SEQUENCE banner_type_banner_type_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE banner_type_banner_type_id_seq OWNED BY banner_type.banner_type_id; +SET search_path = bapsplanner, pg_catalog; +ALTER TABLE ONLY managed_items ALTER COLUMN manageditemid SET DEFAULT nextval('managed_items_manageditemid_seq'::regclass); +ALTER TABLE ONLY managed_playlists ALTER COLUMN managedplaylistid SET DEFAULT nextval('managed_playlists_managedplaylistid_seq'::regclass); +ALTER TABLE ONLY timeslot_items ALTER COLUMN timeslot_item_id SET DEFAULT nextval('timeslot_items_timeslot_item_id_seq'::regclass); +SET search_path = jukebox, pg_catalog; +ALTER TABLE ONLY playlist_entries ALTER COLUMN entryid SET DEFAULT nextval('playlist_entries_entryid_seq'::regclass); +ALTER TABLE ONLY request ALTER COLUMN request_id SET DEFAULT nextval('request_request_id_seq'::regclass); +ALTER TABLE ONLY silence_log ALTER COLUMN silenceid SET DEFAULT nextval('silence_log_silenceid_seq'::regclass); +SET search_path = mail, pg_catalog; +ALTER TABLE ONLY alias ALTER COLUMN alias_id SET DEFAULT nextval('alias_alias_id_seq'::regclass); +ALTER TABLE ONLY email ALTER COLUMN email_id SET DEFAULT nextval('emails_email_id_seq'::regclass); +SET search_path = metadata, pg_catalog; +ALTER TABLE ONLY metadata_key ALTER COLUMN metadata_key_id SET DEFAULT nextval('metadata_key_metadata_key_id_seq'::regclass); +ALTER TABLE ONLY package ALTER COLUMN package_id SET DEFAULT nextval('package_package_id_seq'::regclass); +ALTER TABLE ONLY package_image_metadata ALTER COLUMN package_image_metadata_id SET DEFAULT nextval('package_image_metadata_package_image_metadata_id_seq'::regclass); +ALTER TABLE ONLY package_text_metadata ALTER COLUMN package_text_metadata_id SET DEFAULT nextval('package_text_metadata_package_text_metadata_id_seq'::regclass); +SET search_path = music, pg_catalog; +ALTER TABLE ONLY chart_release ALTER COLUMN chart_release_id SET DEFAULT nextval('chart_release_chart_release_id_seq'::regclass); +ALTER TABLE ONLY chart_row ALTER COLUMN chart_row_id SET DEFAULT nextval('chart_row_chart_row_id_seq'::regclass); +ALTER TABLE ONLY chart_type ALTER COLUMN chart_type_id SET DEFAULT nextval('chart_type_chart_type_id_seq'::regclass); +SET search_path = myury, pg_catalog; +ALTER TABLE ONLY act_permission ALTER COLUMN actpermissionid SET DEFAULT nextval('act_permission_actpermissionid_seq'::regclass); +ALTER TABLE ONLY actions ALTER COLUMN actionid SET DEFAULT nextval('actions_actionid_seq'::regclass); +ALTER TABLE ONLY api_class_map ALTER COLUMN api_map_id SET DEFAULT nextval('api_class_map_api_map_id_seq'::regclass); +ALTER TABLE ONLY api_method_auth ALTER COLUMN api_method_auth_id SET DEFAULT nextval('api_method_auth_api_method_auth_id_seq'::regclass); +ALTER TABLE ONLY award_categories ALTER COLUMN awardid SET DEFAULT nextval('award_categories_awardid_seq'::regclass); +ALTER TABLE ONLY award_member ALTER COLUMN awardmemberid SET DEFAULT nextval('award_member_awardmemberid_seq'::regclass); +ALTER TABLE ONLY modules ALTER COLUMN moduleid SET DEFAULT nextval('modules_moduleid_seq'::regclass); +ALTER TABLE ONLY photos ALTER COLUMN photoid SET DEFAULT nextval('photos_photoid_seq'::regclass); +ALTER TABLE ONLY services ALTER COLUMN serviceid SET DEFAULT nextval('services_serviceid_seq'::regclass); +ALTER TABLE ONLY services_versions ALTER COLUMN serviceversionid SET DEFAULT nextval('services_versions_serviceversionid_seq'::regclass); +ALTER TABLE ONLY api_key_log ALTER COLUMN api_log_id SET DEFAULT nextval('api_key_log_api_log_id_seq'::regclass); +SET search_path = people, pg_catalog; +ALTER TABLE ONLY credit_type ALTER COLUMN credit_type_id SET DEFAULT nextval('"schedule.showcredittype_id_seq"'::regclass); +ALTER TABLE ONLY group_root_role ALTER COLUMN group_root_role_id SET DEFAULT nextval('group_root_role_group_root_role_id_seq'::regclass); +ALTER TABLE ONLY group_type ALTER COLUMN group_type_id SET DEFAULT nextval('group_type_group_type_id_seq'::regclass); +ALTER TABLE ONLY role ALTER COLUMN role_id SET DEFAULT nextval('roles_id_seq'::regclass); +ALTER TABLE ONLY role_inheritance ALTER COLUMN role_inheritance_id SET DEFAULT nextval('role_inheritance_role_inheritance_id_seq'::regclass); +ALTER TABLE ONLY role_text_metadata ALTER COLUMN role_text_metadata_id SET DEFAULT nextval('role_metadata_role_metadata_id_seq'::regclass); +ALTER TABLE ONLY role_visibility ALTER COLUMN role_visibility_id SET DEFAULT nextval('types_id_seq'::regclass); +SET search_path = public, pg_catalog; +ALTER TABLE ONLY auth_group ALTER COLUMN id SET DEFAULT nextval('auth_group_id_seq'::regclass); +ALTER TABLE ONLY auth_user ALTER COLUMN id SET DEFAULT nextval('auth_user_id_seq'::regclass); +ALTER TABLE ONLY auth_user_groups ALTER COLUMN id SET DEFAULT nextval('auth_user_groups_id_seq'::regclass); +ALTER TABLE ONLY baps_audio ALTER COLUMN audioid SET DEFAULT nextval('baps_audio_audioid_seq'::regclass); +ALTER TABLE ONLY baps_audiolog ALTER COLUMN audiologid SET DEFAULT nextval('baps_audiolog_audiologid_seq'::regclass); +ALTER TABLE ONLY baps_filefolder ALTER COLUMN filefolderid SET DEFAULT nextval('baps_filefolder_filefolderid_seq'::regclass); +ALTER TABLE ONLY baps_fileitem ALTER COLUMN fileitemid SET DEFAULT nextval('baps_fileitem_fileitemid_seq'::regclass); +ALTER TABLE ONLY baps_item ALTER COLUMN itemid SET DEFAULT nextval('baps_item_itemid_seq'::regclass); +ALTER TABLE ONLY baps_libraryitem ALTER COLUMN libraryitemid SET DEFAULT nextval('baps_libraryitem_libraryitemid_seq'::regclass); +ALTER TABLE ONLY baps_listing ALTER COLUMN listingid SET DEFAULT nextval('baps_listing_listingid_seq'::regclass); +ALTER TABLE ONLY baps_server ALTER COLUMN serverid SET DEFAULT nextval('baps_server_serverid_seq'::regclass); +ALTER TABLE ONLY baps_show ALTER COLUMN showid SET DEFAULT nextval('baps_show_showid_seq'::regclass); +ALTER TABLE ONLY baps_textitem ALTER COLUMN textitemid SET DEFAULT nextval('baps_textitem_textitemid_seq'::regclass); +ALTER TABLE ONLY baps_user ALTER COLUMN userid SET DEFAULT nextval('baps_user_userid_seq'::regclass); +ALTER TABLE ONLY baps_user_external ALTER COLUMN userexternalid SET DEFAULT nextval('baps_user_external_userexternalid_seq'::regclass); +ALTER TABLE ONLY baps_user_filefolder ALTER COLUMN userfilefolderid SET DEFAULT nextval('baps_user_filefolder_userfilefolderid_seq'::regclass); +ALTER TABLE ONLY l_newsfeed ALTER COLUMN feedid SET DEFAULT nextval('l_newsfeeds_feedid_seq'::regclass); +ALTER TABLE ONLY l_presenterstatus ALTER COLUMN presenterstatusid SET DEFAULT nextval('l_presenterstatus_presenterstatusid_seq'::regclass); +ALTER TABLE ONLY mail_alias_text ALTER COLUMN aliasid SET DEFAULT nextval('mail_aliasid_seq'::regclass); +ALTER TABLE ONLY mail_list ALTER COLUMN listid SET DEFAULT nextval('mail_list_listid_seq'::regclass); +ALTER TABLE ONLY member_news_feed ALTER COLUMN membernewsfeedid SET DEFAULT nextval('member_news_feed_membernewsfeedid_seq'::regclass); +ALTER TABLE ONLY member_presenterstatus ALTER COLUMN memberpresenterstatusid SET DEFAULT nextval('member_presenterstatus_memberpresenterstatusid_seq'::regclass); +ALTER TABLE ONLY news_feed ALTER COLUMN newsentryid SET DEFAULT nextval('news_feed_newsentryid_seq'::regclass); +ALTER TABLE ONLY rec_trackcorrection ALTER COLUMN correctionid SET DEFAULT nextval('rec_trackcorrection_correctionid_seq'::regclass); +ALTER TABLE ONLY selector ALTER COLUMN selid SET DEFAULT nextval('selector_selid_seq'::regclass); +ALTER TABLE ONLY selector_actions ALTER COLUMN action SET DEFAULT nextval('selector_actions_action_seq'::regclass); +ALTER TABLE ONLY strm_log ALTER COLUMN logid SET DEFAULT nextval('strm_log_logid_seq'::regclass); +ALTER TABLE ONLY strm_logfile ALTER COLUMN logfileid SET DEFAULT nextval('strm_logfile_logfileid_seq'::regclass); +ALTER TABLE ONLY strm_stream ALTER COLUMN streamid SET DEFAULT nextval('strm_stream_streamid_seq'::regclass); +ALTER TABLE ONLY strm_useragent ALTER COLUMN useragentid SET DEFAULT nextval('strm_client_clientid_seq'::regclass); +ALTER TABLE ONLY terms ALTER COLUMN termid SET DEFAULT nextval('terms_termid_seq'::regclass); +SET search_path = schedule, pg_catalog; +ALTER TABLE ONLY block ALTER COLUMN block_id SET DEFAULT nextval('blocks_id_seq'::regclass); +ALTER TABLE ONLY block_range_rule ALTER COLUMN block_range_rule_id SET DEFAULT nextval('block_range_rule_block_range_rule_id_seq'::regclass); +ALTER TABLE ONLY genre ALTER COLUMN genre_id SET DEFAULT nextval('genre_genre_id_seq'::regclass); +ALTER TABLE ONLY location ALTER COLUMN location_id SET DEFAULT nextval('location_location_id_seq'::regclass); +ALTER TABLE ONLY show ALTER COLUMN show_id SET DEFAULT nextval('show_show_id_seq'::regclass); +ALTER TABLE ONLY show_credit ALTER COLUMN show_credit_id SET DEFAULT nextval('show_credit_show_credit_id_seq'::regclass); +ALTER TABLE ONLY show_genre ALTER COLUMN show_genre_id SET DEFAULT nextval('show_genre_show_genre_id_seq'::regclass); +ALTER TABLE ONLY show_image_metadata ALTER COLUMN show_image_metadata_id SET DEFAULT nextval('show_image_metadata_show_image_metadata_id_seq'::regclass); +ALTER TABLE ONLY show_location ALTER COLUMN show_location_id SET DEFAULT nextval('show_location_show_location_id_seq'::regclass); +ALTER TABLE ONLY show_metadata ALTER COLUMN show_metadata_id SET DEFAULT nextval('show_metadata_show_metadata_id_seq'::regclass); +ALTER TABLE ONLY show_season ALTER COLUMN show_season_id SET DEFAULT nextval('show_season_show_season_id_seq'::regclass); +ALTER TABLE ONLY show_season_requested_time ALTER COLUMN show_season_requested_time_id SET DEFAULT nextval('show_season_requested_time_show_season_requested_time_id_seq'::regclass); +ALTER TABLE ONLY show_season_requested_week ALTER COLUMN show_season_requested_week_id SET DEFAULT nextval('show_season_requested_week_show_season_requested_week_id_seq'::regclass); +ALTER TABLE ONLY show_season_timeslot ALTER COLUMN show_season_timeslot_id SET DEFAULT nextval('show_season_timeslot_show_season_timeslot_id_seq'::regclass); +ALTER TABLE ONLY show_type ALTER COLUMN show_type_id SET DEFAULT nextval('show_type_show_type_id_seq'::regclass); +ALTER TABLE ONLY timeslot_metadata ALTER COLUMN timeslot_metadata_id SET DEFAULT nextval('timeslot_metadata_timeslot_metadata_id_seq'::regclass); +SET search_path = sis2, pg_catalog; +ALTER TABLE ONLY commtype ALTER COLUMN commtypeid SET DEFAULT nextval('commtype_commtypeid_seq'::regclass); +ALTER TABLE ONLY member_signin ALTER COLUMN member_signin_id SET DEFAULT nextval('member_signin_member_signin_id_seq'::regclass); +ALTER TABLE ONLY messages ALTER COLUMN commid SET DEFAULT nextval('messages_commid_seq'::regclass); +ALTER TABLE ONLY statustype ALTER COLUMN statusid SET DEFAULT nextval('statustype_statusid_seq'::regclass); +SET search_path = tracklist, pg_catalog; +ALTER TABLE ONLY tracklist ALTER COLUMN audiologid SET DEFAULT nextval('tracklist_audiologid_seq'::regclass); +SET search_path = uryplayer, pg_catalog; +ALTER TABLE ONLY podcast ALTER COLUMN podcast_id SET DEFAULT nextval('podcast_podcast_id_seq'::regclass); +ALTER TABLE ONLY podcast_credit ALTER COLUMN podcast_credit_id SET DEFAULT nextval('podcast_credit_podcast_credit_id_seq'::regclass); +ALTER TABLE ONLY podcast_image_metadata ALTER COLUMN podcast_image_metadata_id SET DEFAULT nextval('podcast_image_metadata_podcast_image_metadata_id_seq'::regclass); +ALTER TABLE ONLY podcast_metadata ALTER COLUMN podcast_metadata_id SET DEFAULT nextval('podcast_metadata_podcast_metadata_id_seq'::regclass); +ALTER TABLE ONLY podcast_package_entry ALTER COLUMN podcast_package_entry_id SET DEFAULT nextval('podcast_package_entry_podcast_package_entry_id_seq'::regclass); +SET search_path = webcam, pg_catalog; +ALTER TABLE ONLY streams ALTER COLUMN streamid SET DEFAULT nextval('streams_streamid_seq'::regclass); +SET search_path = website, pg_catalog; +ALTER TABLE ONLY banner ALTER COLUMN banner_id SET DEFAULT nextval('banner_banner_id_seq'::regclass); +ALTER TABLE ONLY banner_campaign ALTER COLUMN banner_campaign_id SET DEFAULT nextval('banner_campaign_banner_campaign_id_seq'::regclass); +ALTER TABLE ONLY banner_location ALTER COLUMN banner_location_id SET DEFAULT nextval('banner_location_banner_location_id_seq'::regclass); +ALTER TABLE ONLY banner_timeslot ALTER COLUMN id SET DEFAULT nextval('banner_timeslot_id_seq'::regclass); +ALTER TABLE ONLY banner_type ALTER COLUMN banner_type_id SET DEFAULT nextval('banner_type_banner_type_id_seq'::regclass); + +-------------- +-- Add constraints and keys +-- These were missing from the initial dump for some reason +-------------- +SET search_path = bapsplanner, pg_catalog; + +-- +-- Name: auto_playlists_pkey; Type: CONSTRAINT; Schema: bapsplanner +-- + +ALTER TABLE ONLY auto_playlists + ADD CONSTRAINT auto_playlists_pkey PRIMARY KEY (auto_playlist_id); + + +-- +-- Name: client_ids_pkey; Type: CONSTRAINT; Schema: bapsplanner +-- + +ALTER TABLE ONLY client_ids + ADD CONSTRAINT client_ids_pkey PRIMARY KEY (client_id); + + +-- +-- Name: managed_items_pkey; Type: CONSTRAINT; Schema: bapsplanner +-- + +ALTER TABLE ONLY managed_items + ADD CONSTRAINT managed_items_pkey PRIMARY KEY (manageditemid); + + +-- +-- Name: managed_playlists_folder_key; Type: CONSTRAINT; Schema: bapsplanner +-- + +ALTER TABLE ONLY managed_playlists + ADD CONSTRAINT managed_playlists_folder_key UNIQUE (folder); + + +-- +-- Name: managed_playlists_name_key; Type: CONSTRAINT; Schema: bapsplanner +-- + +ALTER TABLE ONLY managed_playlists + ADD CONSTRAINT managed_playlists_name_key UNIQUE (name); + + +-- +-- Name: managed_playlists_pkey; Type: CONSTRAINT; Schema: bapsplanner +-- + +ALTER TABLE ONLY managed_playlists + ADD CONSTRAINT managed_playlists_pkey PRIMARY KEY (managedplaylistid); + + +-- +-- Name: secure_play_token_pkey; Type: CONSTRAINT; Schema: bapsplanner +-- + +ALTER TABLE ONLY secure_play_token + ADD CONSTRAINT secure_play_token_pkey PRIMARY KEY (sessionid, memberid, "timestamp", trackid); + + +-- +-- Name: timeslot_items_pkey; Type: CONSTRAINT; Schema: bapsplanner +-- + +ALTER TABLE ONLY timeslot_items + ADD CONSTRAINT timeslot_items_pkey PRIMARY KEY (timeslot_item_id); + +SET search_path = jukebox, pg_catalog; + +-- +-- Name: playlist_entries_pkey; Type: CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY playlist_entries + ADD CONSTRAINT playlist_entries_pkey PRIMARY KEY (playlistid, trackid, revision_added); + + +-- +-- Name: playlist_revisions_pkey; Type: CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY playlist_revisions + ADD CONSTRAINT playlist_revisions_pkey PRIMARY KEY (playlistid, revisionid); + + +-- +-- Name: playlist_timeslot_pkey; Type: CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY playlist_timeslot + ADD CONSTRAINT playlist_timeslot_pkey PRIMARY KEY (id); + + +-- +-- Name: playlists_pkey; Type: CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY playlists + ADD CONSTRAINT playlists_pkey PRIMARY KEY (playlistid); + + +-- +-- Name: request_pkey; Type: CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY request + ADD CONSTRAINT request_pkey PRIMARY KEY (request_id); + + +-- +-- Name: silence_log_pkey; Type: CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY silence_log + ADD CONSTRAINT silence_log_pkey PRIMARY KEY (silenceid); + + +-- +-- Name: track_blacklist_pkey; Type: CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY track_blacklist + ADD CONSTRAINT track_blacklist_pkey PRIMARY KEY (trackid); + + +SET search_path = mail, pg_catalog; + +-- +-- Name: alias_list_pkey; Type: CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY alias_list + ADD CONSTRAINT alias_list_pkey PRIMARY KEY (alias_id, destination); + + +-- +-- Name: alias_member_pkey; Type: CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY alias_member + ADD CONSTRAINT alias_member_pkey PRIMARY KEY (alias_id, destination); + + +-- +-- Name: alias_officer_pkey; Type: CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY alias_officer + ADD CONSTRAINT alias_officer_pkey PRIMARY KEY (alias_id, destination); + + +-- +-- Name: alias_pkey; Type: CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY alias + ADD CONSTRAINT alias_pkey PRIMARY KEY (alias_id); + + +-- +-- Name: alias_source_key; Type: CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY alias + ADD CONSTRAINT alias_source_key UNIQUE (source); + + +-- +-- Name: alias_text_pkey; Type: CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY alias_text + ADD CONSTRAINT alias_text_pkey PRIMARY KEY (alias_id, destination); + + +-- +-- Name: email_recipient_list_pkey; Type: CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY email_recipient_list + ADD CONSTRAINT email_recipient_list_pkey PRIMARY KEY (email_id, listid); + + +-- +-- Name: email_recipient_user_pkey; Type: CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY email_recipient_member + ADD CONSTRAINT email_recipient_user_pkey PRIMARY KEY (email_id, memberid); + + +-- +-- Name: emails_pkey; Type: CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY email + ADD CONSTRAINT emails_pkey PRIMARY KEY (email_id); + + +SET search_path = metadata, pg_catalog; + +-- +-- Name: metadata_key_name_key; Type: CONSTRAINT; Schema: metadata +-- + +ALTER TABLE ONLY metadata_key + ADD CONSTRAINT metadata_key_name_key UNIQUE (name); + + +-- +-- Name: metadata_key_pkey; Type: CONSTRAINT; Schema: metadata +-- + +ALTER TABLE ONLY metadata_key + ADD CONSTRAINT metadata_key_pkey PRIMARY KEY (metadata_key_id); + + +-- +-- Name: package_image_metadata_pkey; Type: CONSTRAINT; Schema: metadata +-- + +ALTER TABLE ONLY package_image_metadata + ADD CONSTRAINT package_image_metadata_pkey PRIMARY KEY (package_image_metadata_id); + + +-- +-- Name: package_pkey; Type: CONSTRAINT; Schema: metadata +-- + +ALTER TABLE ONLY package + ADD CONSTRAINT package_pkey PRIMARY KEY (package_id); + + +-- +-- Name: package_text_metadata_pkey; Type: CONSTRAINT; Schema: metadata +-- + +ALTER TABLE ONLY package_text_metadata + ADD CONSTRAINT package_text_metadata_pkey PRIMARY KEY (package_text_metadata_id); + + +SET search_path = music, pg_catalog; + +-- +-- Name: chart_release_pkey; Type: CONSTRAINT; Schema: music +-- + +ALTER TABLE ONLY chart_release + ADD CONSTRAINT chart_release_pkey PRIMARY KEY (chart_release_id); + + +-- +-- Name: chart_row_chart_row_id_key; Type: CONSTRAINT; Schema: music +-- + +ALTER TABLE ONLY chart_row + ADD CONSTRAINT chart_row_chart_row_id_key UNIQUE (chart_row_id, "position"); + + +-- +-- Name: chart_row_pkey; Type: CONSTRAINT; Schema: music +-- + +ALTER TABLE ONLY chart_row + ADD CONSTRAINT chart_row_pkey PRIMARY KEY (chart_row_id); + + +-- +-- Name: chart_type_pkey; Type: CONSTRAINT; Schema: music +-- + +ALTER TABLE ONLY chart_type + ADD CONSTRAINT chart_type_pkey PRIMARY KEY (chart_type_id); + + +SET search_path = myury, pg_catalog; + +-- +-- Name: act_permission_pkey; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY act_permission + ADD CONSTRAINT act_permission_pkey PRIMARY KEY (actpermissionid); + + +-- +-- Name: act_permission_serviceid_key; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY act_permission + ADD CONSTRAINT act_permission_serviceid_key UNIQUE (serviceid, moduleid, actionid, typeid); + + +-- +-- Name: actions_moduleid_key; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY actions + ADD CONSTRAINT actions_moduleid_key UNIQUE (moduleid, name); + + +-- +-- Name: actions_pkey; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY actions + ADD CONSTRAINT actions_pkey PRIMARY KEY (actionid); + + +-- +-- Name: api_class_map_api_name_key; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY api_class_map + ADD CONSTRAINT api_class_map_api_name_key UNIQUE (api_name); + + +-- +-- Name: api_class_map_class_name_key; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY api_class_map + ADD CONSTRAINT api_class_map_class_name_key UNIQUE (class_name); + + +-- +-- Name: api_class_map_pkey; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY api_class_map + ADD CONSTRAINT api_class_map_pkey PRIMARY KEY (api_map_id); + + +-- +-- Name: api_key_auth_pkey; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY api_key_auth + ADD CONSTRAINT api_key_auth_pkey PRIMARY KEY (key_string, typeid); + +-- +-- Name: api_key_pkey; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY api_key + ADD CONSTRAINT api_key_pkey PRIMARY KEY (key_string); + + +-- +-- Name: api_method_auth_class_name_method_name_typeid_key; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY api_method_auth + ADD CONSTRAINT api_method_auth_class_name_method_name_typeid_key UNIQUE (class_name, method_name, typeid); + + +-- +-- Name: api_method_auth_pkey; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY api_method_auth + ADD CONSTRAINT api_method_auth_pkey PRIMARY KEY (api_method_auth_id); + + +-- +-- Name: award_categories_name_key; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY award_categories + ADD CONSTRAINT award_categories_name_key UNIQUE (name); + + +-- +-- Name: award_categories_pkey; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY award_categories + ADD CONSTRAINT award_categories_pkey PRIMARY KEY (awardid); + + +-- +-- Name: award_member_pkey; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY award_member + ADD CONSTRAINT award_member_pkey PRIMARY KEY (awardmemberid); + + +-- +-- Name: modules_pkey; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY modules + ADD CONSTRAINT modules_pkey PRIMARY KEY (moduleid); + + +-- +-- Name: modules_serviceid_key; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY modules + ADD CONSTRAINT modules_serviceid_key UNIQUE (serviceid, name); + + +-- +-- Name: photos_pkey; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY photos + ADD CONSTRAINT photos_pkey PRIMARY KEY (photoid); + + +-- +-- Name: services_name_key; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY services + ADD CONSTRAINT services_name_key UNIQUE (name); + + +-- +-- Name: services_pkey; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY services + ADD CONSTRAINT services_pkey PRIMARY KEY (serviceid); + + +-- +-- Name: services_versions_pkey; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY services_versions + ADD CONSTRAINT services_versions_pkey PRIMARY KEY (serviceversionid); + + +-- +-- Name: services_versions_serviceid_key; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY services_versions + ADD CONSTRAINT services_versions_serviceid_key UNIQUE (serviceid, version); + + +-- +-- Name: services_versions_serviceid_key1; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY services_versions + ADD CONSTRAINT services_versions_serviceid_key1 UNIQUE (serviceid, path); + + +-- +-- Name: services_versions_users_pkey; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY services_versions_member + ADD CONSTRAINT services_versions_users_pkey PRIMARY KEY (memberid, serviceversionid); + + +-- +-- Name: single_login_token_pkey; Type: CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY password_reset_token + ADD CONSTRAINT single_login_token_pkey PRIMARY KEY (token); + + +SET search_path = people, pg_catalog; + +-- +-- Name: group_root_role_pkey; Type: CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY group_root_role + ADD CONSTRAINT group_root_role_pkey PRIMARY KEY (group_root_role_id); + + +-- +-- Name: group_root_role_role_id_id_key; Type: CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY group_root_role + ADD CONSTRAINT group_root_role_role_id_id_key UNIQUE (role_id_id); + + +-- +-- Name: group_type_pkey; Type: CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY group_type + ADD CONSTRAINT group_type_pkey PRIMARY KEY (group_type_id); + + +-- +-- Name: metadata_pkey; Type: CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY metadata + ADD CONSTRAINT metadata_pkey PRIMARY KEY (roleid, key); + + + +-- +-- Name: role_inheritance_pkey; Type: CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY role_inheritance + ADD CONSTRAINT role_inheritance_pkey PRIMARY KEY (role_inheritance_id); + + +-- +-- Name: role_metadata_pkey; Type: CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY role_text_metadata + ADD CONSTRAINT role_metadata_pkey PRIMARY KEY (role_text_metadata_id); + + +-- +-- Name: roles_pkey; Type: CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY role + ADD CONSTRAINT roles_pkey PRIMARY KEY (role_id); + + +-- +-- Name: schedule.showcredittype_pkey; Type: CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY credit_type + ADD CONSTRAINT "schedule.showcredittype_pkey" PRIMARY KEY (credit_type_id); + + +-- +-- Name: types_name_key; Type: CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY role_visibility + ADD CONSTRAINT types_name_key UNIQUE (name); + + +-- +-- Name: types_pkey; Type: CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY role_visibility + ADD CONSTRAINT types_pkey PRIMARY KEY (role_visibility_id); + + +SET search_path = public, pg_catalog; + +-- +-- Name: auth_group_name_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth_group + ADD CONSTRAINT auth_group_name_key UNIQUE (name); + + +-- +-- Name: auth_group_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth_group + ADD CONSTRAINT auth_group_pkey PRIMARY KEY (id); + + +-- +-- Name: auth_officer_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth_officer + ADD CONSTRAINT auth_officer_pkey PRIMARY KEY (officerid, lookupid); + + +-- +-- Name: auth_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth + ADD CONSTRAINT auth_pkey PRIMARY KEY (memberid, lookupid, starttime); + + +-- +-- Name: auth_subnet_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth_subnet + ADD CONSTRAINT auth_subnet_pkey PRIMARY KEY (typeid, subnet); + + +-- +-- Name: auth_trainingstatus_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth_trainingstatus + ADD CONSTRAINT auth_trainingstatus_pkey PRIMARY KEY (typeid, presenterstatusid); + + +-- +-- Name: auth_user_groups_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth_user_groups + ADD CONSTRAINT auth_user_groups_pkey PRIMARY KEY (id); + + +-- +-- Name: auth_user_groups_user_id_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth_user_groups + ADD CONSTRAINT auth_user_groups_user_id_key UNIQUE (user_id, group_id); + + +-- +-- Name: auth_user_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth_user + ADD CONSTRAINT auth_user_pkey PRIMARY KEY (id); + + +-- +-- Name: auth_user_username_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth_user + ADD CONSTRAINT auth_user_username_key UNIQUE (username); + + +-- +-- Name: baps_audio_filename_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_audio + ADD CONSTRAINT baps_audio_filename_key UNIQUE (filename); + + +-- +-- Name: baps_audio_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_audio + ADD CONSTRAINT baps_audio_pkey PRIMARY KEY (audioid); + + +-- +-- Name: baps_audio_trackid_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_audio + ADD CONSTRAINT baps_audio_trackid_key UNIQUE (trackid); + + +-- +-- Name: baps_audiolog_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_audiolog + ADD CONSTRAINT baps_audiolog_pkey PRIMARY KEY (audiologid); + + +-- +-- Name: baps_filefolder_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_filefolder + ADD CONSTRAINT baps_filefolder_pkey PRIMARY KEY (filefolderid); + +ALTER TABLE baps_filefolder CLUSTER ON baps_filefolder_pkey; + + +-- +-- Name: baps_filefolder_workgroup_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_filefolder + ADD CONSTRAINT baps_filefolder_workgroup_key UNIQUE (workgroup, server, share); + + +-- +-- Name: baps_fileitem_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_fileitem + ADD CONSTRAINT baps_fileitem_pkey PRIMARY KEY (fileitemid); + +ALTER TABLE baps_fileitem CLUSTER ON baps_fileitem_pkey; + + +-- +-- Name: baps_item_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_item + ADD CONSTRAINT baps_item_pkey PRIMARY KEY (itemid); + +ALTER TABLE baps_item CLUSTER ON baps_item_pkey; + + +-- +-- Name: baps_libraryitem_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_libraryitem + ADD CONSTRAINT baps_libraryitem_pkey PRIMARY KEY (libraryitemid); + +ALTER TABLE baps_libraryitem CLUSTER ON baps_libraryitem_pkey; + + +-- +-- Name: baps_listing_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_listing + ADD CONSTRAINT baps_listing_pkey PRIMARY KEY (listingid); + + +-- +-- Name: baps_server_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_server + ADD CONSTRAINT baps_server_pkey PRIMARY KEY (serverid); + + +-- +-- Name: baps_server_servername_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_server + ADD CONSTRAINT baps_server_servername_key UNIQUE (servername); + + +-- +-- Name: baps_show_name_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_show + ADD CONSTRAINT baps_show_name_key UNIQUE (name, userid); + + +-- +-- Name: baps_show_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_show + ADD CONSTRAINT baps_show_pkey PRIMARY KEY (showid); + +ALTER TABLE baps_show CLUSTER ON baps_show_pkey; + + +-- +-- Name: baps_textitem_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_textitem + ADD CONSTRAINT baps_textitem_pkey PRIMARY KEY (textitemid); + +ALTER TABLE baps_textitem CLUSTER ON baps_textitem_pkey; + + +-- +-- Name: baps_user_external_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_user_external + ADD CONSTRAINT baps_user_external_pkey PRIMARY KEY (userexternalid); + + +-- +-- Name: baps_user_external_userid_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_user_external + ADD CONSTRAINT baps_user_external_userid_key UNIQUE (userid, externalid); + + +-- +-- Name: baps_user_filefolder_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_user_filefolder + ADD CONSTRAINT baps_user_filefolder_pkey PRIMARY KEY (userfilefolderid); + + +-- +-- Name: baps_user_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_user + ADD CONSTRAINT baps_user_pkey PRIMARY KEY (userid); + + +-- +-- Name: baps_user_username_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_user + ADD CONSTRAINT baps_user_username_key UNIQUE (username); + +ALTER TABLE baps_user CLUSTER ON baps_user_username_key; + + +-- +-- Name: chart_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY chart + ADD CONSTRAINT chart_pkey PRIMARY KEY (chartweek, "position"); + + +-- +-- Name: l_action_descr_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY l_action + ADD CONSTRAINT l_action_descr_key UNIQUE (descr); + + +-- +-- Name: l_action_phpconstant_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY l_action + ADD CONSTRAINT l_action_phpconstant_key UNIQUE (phpconstant); + + +-- +-- Name: l_action_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY l_action + ADD CONSTRAINT l_action_pkey PRIMARY KEY (typeid); + + +-- +-- Name: l_college_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY l_college + ADD CONSTRAINT l_college_pkey PRIMARY KEY (collegeid); + + +-- +-- Name: l_musicinterest_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY l_musicinterest + ADD CONSTRAINT l_musicinterest_pkey PRIMARY KEY (typeid); + + +-- +-- Name: l_newsfeeds_feedname_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY l_newsfeed + ADD CONSTRAINT l_newsfeeds_feedname_key UNIQUE (feedname); + + +-- +-- Name: l_newsfeeds_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY l_newsfeed + ADD CONSTRAINT l_newsfeeds_pkey PRIMARY KEY (feedid); + + +-- +-- Name: l_presenterstatus_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY l_presenterstatus + ADD CONSTRAINT l_presenterstatus_pkey PRIMARY KEY (presenterstatusid); + + +-- +-- Name: l_status_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY l_status + ADD CONSTRAINT l_status_pkey PRIMARY KEY (statusid); + + +-- +-- Name: l_subnet_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY l_subnet + ADD CONSTRAINT l_subnet_pkey PRIMARY KEY (subnet); + + +-- +-- Name: mail_alias_list_name_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY mail_alias_list + ADD CONSTRAINT mail_alias_list_name_key UNIQUE (name, listid); + + +-- +-- Name: mail_alias_list_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY mail_alias_list + ADD CONSTRAINT mail_alias_list_pkey PRIMARY KEY (aliasid); + + +-- +-- Name: mail_alias_member_name_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY mail_alias_member + ADD CONSTRAINT mail_alias_member_name_key UNIQUE (name, memberid); + + +-- +-- Name: mail_alias_member_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY mail_alias_member + ADD CONSTRAINT mail_alias_member_pkey PRIMARY KEY (aliasid); + + +-- +-- Name: mail_alias_officer_name_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY mail_alias_officer + ADD CONSTRAINT mail_alias_officer_name_key UNIQUE (name, officerid); + + +-- +-- Name: mail_alias_officer_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY mail_alias_officer + ADD CONSTRAINT mail_alias_officer_pkey PRIMARY KEY (aliasid); + + +-- +-- Name: mail_alias_text_name_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY mail_alias_text + ADD CONSTRAINT mail_alias_text_name_key UNIQUE (name, dest); + + +-- +-- Name: mail_alias_text_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY mail_alias_text + ADD CONSTRAINT mail_alias_text_pkey PRIMARY KEY (aliasid); + + +-- +-- Name: mail_list_listaddress_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY mail_list + ADD CONSTRAINT mail_list_listaddress_key UNIQUE (listaddress); + + +-- +-- Name: mail_list_listname_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY mail_list + ADD CONSTRAINT mail_list_listname_key UNIQUE (listname); + + +-- +-- Name: mail_list_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY mail_list + ADD CONSTRAINT mail_list_pkey PRIMARY KEY (listid); + + +-- +-- Name: member_eduroam_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member + ADD CONSTRAINT member_eduroam_key UNIQUE (eduroam); + + +-- +-- Name: member_email_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member + ADD CONSTRAINT member_email_key UNIQUE (email); + + +-- +-- Name: member_local_alias_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member + ADD CONSTRAINT member_local_alias_key UNIQUE (local_alias); + + +-- +-- Name: member_local_name_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member + ADD CONSTRAINT member_local_name_key UNIQUE (local_name); + + +-- +-- Name: member_mail_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY mail_subscription + ADD CONSTRAINT member_mail_pkey PRIMARY KEY (memberid, listid); + + +-- +-- Name: member_news_feed_memberid_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member_news_feed + ADD CONSTRAINT member_news_feed_memberid_key UNIQUE (memberid, newsentryid); + + +-- +-- Name: member_news_feed_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member_news_feed + ADD CONSTRAINT member_news_feed_pkey PRIMARY KEY (membernewsfeedid); + + +-- +-- Name: member_officer_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member_officer + ADD CONSTRAINT member_officer_pkey PRIMARY KEY (member_officerid); + + +-- +-- Name: member_pass_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member_pass + ADD CONSTRAINT member_pass_pkey PRIMARY KEY (memberid); + + +-- +-- Name: member_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member + ADD CONSTRAINT member_pkey PRIMARY KEY (memberid); + + +-- +-- Name: member_presenterstatus_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member_presenterstatus + ADD CONSTRAINT member_presenterstatus_pkey PRIMARY KEY (memberpresenterstatusid); + + +-- +-- Name: member_year_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member_year + ADD CONSTRAINT member_year_pkey PRIMARY KEY (memberid, year); + + +-- +-- Name: net_switchport_tags_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY net_switchport_tags + ADD CONSTRAINT net_switchport_tags_pkey PRIMARY KEY (portid, vlanid); + + +-- +-- Name: net_switchports_mac_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY net_switchport + ADD CONSTRAINT net_switchports_mac_key UNIQUE (mac); + + +-- +-- Name: net_switchports_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY net_switchport + ADD CONSTRAINT net_switchports_pkey PRIMARY KEY (portid); + + +-- +-- Name: net_vlan_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY net_vlan + ADD CONSTRAINT net_vlan_pkey PRIMARY KEY (vlanid); + + +-- +-- Name: news_feed_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY news_feed + ADD CONSTRAINT news_feed_pkey PRIMARY KEY (newsentryid); + + +-- +-- Name: nipsweb_migrate_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY nipsweb_migrate + ADD CONSTRAINT nipsweb_migrate_pkey PRIMARY KEY (memberid); + + +-- +-- Name: officer_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY officer + ADD CONSTRAINT officer_pkey PRIMARY KEY (officerid); + + +-- +-- Name: rec_cleanlookup_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_cleanlookup + ADD CONSTRAINT rec_cleanlookup_pkey PRIMARY KEY (clean_code); + + +-- +-- Name: rec_formatlookup_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_formatlookup + ADD CONSTRAINT rec_formatlookup_pkey PRIMARY KEY (format_code); + + +-- +-- Name: rec_genrelookup_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_genrelookup + ADD CONSTRAINT rec_genrelookup_pkey PRIMARY KEY (genre_code); + + +-- +-- Name: rec_itunes_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_itunes + ADD CONSTRAINT rec_itunes_pkey PRIMARY KEY (trackid); + + +-- +-- Name: rec_labelqueue_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_labelqueue + ADD CONSTRAINT rec_labelqueue_pkey PRIMARY KEY (queueid); + + +-- +-- Name: rec_locationlookup_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_locationlookup + ADD CONSTRAINT rec_locationlookup_pkey PRIMARY KEY (location_code); + + +-- +-- Name: rec_lookup_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_lookup + ADD CONSTRAINT rec_lookup_pkey PRIMARY KEY (code_type, code); + + +-- +-- Name: rec_medialookup_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_medialookup + ADD CONSTRAINT rec_medialookup_pkey PRIMARY KEY (media_code); + + +-- +-- Name: rec_record_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_record + ADD CONSTRAINT rec_record_pkey PRIMARY KEY (recordid); + + +-- +-- Name: rec_statuslookup_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_statuslookup + ADD CONSTRAINT rec_statuslookup_pkey PRIMARY KEY (status_code); + + +-- +-- Name: rec_track_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_track + ADD CONSTRAINT rec_track_pkey PRIMARY KEY (trackid); + + +-- +-- Name: rec_track_trackid_recordid_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_track + ADD CONSTRAINT rec_track_trackid_recordid_key UNIQUE (trackid, recordid); + + +-- +-- Name: rec_trackcorrection_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_trackcorrection + ADD CONSTRAINT rec_trackcorrection_pkey PRIMARY KEY (correctionid); + + +-- +-- Name: recommended_listening_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY recommended_listening + ADD CONSTRAINT recommended_listening_pkey PRIMARY KEY (week, "position"); + + +-- +-- Name: selector_actions_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY selector_actions + ADD CONSTRAINT selector_actions_pkey PRIMARY KEY (action); + + +-- +-- Name: selector_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY selector + ADD CONSTRAINT selector_pkey PRIMARY KEY (selid); + + +-- +-- Name: sso_session_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY sso_session + ADD CONSTRAINT sso_session_pkey PRIMARY KEY (id); + + +-- +-- Name: strm_client_clientname_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY strm_useragent + ADD CONSTRAINT strm_client_clientname_key UNIQUE (useragent); + + +-- +-- Name: strm_client_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY strm_useragent + ADD CONSTRAINT strm_client_pkey PRIMARY KEY (useragentid); + + +-- +-- Name: strm_log_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY strm_log + ADD CONSTRAINT strm_log_pkey PRIMARY KEY (logid); + + +-- +-- Name: strm_logfile_filename_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY strm_logfile + ADD CONSTRAINT strm_logfile_filename_key UNIQUE (filename); + + +-- +-- Name: strm_logfile_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY strm_logfile + ADD CONSTRAINT strm_logfile_pkey PRIMARY KEY (logfileid); + + +-- +-- Name: strm_stream_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY strm_stream + ADD CONSTRAINT strm_stream_pkey PRIMARY KEY (streamid); + + +-- +-- Name: strm_stream_streamname_key; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY strm_stream + ADD CONSTRAINT strm_stream_streamname_key UNIQUE (streamname); + + +-- +-- Name: team_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY team + ADD CONSTRAINT team_pkey PRIMARY KEY (teamid); + + +-- +-- Name: terms_pkey; Type: CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY terms + ADD CONSTRAINT terms_pkey PRIMARY KEY (termid); + + +SET search_path = schedule, pg_catalog; + +-- +-- Name: block_direct_rules_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY block_show_rule + ADD CONSTRAINT block_direct_rules_pkey PRIMARY KEY (block_show_rule_id); + + +-- +-- Name: block_range_rule_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY block_range_rule + ADD CONSTRAINT block_range_rule_pkey PRIMARY KEY (block_range_rule_id); + + +-- +-- Name: blocks_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY block + ADD CONSTRAINT blocks_pkey PRIMARY KEY (block_id); + + +-- +-- Name: genre_name_key; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY genre + ADD CONSTRAINT genre_name_key UNIQUE (name); + + +-- +-- Name: genre_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY genre + ADD CONSTRAINT genre_pkey PRIMARY KEY (genre_id); + + +-- +-- Name: location_location_name_key; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY location + ADD CONSTRAINT location_location_name_key UNIQUE (location_name); + + +-- +-- Name: location_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY location + ADD CONSTRAINT location_pkey PRIMARY KEY (location_id); + + +-- +-- Name: season_metadata_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY season_metadata + ADD CONSTRAINT season_metadata_pkey PRIMARY KEY (season_metadata_id); + + +-- +-- Name: show_credit_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_credit + ADD CONSTRAINT show_credit_pkey PRIMARY KEY (show_credit_id); + + +-- +-- Name: show_genre_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_genre + ADD CONSTRAINT show_genre_pkey PRIMARY KEY (show_genre_id); + + +-- +-- Name: show_image_metadata_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_image_metadata + ADD CONSTRAINT show_image_metadata_pkey PRIMARY KEY (show_image_metadata_id); + + +-- +-- Name: show_location_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_location + ADD CONSTRAINT show_location_pkey PRIMARY KEY (show_location_id); + + +-- +-- Name: show_metadata_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_metadata + ADD CONSTRAINT show_metadata_pkey PRIMARY KEY (show_metadata_id); + + +-- +-- Name: show_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show + ADD CONSTRAINT show_pkey PRIMARY KEY (show_id); + + +-- +-- Name: show_podcast_link_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_podcast_link + ADD CONSTRAINT show_podcast_link_pkey PRIMARY KEY (podcast_id, show_id); + + +-- +-- Name: show_season_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_season + ADD CONSTRAINT show_season_pkey PRIMARY KEY (show_season_id); + + +-- +-- Name: show_season_requested_time_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_season_requested_time + ADD CONSTRAINT show_season_requested_time_pkey PRIMARY KEY (show_season_requested_time_id); + + +-- +-- Name: show_season_requested_week_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_season_requested_week + ADD CONSTRAINT show_season_requested_week_pkey PRIMARY KEY (show_season_requested_week_id); + + +-- +-- Name: show_season_requested_week_show_season_id_key; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_season_requested_week + ADD CONSTRAINT show_season_requested_week_show_season_id_key UNIQUE (show_season_id, week); + + +-- +-- Name: show_season_timeslot_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_season_timeslot + ADD CONSTRAINT show_season_timeslot_pkey PRIMARY KEY (show_season_timeslot_id); + + +-- +-- Name: show_season_timeslot_start_time_key; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_season_timeslot + ADD CONSTRAINT show_season_timeslot_start_time_key UNIQUE (start_time); + + +-- +-- Name: show_type_name_key; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_type + ADD CONSTRAINT show_type_name_key UNIQUE (name); + + +-- +-- Name: show_type_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_type + ADD CONSTRAINT show_type_pkey PRIMARY KEY (show_type_id); + + +-- +-- Name: timeslot_metadata_pkey; Type: CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY timeslot_metadata + ADD CONSTRAINT timeslot_metadata_pkey PRIMARY KEY (timeslot_metadata_id); + + +SET search_path = sis2, pg_catalog; + +-- +-- Name: sis_type_pkey; Type: CONSTRAINT; Schema: sis2 +-- + +ALTER TABLE ONLY commtype + ADD CONSTRAINT sis_type_pkey PRIMARY KEY (commtypeid); + + +-- +-- Name: config_pkey; Type: CONSTRAINT; Schema: sis2 +-- + +ALTER TABLE ONLY config + ADD CONSTRAINT config_pkey PRIMARY KEY (setting); + + +-- +-- Name: member_options_pkey; Type: CONSTRAINT; Schema: sis2 +-- + +ALTER TABLE ONLY member_options + ADD CONSTRAINT member_options_pkey PRIMARY KEY (memberid); + + +-- +-- Name: member_signin_memberid_show_season_timeslot_id_key; Type: CONSTRAINT; Schema: sis2 +-- + +ALTER TABLE ONLY member_signin + ADD CONSTRAINT member_signin_memberid_show_season_timeslot_id_key UNIQUE (memberid, show_season_timeslot_id); + + +-- +-- Name: member_signin_pkey; Type: CONSTRAINT; Schema: sis2 +-- + +ALTER TABLE ONLY member_signin + ADD CONSTRAINT member_signin_pkey PRIMARY KEY (member_signin_id); + + +-- +-- Name: sis_status_pkey; Type: CONSTRAINT; Schema: sis2 +-- + +ALTER TABLE ONLY statustype + ADD CONSTRAINT sis_status_pkey PRIMARY KEY (statusid); + + +SET search_path = tracklist, pg_catalog; + +-- +-- Name: pri_track_notrec; Type: CONSTRAINT; Schema: tracklist +-- + +ALTER TABLE ONLY track_notrec + ADD CONSTRAINT pri_track_notrec PRIMARY KEY (audiologid); + + +-- +-- Name: pri_track_rec; Type: CONSTRAINT; Schema: tracklist +-- + +ALTER TABLE ONLY track_rec + ADD CONSTRAINT pri_track_rec PRIMARY KEY (audiologid); + + +-- +-- Name: source_source_key; Type: CONSTRAINT; Schema: tracklist +-- + +ALTER TABLE ONLY source + ADD CONSTRAINT source_source_key UNIQUE (source); + + +-- +-- Name: source_sourceid_key; Type: CONSTRAINT; Schema: tracklist +-- + +ALTER TABLE ONLY source + ADD CONSTRAINT source_sourceid_key UNIQUE (sourceid); + + +-- +-- Name: state_stateid_key; Type: CONSTRAINT; Schema: tracklist +-- + +ALTER TABLE ONLY state + ADD CONSTRAINT state_stateid_key UNIQUE (stateid); + + +-- +-- Name: tracklist_pkey; Type: CONSTRAINT; Schema: tracklist +-- + +ALTER TABLE ONLY tracklist + ADD CONSTRAINT tracklist_pkey PRIMARY KEY (audiologid); + + +SET search_path = uryplayer, pg_catalog; + +-- +-- Name: podcast_credit_pkey; Type: CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_credit + ADD CONSTRAINT podcast_credit_pkey PRIMARY KEY (podcast_credit_id); + + +-- +-- Name: podcast_image_metadata_pkey; Type: CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_image_metadata + ADD CONSTRAINT podcast_image_metadata_pkey PRIMARY KEY (podcast_image_metadata_id); + + +-- +-- Name: podcast_metadata_pkey; Type: CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_metadata + ADD CONSTRAINT podcast_metadata_pkey PRIMARY KEY (podcast_metadata_id); + + +-- +-- Name: podcast_package_entry_pkey; Type: CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_package_entry + ADD CONSTRAINT podcast_package_entry_pkey PRIMARY KEY (podcast_package_entry_id); + + +-- +-- Name: podcast_pkey; Type: CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast + ADD CONSTRAINT podcast_pkey PRIMARY KEY (podcast_id); + + +SET search_path = webcam, pg_catalog; + +-- +-- Name: memberviews_pkey; Type: CONSTRAINT; Schema: webcam +-- + +ALTER TABLE ONLY memberviews + ADD CONSTRAINT memberviews_pkey PRIMARY KEY (memberid); + + +-- +-- Name: streams_pkey; Type: CONSTRAINT; Schema: webcam +-- + +ALTER TABLE ONLY streams + ADD CONSTRAINT streams_pkey PRIMARY KEY (streamid); + + +SET search_path = website, pg_catalog; + +-- +-- Name: banner_campaign_pkey; Type: CONSTRAINT; Schema: website +-- + +ALTER TABLE ONLY banner_campaign + ADD CONSTRAINT banner_campaign_pkey PRIMARY KEY (banner_campaign_id); + + +-- +-- Name: banner_location_pkey; Type: CONSTRAINT; Schema: website +-- + +ALTER TABLE ONLY banner_location + ADD CONSTRAINT banner_location_pkey PRIMARY KEY (banner_location_id); + + +-- +-- Name: banner_pkey; Type: CONSTRAINT; Schema: website +-- + +ALTER TABLE ONLY banner + ADD CONSTRAINT banner_pkey PRIMARY KEY (banner_id); + + +-- +-- Name: banner_timeslot_pkey; Type: CONSTRAINT; Schema: website +-- + +ALTER TABLE ONLY banner_timeslot + ADD CONSTRAINT banner_timeslot_pkey PRIMARY KEY (id); + + +-- +-- Name: banner_type_pkey; Type: CONSTRAINT; Schema: website +-- + +ALTER TABLE ONLY banner_type + ADD CONSTRAINT banner_type_pkey PRIMARY KEY (banner_type_id); + + +SET search_path = jukebox, pg_catalog; + +-- +-- Name: playlist_availability_approvedid_idx; Type: INDEX; Schema: jukebox +-- + +CREATE INDEX playlist_availability_approvedid_idx ON playlist_availability USING btree (approvedid); + + +-- +-- Name: playlist_availability_banner_id_idx; Type: INDEX; Schema: jukebox +-- + +CREATE INDEX playlist_availability_banner_id_idx ON playlist_availability USING btree (playlistid); + + +-- +-- Name: playlist_availability_banner_location_id_idx; Type: INDEX; Schema: jukebox +-- + +CREATE INDEX playlist_availability_banner_location_id_idx ON playlist_availability USING btree (weight); + + +-- +-- Name: playlist_availability_effective_from_idx; Type: INDEX; Schema: jukebox +-- + +CREATE INDEX playlist_availability_effective_from_idx ON playlist_availability USING btree (effective_from); + + +-- +-- Name: playlist_availability_effective_to_idx; Type: INDEX; Schema: jukebox +-- + +CREATE INDEX playlist_availability_effective_to_idx ON playlist_availability USING btree (effective_to); + + +-- +-- Name: playlist_availability_memberid_idx; Type: INDEX; Schema: jukebox +-- + +CREATE INDEX playlist_availability_memberid_idx ON playlist_availability USING btree (memberid); + + +SET search_path = metadata, pg_catalog; + +-- +-- Name: package_image_metadata_approvedid; Type: INDEX; Schema: metadata +-- + +CREATE INDEX package_image_metadata_approvedid ON package_image_metadata USING btree (approvedid); + + +-- +-- Name: package_image_metadata_element_id; Type: INDEX; Schema: metadata +-- + +CREATE INDEX package_image_metadata_element_id ON package_image_metadata USING btree (element_id); + + +-- +-- Name: package_image_metadata_memberid; Type: INDEX; Schema: metadata +-- + +CREATE INDEX package_image_metadata_memberid ON package_image_metadata USING btree (memberid); + + +-- +-- Name: package_image_metadata_metadata_key_id; Type: INDEX; Schema: metadata +-- + +CREATE INDEX package_image_metadata_metadata_key_id ON package_image_metadata USING btree (metadata_key_id); + + +-- +-- Name: package_name; Type: INDEX; Schema: metadata +-- + +CREATE INDEX package_name ON package USING btree (name); + + +-- +-- Name: package_name_like; Type: INDEX; Schema: metadata +-- + +CREATE INDEX package_name_like ON package USING btree (name varchar_pattern_ops); + + +-- +-- Name: package_text_metadata_approvedid; Type: INDEX; Schema: metadata +-- + +CREATE INDEX package_text_metadata_approvedid ON package_text_metadata USING btree (approvedid); + + +-- +-- Name: package_text_metadata_element_id; Type: INDEX; Schema: metadata +-- + +CREATE INDEX package_text_metadata_element_id ON package_text_metadata USING btree (element_id); + + +-- +-- Name: package_text_metadata_memberid; Type: INDEX; Schema: metadata +-- + +CREATE INDEX package_text_metadata_memberid ON package_text_metadata USING btree (memberid); + + +-- +-- Name: package_text_metadata_metadata_key_id; Type: INDEX; Schema: metadata +-- + +CREATE INDEX package_text_metadata_metadata_key_id ON package_text_metadata USING btree (metadata_key_id); + + +SET search_path = music, pg_catalog; + +-- +-- Name: chart_release_chart_type_id; Type: INDEX; Schema: music +-- + +CREATE INDEX chart_release_chart_type_id ON chart_release USING btree (chart_type_id); + + +-- +-- Name: chart_row_chart_release_id; Type: INDEX; Schema: music +-- + +CREATE INDEX chart_row_chart_release_id ON chart_row USING btree (chart_release_id); + + +-- +-- Name: chart_type_name; Type: INDEX; Schema: music +-- + +CREATE INDEX chart_type_name ON chart_type USING btree (name); + + +-- +-- Name: chart_type_name_like; Type: INDEX; Schema: music +-- + +CREATE INDEX chart_type_name_like ON chart_type USING btree (name varchar_pattern_ops); + + +SET search_path = people, pg_catalog; + +-- +-- Name: group_root_role_group_type_id; Type: INDEX; Schema: people +-- + +CREATE INDEX group_root_role_group_type_id ON group_root_role USING btree (group_type_id); + + +-- +-- Name: role_metadata_effective_from_index; Type: INDEX; Schema: people +-- + +CREATE INDEX role_metadata_effective_from_index ON role_text_metadata USING btree (effective_from); + + +-- +-- Name: role_metadata_effective_to_index; Type: INDEX; Schema: people +-- + +CREATE INDEX role_metadata_effective_to_index ON role_text_metadata USING btree (effective_to); + + +SET search_path = public, pg_catalog; + +-- +-- Name: audiolog_timeplayed_index; Type: INDEX; Schema: public +-- + +CREATE INDEX audiolog_timeplayed_index ON baps_audiolog USING btree (timeplayed); + + +-- +-- Name: audiolog_timestopped_index; Type: INDEX; Schema: public +-- + +CREATE INDEX audiolog_timestopped_index ON baps_audiolog USING btree (timestopped); + + +-- +-- Name: baps_item_fileitemid_key; Type: INDEX; Schema: public +-- + +CREATE INDEX baps_item_fileitemid_key ON baps_item USING btree (fileitemid); + + +-- +-- Name: baps_item_libraryitemid_key; Type: INDEX; Schema: public +-- + +CREATE INDEX baps_item_libraryitemid_key ON baps_item USING btree (libraryitemid); + + +-- +-- Name: baps_item_listingid_key; Type: INDEX; Schema: public +-- + +CREATE INDEX baps_item_listingid_key ON baps_item USING btree (listingid); + + +-- +-- Name: baps_item_position_key; Type: INDEX; Schema: public +-- + +CREATE INDEX baps_item_position_key ON baps_item USING btree ("position"); + + +-- +-- Name: baps_item_textitemid_key; Type: INDEX; Schema: public +-- + +CREATE INDEX baps_item_textitemid_key ON baps_item USING btree (textitemid); + + +-- +-- Name: baps_item_viewable_key; Type: INDEX; Schema: public +-- + +CREATE INDEX baps_item_viewable_key ON baps_show USING btree (viewable); + + +-- +-- Name: baps_libraryitem_trackid_index; Type: INDEX; Schema: public +-- + +CREATE INDEX baps_libraryitem_trackid_index ON baps_libraryitem USING btree (trackid); + + +-- +-- Name: baps_listing_channel_key; Type: INDEX; Schema: public +-- + +CREATE INDEX baps_listing_channel_key ON baps_listing USING btree (channel); + + +-- +-- Name: baps_listing_showid_key; Type: INDEX; Schema: public +-- + +CREATE INDEX baps_listing_showid_key ON baps_listing USING btree (showid); + + +-- +-- Name: baps_show_broadcastdate_index; Type: INDEX; Schema: public +-- + +CREATE INDEX baps_show_broadcastdate_index ON baps_show USING btree (broadcastdate); + + +-- +-- Name: baps_show_userid_key; Type: INDEX; Schema: public +-- + +CREATE INDEX baps_show_userid_key ON baps_show USING btree (userid); + + +-- +-- Name: baps_user_usernamechart_key; Type: INDEX; Schema: public +-- + +CREATE INDEX baps_user_usernamechart_key ON baps_user USING btree (username) WHERE ((username)::text = 'chart'::text); + + +-- +-- Name: chartweektimestamp; Type: INDEX; Schema: public +-- + +CREATE INDEX chartweektimestamp ON chart USING btree (chartweek); + + +-- +-- Name: i_endtime; Type: INDEX; Schema: public +-- + +CREATE INDEX i_endtime ON strm_log USING btree (endtime); + + +-- +-- Name: idx_member_eduroam; Type: INDEX; Schema: public +-- + +CREATE INDEX idx_member_eduroam ON member USING btree (eduroam); + + +-- +-- Name: idx_member_email; Type: INDEX; Schema: public +-- + +CREATE INDEX idx_member_email ON member USING btree (email); + + +-- +-- Name: idx_member_localalias; Type: INDEX; Schema: public +-- + +CREATE INDEX idx_member_localalias ON member USING btree (local_alias); + + +-- +-- Name: idx_member_localname; Type: INDEX; Schema: public +-- + +CREATE INDEX idx_member_localname ON member USING btree (local_name); + + +-- +-- Name: l_college_collegeid_key; Type: INDEX; Schema: public +-- + +CREATE UNIQUE INDEX l_college_collegeid_key ON l_college USING btree (collegeid); + + +-- +-- Name: l_musicinterest_typeid_key; Type: INDEX; Schema: public +-- + +CREATE UNIQUE INDEX l_musicinterest_typeid_key ON l_musicinterest USING btree (typeid); + + +-- +-- Name: member_memberid_key; Type: INDEX; Schema: public +-- + +CREATE UNIQUE INDEX member_memberid_key ON member USING btree (memberid); + + +-- +-- Name: member_office_member_office_key; Type: INDEX; Schema: public +-- + +CREATE UNIQUE INDEX member_office_member_office_key ON member_officer USING btree (member_officerid); + + +-- +-- Name: officer_officerid_key; Type: INDEX; Schema: public +-- + +CREATE UNIQUE INDEX officer_officerid_key ON officer USING btree (officerid); + + +-- +-- Name: rec_record_dateadded_key; Type: INDEX; Schema: public +-- + +CREATE INDEX rec_record_dateadded_key ON rec_record USING btree (dateadded); + + +-- +-- Name: rec_record_format_key; Type: INDEX; Schema: public +-- + +CREATE INDEX rec_record_format_key ON rec_record USING btree (format) WHERE (format = 's'::bpchar); + + +-- +-- Name: rec_record_recordid_key; Type: INDEX; Schema: public +-- + +CREATE UNIQUE INDEX rec_record_recordid_key ON rec_record USING btree (recordid); + + +-- +-- Name: rec_track_artist_index; Type: INDEX; Schema: public +-- + +CREATE INDEX rec_track_artist_index ON rec_track USING btree (artist); + + +-- +-- Name: rec_track_recordid_key; Type: INDEX; Schema: public +-- + +CREATE INDEX rec_track_recordid_key ON rec_track USING btree (recordid); + + +-- +-- Name: rec_track_trackid_key; Type: INDEX; Schema: public +-- + +CREATE UNIQUE INDEX rec_track_trackid_key ON rec_track USING btree (trackid); + + +-- +-- Name: rec_unique_recordid; Type: INDEX; Schema: public +-- + +CREATE INDEX rec_unique_recordid ON rec_labelqueue USING btree (recordid); + + +-- +-- Name: recommended_listening_chartweek_key; Type: INDEX; Schema: public +-- + +CREATE INDEX recommended_listening_chartweek_key ON recommended_listening USING btree (week); + +-- +-- Name: strm_log_starttime_key; Type: INDEX; Schema: public +-- + +CREATE INDEX strm_log_starttime_key ON strm_log USING btree (starttime); + +ALTER TABLE strm_log CLUSTER ON strm_log_starttime_key; + + +-- +-- Name: strm_log_steamid_key; Type: INDEX; Schema: public +-- + +CREATE INDEX strm_log_steamid_key ON strm_log USING btree (streamid); + + +-- +-- Name: team_teamid_key; Type: INDEX; Schema: public +-- + +CREATE UNIQUE INDEX team_teamid_key ON team USING btree (teamid); + + +SET search_path = schedule, pg_catalog; + +-- +-- Name: block_range_rule_end_time_index; Type: INDEX; Schema: schedule +-- + +CREATE INDEX block_range_rule_end_time_index ON block_range_rule USING btree (end_time); + + +-- +-- Name: block_range_rule_start_time_index; Type: INDEX; Schema: schedule +-- + +CREATE INDEX block_range_rule_start_time_index ON block_range_rule USING btree (start_time); + + +-- +-- Name: duration; Type: INDEX; Schema: schedule +-- + +CREATE INDEX duration ON show_season_timeslot USING btree (duration); + + +-- +-- Name: season_metadata_effective_from_index; Type: INDEX; Schema: schedule +-- + +CREATE INDEX season_metadata_effective_from_index ON season_metadata USING btree (effective_from); + + +-- +-- Name: season_metadata_effective_to_index; Type: INDEX; Schema: schedule +-- + +CREATE INDEX season_metadata_effective_to_index ON season_metadata USING btree (effective_to); + + +-- +-- Name: show_credit_effective_from_index; Type: INDEX; Schema: schedule +-- + +CREATE INDEX show_credit_effective_from_index ON show_credit USING btree (effective_from); + + +-- +-- Name: show_credit_effective_to_index; Type: INDEX; Schema: schedule +-- + +CREATE INDEX show_credit_effective_to_index ON show_credit USING btree (effective_to); + + +-- +-- Name: show_id_index; Type: INDEX; Schema: schedule +-- + +CREATE INDEX show_id_index ON show_credit USING btree (show_id); + + +-- +-- Name: show_image_metadata_approvedid; Type: INDEX; Schema: schedule +-- + +CREATE INDEX show_image_metadata_approvedid ON show_image_metadata USING btree (approvedid); + + +-- +-- Name: show_image_metadata_memberid; Type: INDEX; Schema: schedule +-- + +CREATE INDEX show_image_metadata_memberid ON show_image_metadata USING btree (memberid); + + +-- +-- Name: show_image_metadata_metadata_key_id; Type: INDEX; Schema: schedule +-- + +CREATE INDEX show_image_metadata_metadata_key_id ON show_image_metadata USING btree (metadata_key_id); + + +-- +-- Name: show_image_metadata_show_id; Type: INDEX; Schema: schedule +-- + +CREATE INDEX show_image_metadata_show_id ON show_image_metadata USING btree (show_id); + + +-- +-- Name: show_metadata_effective_from_index; Type: INDEX; Schema: schedule +-- + +CREATE INDEX show_metadata_effective_from_index ON show_metadata USING btree (effective_from); + + +-- +-- Name: show_metadata_effective_to_index; Type: INDEX; Schema: schedule +-- + +CREATE INDEX show_metadata_effective_to_index ON show_metadata USING btree (effective_to); + + +-- +-- Name: show_podcast_link_podcast_id; Type: INDEX; Schema: schedule +-- + +CREATE INDEX show_podcast_link_podcast_id ON show_podcast_link USING btree (podcast_id); + + +-- +-- Name: show_season_index; Type: INDEX; Schema: schedule +-- + +CREATE INDEX show_season_index ON show_season_timeslot USING btree (show_season_id); + + +-- +-- Name: start_time_index; Type: INDEX; Schema: schedule +-- + +CREATE INDEX start_time_index ON show_season_timeslot USING btree (start_time); + + +-- +-- Name: timeslot_metadata_effective_from_index; Type: INDEX; Schema: schedule +-- + +CREATE INDEX timeslot_metadata_effective_from_index ON timeslot_metadata USING btree (effective_from); + + +-- +-- Name: timeslot_metadata_effective_to_index; Type: INDEX; Schema: schedule +-- + +CREATE INDEX timeslot_metadata_effective_to_index ON timeslot_metadata USING btree (effective_to); + + +SET search_path = tracklist, pg_catalog; + +-- +-- Name: index_tracklist_tracklist_timeslotid; Type: INDEX; Schema: tracklist +-- + +CREATE INDEX index_tracklist_tracklist_timeslotid ON tracklist USING btree (timeslotid); + + +-- +-- Name: index_tracklist_tracklist_timestart; Type: INDEX; Schema: tracklist +-- + +CREATE INDEX index_tracklist_tracklist_timestart ON tracklist USING btree (timestart); + + +-- +-- Name: index_tracklist_tracklist_timestop; Type: INDEX; Schema: tracklist +-- + +CREATE INDEX index_tracklist_tracklist_timestop ON tracklist USING btree (timestop); + + +-- +-- Name: tracklist_tracklist_timeslotid; Type: INDEX; Schema: tracklist +-- + +CREATE INDEX tracklist_tracklist_timeslotid ON tracklist USING btree (timeslotid); + + +SET search_path = uryplayer, pg_catalog; + +-- +-- Name: podcast_approvedid; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_approvedid ON podcast USING btree (approvedid); + + +-- +-- Name: podcast_credit_approvedid; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_credit_approvedid ON podcast_credit USING btree (approvedid); + + +-- +-- Name: podcast_credit_credit_type_id; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_credit_credit_type_id ON podcast_credit USING btree (credit_type_id); + + +-- +-- Name: podcast_credit_creditid; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_credit_creditid ON podcast_credit USING btree (creditid); + + +-- +-- Name: podcast_credit_memberid; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_credit_memberid ON podcast_credit USING btree (memberid); + + +-- +-- Name: podcast_credit_podcast_id; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_credit_podcast_id ON podcast_credit USING btree (podcast_id); + + +-- +-- Name: podcast_image_metadata_approvedid; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_image_metadata_approvedid ON podcast_image_metadata USING btree (approvedid); + + +-- +-- Name: podcast_image_metadata_memberid; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_image_metadata_memberid ON podcast_image_metadata USING btree (memberid); + + +-- +-- Name: podcast_image_metadata_metadata_key_id; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_image_metadata_metadata_key_id ON podcast_image_metadata USING btree (metadata_key_id); + + +-- +-- Name: podcast_image_metadata_podcast_id; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_image_metadata_podcast_id ON podcast_image_metadata USING btree (podcast_id); + + +-- +-- Name: podcast_memberid; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_memberid ON podcast USING btree (memberid); + + +-- +-- Name: podcast_metadata_approvedid; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_metadata_approvedid ON podcast_metadata USING btree (approvedid); + + +-- +-- Name: podcast_metadata_memberid; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_metadata_memberid ON podcast_metadata USING btree (memberid); + + +-- +-- Name: podcast_metadata_metadata_key_id; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_metadata_metadata_key_id ON podcast_metadata USING btree (metadata_key_id); + + +-- +-- Name: podcast_metadata_podcast_id; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_metadata_podcast_id ON podcast_metadata USING btree (podcast_id); + + +-- +-- Name: podcast_package_entry_approvedid; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_package_entry_approvedid ON podcast_package_entry USING btree (approvedid); + + +-- +-- Name: podcast_package_entry_memberid; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_package_entry_memberid ON podcast_package_entry USING btree (memberid); + + +-- +-- Name: podcast_package_entry_package_id; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_package_entry_package_id ON podcast_package_entry USING btree (package_id); + + +-- +-- Name: podcast_package_entry_podcast_id; Type: INDEX; Schema: uryplayer +-- + +CREATE INDEX podcast_package_entry_podcast_id ON podcast_package_entry USING btree (podcast_id); + + +SET search_path = website, pg_catalog; + +-- +-- Name: banner_campaign_approvedid; Type: INDEX; Schema: website +-- + +CREATE INDEX banner_campaign_approvedid ON banner_campaign USING btree (approvedid); + + +-- +-- Name: banner_campaign_banner_id; Type: INDEX; Schema: website +-- + +CREATE INDEX banner_campaign_banner_id ON banner_campaign USING btree (banner_id); + + +-- +-- Name: banner_campaign_banner_location_id; Type: INDEX; Schema: website +-- + +CREATE INDEX banner_campaign_banner_location_id ON banner_campaign USING btree (banner_location_id); + + +-- +-- Name: banner_campaign_effective_from_index; Type: INDEX; Schema: website +-- + +CREATE INDEX banner_campaign_effective_from_index ON banner_campaign USING btree (effective_from); + + +-- +-- Name: banner_campaign_effective_to_index; Type: INDEX; Schema: website +-- + +CREATE INDEX banner_campaign_effective_to_index ON banner_campaign USING btree (effective_to); + + +-- +-- Name: banner_campaign_memberid; Type: INDEX; Schema: website +-- + +CREATE INDEX banner_campaign_memberid ON banner_campaign USING btree (memberid); + + +-- +-- Name: banner_location_name; Type: INDEX; Schema: website +-- + +CREATE INDEX banner_location_name ON banner_location USING btree (name); + + +-- +-- Name: banner_location_name_like; Type: INDEX; Schema: website +-- + +CREATE INDEX banner_location_name_like ON banner_location USING btree (name varchar_pattern_ops); + + +-- +-- Name: banner_timeslot_approvedid; Type: INDEX; Schema: website +-- + +CREATE INDEX banner_timeslot_approvedid ON banner_timeslot USING btree (approvedid); + + +-- +-- Name: banner_timeslot_banner_campaign_id; Type: INDEX; Schema: website +-- + +CREATE INDEX banner_timeslot_banner_campaign_id ON banner_timeslot USING btree (banner_campaign_id); + + +-- +-- Name: banner_timeslot_from_time_index; Type: INDEX; Schema: website +-- + +CREATE INDEX banner_timeslot_from_time_index ON banner_timeslot USING btree (start_time); + + +-- +-- Name: banner_timeslot_memberid; Type: INDEX; Schema: website +-- + +CREATE INDEX banner_timeslot_memberid ON banner_timeslot USING btree (memberid); + + +-- +-- Name: banner_timeslot_to_time_index; Type: INDEX; Schema: website +-- + +CREATE INDEX banner_timeslot_to_time_index ON banner_timeslot USING btree (end_time); + + +-- +-- Name: banner_type_name; Type: INDEX; Schema: website +-- + +CREATE INDEX banner_type_name ON banner_type USING btree (name); + + +-- +-- Name: banner_type_name_like; Type: INDEX; Schema: website +-- + +CREATE INDEX banner_type_name_like ON banner_type USING btree (name varchar_pattern_ops); + + +SET search_path = public, pg_catalog; + +-- +-- Name: bapstotracklist; Type: TRIGGER; Schema: public +-- + +CREATE TRIGGER bapstotracklist AFTER UPDATE ON baps_audiolog FOR EACH ROW EXECUTE PROCEDURE bapstotracklist(); + + +-- +-- Name: clearitem; Type: TRIGGER; Schema: public +-- + +CREATE TRIGGER clearitem BEFORE DELETE ON baps_item FOR EACH ROW EXECUTE PROCEDURE clear_item_func(); + +ALTER TABLE baps_item DISABLE TRIGGER clearitem; + + +-- +-- Name: set_shelfcode_trigger; Type: TRIGGER; Schema: public +-- + +CREATE TRIGGER set_shelfcode_trigger BEFORE INSERT ON rec_record FOR EACH ROW EXECUTE PROCEDURE set_shelfcode_func(); + + +SET search_path = bapsplanner, pg_catalog; + +-- +-- Name: managed_items_managedplaylistid_fkey; Type: FK CONSTRAINT; Schema: bapsplanner +-- + +ALTER TABLE ONLY managed_items + ADD CONSTRAINT managed_items_managedplaylistid_fkey FOREIGN KEY (managedplaylistid) REFERENCES managed_playlists(managedplaylistid) ON DELETE CASCADE; + + +-- +-- Name: managed_items_memberid_fkey; Type: FK CONSTRAINT; Schema: bapsplanner +-- + +ALTER TABLE ONLY managed_items + ADD CONSTRAINT managed_items_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) ON DELETE SET NULL; + + +-- +-- Name: secure_play_token_trackid_fkey; Type: FK CONSTRAINT; Schema: bapsplanner +-- + +ALTER TABLE ONLY secure_play_token + ADD CONSTRAINT secure_play_token_trackid_fkey FOREIGN KEY (trackid) REFERENCES public.rec_track(trackid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: spt_fk_memberid; Type: FK CONSTRAINT; Schema: bapsplanner +-- + +ALTER TABLE ONLY secure_play_token + ADD CONSTRAINT spt_fk_memberid FOREIGN KEY (memberid) REFERENCES public.member(memberid); + + +-- +-- Name: timeslot_items_rec_track_id_fkey; Type: FK CONSTRAINT; Schema: bapsplanner +-- + +ALTER TABLE ONLY timeslot_items + ADD CONSTRAINT timeslot_items_rec_track_id_fkey FOREIGN KEY (rec_track_id) REFERENCES public.rec_track(trackid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: timeslot_items_timeslot_id_fkey; Type: FK CONSTRAINT; Schema: bapsplanner +-- + +ALTER TABLE ONLY timeslot_items + ADD CONSTRAINT timeslot_items_timeslot_id_fkey FOREIGN KEY (timeslot_id) REFERENCES schedule.show_season_timeslot(show_season_timeslot_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +SET search_path = jukebox, pg_catalog; + +-- +-- Name: jukebox_playlist_lock; Type: FK CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY playlists + ADD CONSTRAINT jukebox_playlist_lock FOREIGN KEY (lock) REFERENCES public.member(memberid); + + +-- +-- Name: playlist_availability_pkey; Type: FK CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY playlist_availability + ADD CONSTRAINT playlist_availability_pkey PRIMARY KEY (playlist_availability_id); + + +-- +-- Name: playlist_availability_playlistid_fkey; Type: FK CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY playlist_availability + ADD CONSTRAINT playlist_availability_playlistid_fkey FOREIGN KEY (playlistid) REFERENCES playlists(playlistid) ON UPDATE CASCADE; + + +-- +-- Name: playlist_entries_playlistid_fkey; Type: FK CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY playlist_entries + ADD CONSTRAINT playlist_entries_playlistid_fkey FOREIGN KEY (playlistid) REFERENCES playlists(playlistid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: playlist_entries_playlistid_fkey1; Type: FK CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY playlist_entries + ADD CONSTRAINT playlist_entries_playlistid_fkey1 FOREIGN KEY (playlistid, revision_added) REFERENCES playlist_revisions(playlistid, revisionid) ON DELETE CASCADE; + + +-- +-- Name: playlist_entries_playlistid_fkey2; Type: FK CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY playlist_entries + ADD CONSTRAINT playlist_entries_playlistid_fkey2 FOREIGN KEY (playlistid, revision_removed) REFERENCES playlist_revisions(playlistid, revisionid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: playlist_entries_trackid_fkey; Type: FK CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY playlist_entries + ADD CONSTRAINT playlist_entries_trackid_fkey FOREIGN KEY (trackid) REFERENCES public.rec_track(trackid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: playlist_revisions_author_fkey; Type: FK CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY playlist_revisions + ADD CONSTRAINT playlist_revisions_author_fkey FOREIGN KEY (author) REFERENCES public.member(memberid); + + +-- +-- Name: playlist_revisions_playlistid_fkey; Type: FK CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY playlist_revisions + ADD CONSTRAINT playlist_revisions_playlistid_fkey FOREIGN KEY (playlistid) REFERENCES playlists(playlistid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: playlist_timeslot_approvedid_fkey; Type: FK CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY playlist_timeslot + ADD CONSTRAINT playlist_timeslot_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid); + + +-- +-- Name: playlist_timeslot_memberid_fkey; Type: FK CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY playlist_timeslot + ADD CONSTRAINT playlist_timeslot_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid); + + +-- +-- Name: playlist_timeslot_playlist_availability_id_fkey; Type: FK CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY playlist_timeslot + ADD CONSTRAINT playlist_timeslot_playlist_availability_id_fkey FOREIGN KEY (playlist_availability_id) REFERENCES playlist_availability(playlist_availability_id); + + +-- +-- Name: request_memberid_fkey; Type: FK CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY request + ADD CONSTRAINT request_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid); + + +-- +-- Name: request_trackid_fkey; Type: FK CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY request + ADD CONSTRAINT request_trackid_fkey FOREIGN KEY (trackid) REFERENCES public.rec_track(trackid); + + +-- +-- Name: silence_log_handledby_fkey; Type: FK CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY silence_log + ADD CONSTRAINT silence_log_handledby_fkey FOREIGN KEY (handledby) REFERENCES public.member(memberid) ON DELETE SET NULL; + + +-- +-- Name: track_blacklist_trackid_fkey; Type: FK CONSTRAINT; Schema: jukebox +-- + +ALTER TABLE ONLY track_blacklist + ADD CONSTRAINT track_blacklist_trackid_fkey FOREIGN KEY (trackid) REFERENCES public.rec_track(trackid); + + +SET search_path = mail, pg_catalog; + +-- +-- Name: alias_list_alias_id_fkey; Type: FK CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY alias_list + ADD CONSTRAINT alias_list_alias_id_fkey FOREIGN KEY (alias_id) REFERENCES alias(alias_id); + + +-- +-- Name: alias_list_destination_fkey; Type: FK CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY alias_list + ADD CONSTRAINT alias_list_destination_fkey FOREIGN KEY (destination) REFERENCES public.mail_list(listid); + + +-- +-- Name: alias_member_alias_id_fkey; Type: FK CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY alias_member + ADD CONSTRAINT alias_member_alias_id_fkey FOREIGN KEY (alias_id) REFERENCES alias(alias_id); + + +-- +-- Name: alias_member_destination_fkey; Type: FK CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY alias_member + ADD CONSTRAINT alias_member_destination_fkey FOREIGN KEY (destination) REFERENCES public.member(memberid); + + +-- +-- Name: alias_officer_alias_id_fkey; Type: FK CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY alias_officer + ADD CONSTRAINT alias_officer_alias_id_fkey FOREIGN KEY (alias_id) REFERENCES alias(alias_id); + + +-- +-- Name: alias_officer_destination_fkey; Type: FK CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY alias_officer + ADD CONSTRAINT alias_officer_destination_fkey FOREIGN KEY (destination) REFERENCES public.officer(officerid); + + +-- +-- Name: alias_text_alias_id_fkey; Type: FK CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY alias_text + ADD CONSTRAINT alias_text_alias_id_fkey FOREIGN KEY (alias_id) REFERENCES alias(alias_id); + + +-- +-- Name: email_recipient_list_email_id_fkey; Type: FK CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY email_recipient_list + ADD CONSTRAINT email_recipient_list_email_id_fkey FOREIGN KEY (email_id) REFERENCES email(email_id) ON DELETE CASCADE; + + +-- +-- Name: email_recipient_list_listid_fkey; Type: FK CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY email_recipient_list + ADD CONSTRAINT email_recipient_list_listid_fkey FOREIGN KEY (listid) REFERENCES public.mail_list(listid) ON DELETE RESTRICT; + + +-- +-- Name: email_recipient_user_email_id_fkey; Type: FK CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY email_recipient_member + ADD CONSTRAINT email_recipient_user_email_id_fkey FOREIGN KEY (email_id) REFERENCES email(email_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: email_recipient_user_memberid_fkey; Type: FK CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY email_recipient_member + ADD CONSTRAINT email_recipient_user_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) ON DELETE RESTRICT; + + +-- +-- Name: email_sender_fkey; Type: FK CONSTRAINT; Schema: mail +-- + +ALTER TABLE ONLY email + ADD CONSTRAINT email_sender_fkey FOREIGN KEY (sender) REFERENCES public.member(memberid) ON DELETE RESTRICT; + + +SET search_path = metadata, pg_catalog; + +-- +-- Name: package_image_metadata_approvedid_fkey; Type: FK CONSTRAINT; Schema: metadata +-- + +ALTER TABLE ONLY package_image_metadata + ADD CONSTRAINT package_image_metadata_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: package_image_metadata_element_id_fkey; Type: FK CONSTRAINT; Schema: metadata +-- + +ALTER TABLE ONLY package_image_metadata + ADD CONSTRAINT package_image_metadata_element_id_fkey FOREIGN KEY (element_id) REFERENCES package(package_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: package_image_metadata_memberid_fkey; Type: FK CONSTRAINT; Schema: metadata +-- + +ALTER TABLE ONLY package_image_metadata + ADD CONSTRAINT package_image_metadata_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: package_image_metadata_metadata_key_id_fkey; Type: FK CONSTRAINT; Schema: metadata +-- + +ALTER TABLE ONLY package_image_metadata + ADD CONSTRAINT package_image_metadata_metadata_key_id_fkey FOREIGN KEY (metadata_key_id) REFERENCES metadata_key(metadata_key_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: package_text_metadata_approvedid_fkey; Type: FK CONSTRAINT; Schema: metadata +-- + +ALTER TABLE ONLY package_text_metadata + ADD CONSTRAINT package_text_metadata_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: package_text_metadata_element_id_fkey; Type: FK CONSTRAINT; Schema: metadata +-- + +ALTER TABLE ONLY package_text_metadata + ADD CONSTRAINT package_text_metadata_element_id_fkey FOREIGN KEY (element_id) REFERENCES package(package_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: package_text_metadata_memberid_fkey; Type: FK CONSTRAINT; Schema: metadata +-- + +ALTER TABLE ONLY package_text_metadata + ADD CONSTRAINT package_text_metadata_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: package_text_metadata_metadata_key_id_fkey; Type: FK CONSTRAINT; Schema: metadata +-- + +ALTER TABLE ONLY package_text_metadata + ADD CONSTRAINT package_text_metadata_metadata_key_id_fkey FOREIGN KEY (metadata_key_id) REFERENCES metadata_key(metadata_key_id) DEFERRABLE INITIALLY DEFERRED; + + +SET search_path = music, pg_catalog; + +-- +-- Name: chart_release_chart_type_id_fkey; Type: FK CONSTRAINT; Schema: music +-- + +ALTER TABLE ONLY chart_release + ADD CONSTRAINT chart_release_chart_type_id_fkey FOREIGN KEY (chart_type_id) REFERENCES chart_type(chart_type_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: chart_row_chart_release_id_fkey; Type: FK CONSTRAINT; Schema: music +-- + +ALTER TABLE ONLY chart_row + ADD CONSTRAINT chart_row_chart_release_id_fkey FOREIGN KEY (chart_release_id) REFERENCES chart_release(chart_release_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: chart_row_trackid_fkey; Type: FK CONSTRAINT; Schema: music +-- + +ALTER TABLE ONLY chart_row + ADD CONSTRAINT chart_row_trackid_fkey FOREIGN KEY (trackid) REFERENCES public.rec_track(trackid); + + +SET search_path = myury, pg_catalog; + +-- +-- Name: act_permission_actionid_fkey; Type: FK CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY act_permission + ADD CONSTRAINT act_permission_actionid_fkey FOREIGN KEY (actionid) REFERENCES actions(actionid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: act_permission_moduleid_fkey; Type: FK CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY act_permission + ADD CONSTRAINT act_permission_moduleid_fkey FOREIGN KEY (moduleid) REFERENCES modules(moduleid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: act_permission_serviceid_fkey; Type: FK CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY act_permission + ADD CONSTRAINT act_permission_serviceid_fkey FOREIGN KEY (serviceid) REFERENCES services(serviceid) ON DELETE CASCADE; + + +-- +-- Name: act_permission_typeid_fkey; Type: FK CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY act_permission + ADD CONSTRAINT act_permission_typeid_fkey FOREIGN KEY (typeid) REFERENCES public.l_action(typeid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: actions_moduleid_fkey; Type: FK CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY actions + ADD CONSTRAINT actions_moduleid_fkey FOREIGN KEY (moduleid) REFERENCES modules(moduleid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: api_key_auth_api_key_fkey; Type: FK CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY api_key_auth + ADD CONSTRAINT api_key_auth_api_key_fkey FOREIGN KEY (key_string) REFERENCES api_key(key_string); + + +-- +-- Name: api_key_auth_auth_id_fkey; Type: FK CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY api_key_auth + ADD CONSTRAINT api_key_auth_auth_id_fkey FOREIGN KEY (typeid) REFERENCES public.l_action(typeid); + + +-- +-- Name: api_method_auth_typeid_fkey; Type: FK CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY api_method_auth + ADD CONSTRAINT api_method_auth_typeid_fkey FOREIGN KEY (typeid) REFERENCES public.l_action(typeid); + + +-- +-- Name: award_member_awardedby_fkey; Type: FK CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY award_member + ADD CONSTRAINT award_member_awardedby_fkey FOREIGN KEY (awardedby) REFERENCES public.member(memberid) ON DELETE RESTRICT; + + +-- +-- Name: award_member_awardid_fkey; Type: FK CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY award_member + ADD CONSTRAINT award_member_awardid_fkey FOREIGN KEY (awardid) REFERENCES award_categories(awardid) ON DELETE RESTRICT; + + +-- +-- Name: award_member_memberid_fkey; Type: FK CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY award_member + ADD CONSTRAINT award_member_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) ON DELETE RESTRICT; + + +-- +-- Name: modules_serviceid_fkey; Type: FK CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY modules + ADD CONSTRAINT modules_serviceid_fkey FOREIGN KEY (serviceid) REFERENCES services(serviceid) ON DELETE CASCADE; + + +-- +-- Name: password_reset_token_memberid_fkey; Type: FK CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY password_reset_token + ADD CONSTRAINT password_reset_token_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid); + + +-- +-- Name: services_versions_member_memberid_fkey; Type: FK CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY services_versions_member + ADD CONSTRAINT services_versions_member_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid); + + +-- +-- Name: services_versions_member_serviceversionid_fkey; Type: FK CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY services_versions_member + ADD CONSTRAINT services_versions_member_serviceversionid_fkey FOREIGN KEY (serviceversionid) REFERENCES services_versions(serviceversionid); + + +-- +-- Name: services_versions_serviceid_fkey; Type: FK CONSTRAINT; Schema: myury +-- + +ALTER TABLE ONLY services_versions + ADD CONSTRAINT services_versions_serviceid_fkey FOREIGN KEY (serviceid) REFERENCES services(serviceid) ON UPDATE CASCADE ON DELETE CASCADE; + + +SET search_path = people, pg_catalog; + +-- +-- Name: group_root_role_group_leader_id_fkey; Type: FK CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY group_root_role + ADD CONSTRAINT group_root_role_group_leader_id_fkey FOREIGN KEY (group_leader_id) REFERENCES role(role_id) ON UPDATE CASCADE ON DELETE SET NULL; + + +-- +-- Name: group_root_role_group_type_id_fkey; Type: FK CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY group_root_role + ADD CONSTRAINT group_root_role_group_type_id_fkey FOREIGN KEY (group_type_id) REFERENCES group_type(group_type_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: group_root_role_role_id_id_fkey; Type: FK CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY group_root_role + ADD CONSTRAINT group_root_role_role_id_id_fkey FOREIGN KEY (role_id_id) REFERENCES role(role_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: metadata_roleid_fkey; Type: FK CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY metadata + ADD CONSTRAINT metadata_roleid_fkey FOREIGN KEY (roleid) REFERENCES role(role_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: parents_childid_fkey; Type: FK CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY role_inheritance + ADD CONSTRAINT parents_childid_fkey FOREIGN KEY (child_id) REFERENCES role(role_id) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: parents_parentid_fkey; Type: FK CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY role_inheritance + ADD CONSTRAINT parents_parentid_fkey FOREIGN KEY (parent_id) REFERENCES role(role_id) ON UPDATE CASCADE ON DELETE RESTRICT; + + + +-- +-- Name: role_metadata_approvedid_fkey; Type: FK CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY role_text_metadata + ADD CONSTRAINT role_metadata_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: role_metadata_memberid_fkey; Type: FK CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY role_text_metadata + ADD CONSTRAINT role_metadata_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: role_metadata_metadata_key_id_fkey; Type: FK CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY role_text_metadata + ADD CONSTRAINT role_metadata_metadata_key_id_fkey FOREIGN KEY (metadata_key_id) REFERENCES metadata.metadata_key(metadata_key_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: role_metadata_role_id_fkey; Type: FK CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY role_text_metadata + ADD CONSTRAINT role_metadata_role_id_fkey FOREIGN KEY (role_id) REFERENCES role(role_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: roles_visibilitylevel_fkey; Type: FK CONSTRAINT; Schema: people +-- + +ALTER TABLE ONLY role + ADD CONSTRAINT roles_visibilitylevel_fkey FOREIGN KEY (visibilitylevel) REFERENCES role_visibility(role_visibility_id) ON UPDATE CASCADE ON DELETE RESTRICT; + + +SET search_path = public, pg_catalog; + +-- +-- Name: $1; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member_officer + ADD CONSTRAINT "$1" FOREIGN KEY (memberid) REFERENCES member(memberid) ON DELETE RESTRICT; + + +-- +-- Name: $1; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY team + ADD CONSTRAINT "$1" FOREIGN KEY (status) REFERENCES l_status(statusid) ON DELETE RESTRICT; + + +-- +-- Name: $1; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY officer + ADD CONSTRAINT "$1" FOREIGN KEY (teamid) REFERENCES team(teamid) ON DELETE RESTRICT; + + +-- +-- Name: $1; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member_presenterstatus + ADD CONSTRAINT "$1" FOREIGN KEY (confirmedby) REFERENCES member(memberid) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: $1; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_track + ADD CONSTRAINT "$1" FOREIGN KEY (recordid) REFERENCES rec_record(recordid) ON DELETE CASCADE; + + +-- +-- Name: $1; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_record + ADD CONSTRAINT "$1" FOREIGN KEY (status) REFERENCES rec_statuslookup(status_code) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: $1; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_labelqueue + ADD CONSTRAINT "$1" FOREIGN KEY (recordid) REFERENCES rec_record(recordid) ON UPDATE RESTRICT ON DELETE CASCADE; + + +-- +-- Name: $1; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_user_external + ADD CONSTRAINT "$1" FOREIGN KEY (userid) REFERENCES baps_user(userid) ON UPDATE RESTRICT ON DELETE CASCADE; + + +-- +-- Name: $1; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_user_filefolder + ADD CONSTRAINT "$1" FOREIGN KEY (userid) REFERENCES baps_user(userid) ON UPDATE RESTRICT ON DELETE CASCADE; + + +-- +-- Name: $1; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_filefolder + ADD CONSTRAINT "$1" FOREIGN KEY (owner) REFERENCES baps_user(userid) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: $1; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_audiolog + ADD CONSTRAINT "$1" FOREIGN KEY (serverid) REFERENCES baps_server(serverid) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: $1; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_item + ADD CONSTRAINT "$1" FOREIGN KEY (textitemid) REFERENCES baps_textitem(textitemid) ON UPDATE RESTRICT ON DELETE CASCADE; + + +-- +-- Name: $1; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_libraryitem + ADD CONSTRAINT "$1" FOREIGN KEY (recordid) REFERENCES rec_record(recordid) ON UPDATE RESTRICT ON DELETE CASCADE; + + +-- +-- Name: $2; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member + ADD CONSTRAINT "$2" FOREIGN KEY (college) REFERENCES l_college(collegeid) ON DELETE RESTRICT; + + +-- +-- Name: $2; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY officer + ADD CONSTRAINT "$2" FOREIGN KEY (status) REFERENCES l_status(statusid) ON DELETE RESTRICT; + + +-- +-- Name: $2; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member_officer + ADD CONSTRAINT "$2" FOREIGN KEY (officerid) REFERENCES officer(officerid) ON DELETE RESTRICT; + + +-- +-- Name: $2; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member_presenterstatus + ADD CONSTRAINT "$2" FOREIGN KEY (memberid) REFERENCES member(memberid) ON UPDATE RESTRICT ON DELETE CASCADE; + + +-- +-- Name: $2; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY strm_log + ADD CONSTRAINT "$2" FOREIGN KEY (streamid) REFERENCES strm_stream(streamid) ON UPDATE RESTRICT ON DELETE CASCADE; + + +-- +-- Name: $2; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_track + ADD CONSTRAINT "$2" FOREIGN KEY (genre) REFERENCES rec_genrelookup(genre_code) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: $2; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_record + ADD CONSTRAINT "$2" FOREIGN KEY (media) REFERENCES rec_medialookup(media_code) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: $2; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_user_filefolder + ADD CONSTRAINT "$2" FOREIGN KEY (filefolderid) REFERENCES baps_filefolder(filefolderid) ON UPDATE RESTRICT ON DELETE CASCADE; + + +-- +-- Name: $2; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_audiolog + ADD CONSTRAINT "$2" FOREIGN KEY (audioid) REFERENCES baps_audio(audioid) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: $2; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_libraryitem + ADD CONSTRAINT "$2" FOREIGN KEY (trackid) REFERENCES rec_track(trackid) ON UPDATE RESTRICT ON DELETE CASCADE; + + +-- +-- Name: $2; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_user_external + ADD CONSTRAINT "$2" FOREIGN KEY (externalid) REFERENCES member(memberid) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: $3; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY strm_log + ADD CONSTRAINT "$3" FOREIGN KEY (useragentid) REFERENCES strm_useragent(useragentid) ON UPDATE RESTRICT ON DELETE CASCADE; + + +-- +-- Name: $3; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_track + ADD CONSTRAINT "$3" FOREIGN KEY (clean) REFERENCES rec_cleanlookup(clean_code) ON UPDATE RESTRICT ON DELETE RESTRICT; + +-- +-- Name: rec_track_lasteditedby_fkey Type: FK CONSTRAINT; Schema: public; Owner: myradio +-- + +ALTER TABLE ONLY rec_track + ADD CONSTRAINT rec_track_lasteditedby_fkey FOREIGN KEY (last_edited_memberid) REFERENCES member(memberid) ON UPDATE CASCADE ON DELETE SET NULL; + +-- +-- Name: $3; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_record + ADD CONSTRAINT "$3" FOREIGN KEY (format) REFERENCES rec_formatlookup(format_code) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: $6; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_record + ADD CONSTRAINT "$6" FOREIGN KEY (memberid_add) REFERENCES member(memberid) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: $7; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_record + ADD CONSTRAINT "$7" FOREIGN KEY (memberid_lastedit) REFERENCES member(memberid) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: auth_lookupid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth + ADD CONSTRAINT auth_lookupid_fkey FOREIGN KEY (lookupid) REFERENCES l_action(typeid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: auth_memberid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth + ADD CONSTRAINT auth_memberid_fkey FOREIGN KEY (memberid) REFERENCES member(memberid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: auth_officer_lookupid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth_officer + ADD CONSTRAINT auth_officer_lookupid_fkey FOREIGN KEY (lookupid) REFERENCES l_action(typeid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: auth_officer_officerid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth_officer + ADD CONSTRAINT auth_officer_officerid_fkey FOREIGN KEY (officerid) REFERENCES officer(officerid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: auth_subnet_typeid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth_subnet + ADD CONSTRAINT auth_subnet_typeid_fkey FOREIGN KEY (typeid) REFERENCES l_action(typeid); + + +-- +-- Name: auth_trainingstatus_typeid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth_trainingstatus + ADD CONSTRAINT auth_trainingstatus_typeid_fkey FOREIGN KEY (typeid) REFERENCES l_action(typeid); + + +-- +-- Name: auth_user_groups_group_id_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth_user_groups + ADD CONSTRAINT auth_user_groups_group_id_fkey FOREIGN KEY (group_id) REFERENCES auth_group(id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: baps_item_fileitemid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_item + ADD CONSTRAINT baps_item_fileitemid_fkey FOREIGN KEY (fileitemid) REFERENCES baps_fileitem(fileitemid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: baps_item_libraryitemid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_item + ADD CONSTRAINT baps_item_libraryitemid_fkey FOREIGN KEY (libraryitemid) REFERENCES baps_libraryitem(libraryitemid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: baps_item_listingid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_item + ADD CONSTRAINT baps_item_listingid_fkey FOREIGN KEY (listingid) REFERENCES baps_listing(listingid) ON DELETE CASCADE; + + +-- +-- Name: baps_item_textitemid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_item + ADD CONSTRAINT baps_item_textitemid_fkey FOREIGN KEY (textitemid) REFERENCES baps_textitem(textitemid) ON DELETE CASCADE; + + +-- +-- Name: baps_show_userid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY baps_show + ADD CONSTRAINT baps_show_userid_fkey FOREIGN KEY (userid) REFERENCES baps_user(userid) ON UPDATE RESTRICT ON DELETE CASCADE; + + +-- +-- Name: l_presenterstatus; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth_trainingstatus + ADD CONSTRAINT l_presenterstatus FOREIGN KEY (presenterstatusid) REFERENCES l_presenterstatus(presenterstatusid); + + +-- +-- Name: l_presenterstatus_can_award_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY l_presenterstatus + ADD CONSTRAINT l_presenterstatus_can_award_fkey FOREIGN KEY (can_award) REFERENCES l_presenterstatus(presenterstatusid); + + +-- +-- Name: l_presenterstatus_depends_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY l_presenterstatus + ADD CONSTRAINT l_presenterstatus_depends_fkey FOREIGN KEY (depends) REFERENCES l_presenterstatus(presenterstatusid); + + +-- +-- Name: mail_alias_list_listid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY mail_alias_list + ADD CONSTRAINT mail_alias_list_listid_fkey FOREIGN KEY (listid) REFERENCES mail_list(listid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: mail_alias_member_memberid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY mail_alias_member + ADD CONSTRAINT mail_alias_member_memberid_fkey FOREIGN KEY (memberid) REFERENCES member(memberid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: mail_alias_officer_officerid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY mail_alias_officer + ADD CONSTRAINT mail_alias_officer_officerid_fkey FOREIGN KEY (officerid) REFERENCES officer(officerid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: member_mail_listid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY mail_subscription + ADD CONSTRAINT member_mail_listid_fkey FOREIGN KEY (listid) REFERENCES mail_list(listid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: member_mail_memberid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY mail_subscription + ADD CONSTRAINT member_mail_memberid_fkey FOREIGN KEY (memberid) REFERENCES member(memberid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: member_news_feed_memberid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member_news_feed + ADD CONSTRAINT member_news_feed_memberid_fkey FOREIGN KEY (memberid) REFERENCES member(memberid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: member_news_feed_newsentryid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member_news_feed + ADD CONSTRAINT member_news_feed_newsentryid_fkey FOREIGN KEY (newsentryid) REFERENCES news_feed(newsentryid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: member_pass_memberid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member_pass + ADD CONSTRAINT member_pass_memberid_fkey FOREIGN KEY (memberid) REFERENCES member(memberid) ON UPDATE RESTRICT ON DELETE RESTRICT; + + +-- +-- Name: member_presenterstatus_presenterstatusid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member_presenterstatus + ADD CONSTRAINT member_presenterstatus_presenterstatusid_fkey FOREIGN KEY (presenterstatusid) REFERENCES l_presenterstatus(presenterstatusid) ON DELETE RESTRICT; + + +-- +-- Name: member_presenterstatus_revokedby_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member_presenterstatus + ADD CONSTRAINT member_presenterstatus_revokedby_fkey FOREIGN KEY (revokedby) REFERENCES member(memberid); + + +-- +-- Name: member_profile_photo_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member + ADD CONSTRAINT member_profile_photo_fkey FOREIGN KEY (profile_photo) REFERENCES myury.photos(photoid); + + +-- +-- Name: member_year_memberid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY member_year + ADD CONSTRAINT member_year_memberid_fkey FOREIGN KEY (memberid) REFERENCES member(memberid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: net_switchport_tags_portid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY net_switchport_tags + ADD CONSTRAINT net_switchport_tags_portid_fkey FOREIGN KEY (portid) REFERENCES net_switchport(portid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: net_switchport_tags_vlanid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY net_switchport_tags + ADD CONSTRAINT net_switchport_tags_vlanid_fkey FOREIGN KEY (vlanid) REFERENCES net_vlan(vlanid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: net_switchport_vlan_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY net_switchport + ADD CONSTRAINT net_switchport_vlan_fkey FOREIGN KEY (vlanid) REFERENCES net_vlan(vlanid) ON UPDATE CASCADE ON DELETE SET NULL; + + +-- +-- Name: news_feed_feedid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY news_feed + ADD CONSTRAINT news_feed_feedid_fkey FOREIGN KEY (feedid) REFERENCES l_newsfeed(feedid); + + +-- +-- Name: news_feed_memberid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY news_feed + ADD CONSTRAINT news_feed_memberid_fkey FOREIGN KEY (memberid) REFERENCES member(memberid); + + +-- +-- Name: nipsweb_migrate_memberid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY nipsweb_migrate + ADD CONSTRAINT nipsweb_migrate_memberid_fkey FOREIGN KEY (memberid) REFERENCES member(memberid); + + +-- +-- Name: rec_itunes_trackid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_itunes + ADD CONSTRAINT rec_itunes_trackid_fkey FOREIGN KEY (trackid) REFERENCES rec_track(trackid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: rec_track_digitisedby_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_track + ADD CONSTRAINT rec_track_digitisedby_fkey FOREIGN KEY (digitisedby) REFERENCES member(memberid) ON UPDATE CASCADE ON DELETE SET NULL; + + +-- +-- Name: rec_trackcorrection_reviewedby_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_trackcorrection + ADD CONSTRAINT rec_trackcorrection_reviewedby_fkey FOREIGN KEY (reviewedby) REFERENCES member(memberid); + + +-- +-- Name: rec_trackcorrection_trackid_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY rec_trackcorrection + ADD CONSTRAINT rec_trackcorrection_trackid_fkey FOREIGN KEY (trackid) REFERENCES rec_track(trackid); + + +-- +-- Name: selector_action_fkey; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY selector + ADD CONSTRAINT selector_action_fkey FOREIGN KEY (action) REFERENCES selector_actions(action) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: user_id_refs_id_831107f1; Type: FK CONSTRAINT; Schema: public +-- + +ALTER TABLE ONLY auth_user_groups + ADD CONSTRAINT user_id_refs_id_831107f1 FOREIGN KEY (user_id) REFERENCES auth_user(id) DEFERRABLE INITIALLY DEFERRED; + + +SET search_path = schedule, pg_catalog; + +-- +-- Name: block_range_rule_block_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY block_range_rule + ADD CONSTRAINT block_range_rule_block_id_fkey FOREIGN KEY (block_id) REFERENCES block(block_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: block_show_rule_show_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY block_show_rule + ADD CONSTRAINT block_show_rule_show_id_fkey FOREIGN KEY (show_id) REFERENCES show(show_id) ON DELETE CASCADE; + + +-- +-- Name: block_show_rules_block_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY block_show_rule + ADD CONSTRAINT block_show_rules_block_id_fkey FOREIGN KEY (block_id) REFERENCES block(block_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: season_metadata_approvedid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY season_metadata + ADD CONSTRAINT season_metadata_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: season_metadata_memberid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY season_metadata + ADD CONSTRAINT season_metadata_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: season_metadata_metadata_key_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY season_metadata + ADD CONSTRAINT season_metadata_metadata_key_id_fkey FOREIGN KEY (metadata_key_id) REFERENCES metadata.metadata_key(metadata_key_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: season_metadata_show_season_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY season_metadata + ADD CONSTRAINT season_metadata_show_season_id_fkey FOREIGN KEY (show_season_id) REFERENCES show_season(show_season_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: show_credit_approvedid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_credit + ADD CONSTRAINT show_credit_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) ON UPDATE RESTRICT ON DELETE CASCADE; + + +-- +-- Name: show_credit_credit_type_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_credit + ADD CONSTRAINT show_credit_credit_type_id_fkey FOREIGN KEY (credit_type_id) REFERENCES people.credit_type(credit_type_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: show_credit_creditid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_credit + ADD CONSTRAINT show_credit_creditid_fkey FOREIGN KEY (creditid) REFERENCES public.member(memberid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: show_credit_memberid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_credit + ADD CONSTRAINT show_credit_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: show_credit_show_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_credit + ADD CONSTRAINT show_credit_show_id_fkey FOREIGN KEY (show_id) REFERENCES show(show_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: show_genre_approvedid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_genre + ADD CONSTRAINT show_genre_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: show_genre_genre_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_genre + ADD CONSTRAINT show_genre_genre_id_fkey FOREIGN KEY (genre_id) REFERENCES genre(genre_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: show_genre_memberid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_genre + ADD CONSTRAINT show_genre_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: show_genre_show_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_genre + ADD CONSTRAINT show_genre_show_id_fkey FOREIGN KEY (show_id) REFERENCES show(show_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: show_image_metadata_approvedid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_image_metadata + ADD CONSTRAINT show_image_metadata_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: show_image_metadata_memberid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_image_metadata + ADD CONSTRAINT show_image_metadata_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: show_image_metadata_metadata_key_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_image_metadata + ADD CONSTRAINT show_image_metadata_metadata_key_id_fkey FOREIGN KEY (metadata_key_id) REFERENCES metadata.metadata_key(metadata_key_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: show_image_metadata_show_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_image_metadata + ADD CONSTRAINT show_image_metadata_show_id_fkey FOREIGN KEY (show_id) REFERENCES show(show_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: show_location_approvedid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_location + ADD CONSTRAINT show_location_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: show_location_location_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_location + ADD CONSTRAINT show_location_location_id_fkey FOREIGN KEY (location_id) REFERENCES location(location_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: show_location_memberid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_location + ADD CONSTRAINT show_location_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: show_location_show_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_location + ADD CONSTRAINT show_location_show_id_fkey FOREIGN KEY (show_id) REFERENCES show(show_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: show_memberid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show + ADD CONSTRAINT show_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) ON UPDATE RESTRICT ON DELETE CASCADE; + + +-- +-- Name: show_metadata_approvedid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_metadata + ADD CONSTRAINT show_metadata_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: show_metadata_memberid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_metadata + ADD CONSTRAINT show_metadata_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: show_metadata_metadata_key_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_metadata + ADD CONSTRAINT show_metadata_metadata_key_id_fkey FOREIGN KEY (metadata_key_id) REFERENCES metadata.metadata_key(metadata_key_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: show_metadata_show_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_metadata + ADD CONSTRAINT show_metadata_show_id_fkey FOREIGN KEY (show_id) REFERENCES show(show_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: show_podcast_link_podcast_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_podcast_link + ADD CONSTRAINT show_podcast_link_podcast_id_fkey FOREIGN KEY (podcast_id) REFERENCES uryplayer.podcast(podcast_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: show_podcast_link_show_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_podcast_link + ADD CONSTRAINT show_podcast_link_show_id_fkey FOREIGN KEY (show_id) REFERENCES show(show_id); + + +-- +-- Name: show_season_requested_time_show_season_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_season_requested_time + ADD CONSTRAINT show_season_requested_time_show_season_id_fkey FOREIGN KEY (show_season_id) REFERENCES show_season(show_season_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: show_season_requested_week_show_season_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_season_requested_week + ADD CONSTRAINT show_season_requested_week_show_season_id_fkey FOREIGN KEY (show_season_id) REFERENCES show_season(show_season_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: show_season_show_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_season + ADD CONSTRAINT show_season_show_id_fkey FOREIGN KEY (show_id) REFERENCES show(show_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: show_season_termid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_season + ADD CONSTRAINT show_season_termid_fkey FOREIGN KEY (termid) REFERENCES public.terms(termid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: show_season_timeslot_approvedid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_season_timeslot + ADD CONSTRAINT show_season_timeslot_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: show_season_timeslot_memberid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_season_timeslot + ADD CONSTRAINT show_season_timeslot_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: show_season_timeslot_show_season_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show_season_timeslot + ADD CONSTRAINT show_season_timeslot_show_season_id_fkey FOREIGN KEY (show_season_id) REFERENCES show_season(show_season_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: show_show_type_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY show + ADD CONSTRAINT show_show_type_id_fkey FOREIGN KEY (show_type_id) REFERENCES show_type(show_type_id) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: timeslot_metadata_approvedid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY timeslot_metadata + ADD CONSTRAINT timeslot_metadata_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: timeslot_metadata_memberid_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY timeslot_metadata + ADD CONSTRAINT timeslot_metadata_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) ON UPDATE CASCADE ON DELETE RESTRICT; + + +-- +-- Name: timeslot_metadata_metadata_key_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY timeslot_metadata + ADD CONSTRAINT timeslot_metadata_metadata_key_id_fkey FOREIGN KEY (metadata_key_id) REFERENCES metadata.metadata_key(metadata_key_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: timeslot_metadata_show_timeslot_id_fkey; Type: FK CONSTRAINT; Schema: schedule +-- + +ALTER TABLE ONLY timeslot_metadata + ADD CONSTRAINT timeslot_metadata_show_timeslot_id_fkey FOREIGN KEY (show_season_timeslot_id) REFERENCES show_season_timeslot(show_season_timeslot_id) ON UPDATE CASCADE ON DELETE CASCADE; + + +SET search_path = sis2, pg_catalog; + +-- +-- Name: member_options_memberid_fkey; Type: FK CONSTRAINT; Schema: sis2 +-- + +ALTER TABLE ONLY member_options + ADD CONSTRAINT member_options_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid); + + +-- +-- Name: member_signin_memberid_fkey; Type: FK CONSTRAINT; Schema: sis2 +-- + +ALTER TABLE ONLY member_signin + ADD CONSTRAINT member_signin_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) ON DELETE SET NULL; + + +-- +-- Name: member_signin_signerid_fkey; Type: FK CONSTRAINT; Schema: sis2 +-- + +ALTER TABLE ONLY member_signin + ADD CONSTRAINT member_signin_signerid_fkey FOREIGN KEY (signerid) REFERENCES public.member(memberid) ON DELETE SET NULL; + + +-- +-- Name: messages_commtypeid_fkey; Type: FK CONSTRAINT; Schema: sis2 +-- + +ALTER TABLE ONLY messages + ADD CONSTRAINT messages_commtypeid_fkey FOREIGN KEY (commtypeid) REFERENCES commtype(commtypeid) ON DELETE SET NULL; + + +-- +-- Name: messages_statusid_fkey; Type: FK CONSTRAINT; Schema: sis2 +-- + +ALTER TABLE ONLY messages + ADD CONSTRAINT messages_statusid_fkey FOREIGN KEY (statusid) REFERENCES statustype(statusid) ON DELETE SET NULL; + + +-- +-- Name: messages_timeslotid_fkey; Type: FK CONSTRAINT; Schema: sis2 +-- + +ALTER TABLE ONLY messages + ADD CONSTRAINT messages_timeslotid_fkey FOREIGN KEY (timeslotid) REFERENCES schedule.show_season_timeslot(show_season_timeslot_id) ON DELETE CASCADE; + + +SET search_path = tracklist, pg_catalog; + +-- +-- Name: bapsaudiologid; Type: FK CONSTRAINT; Schema: tracklist +-- + +ALTER TABLE ONLY tracklist + ADD CONSTRAINT bapsaudiologid FOREIGN KEY (bapsaudioid) REFERENCES public.baps_audiolog(audiologid) ON UPDATE CASCADE ON DELETE SET NULL; + + +-- +-- Name: selbaps_bapsloc_fkey; Type: FK CONSTRAINT; Schema: tracklist +-- + +ALTER TABLE ONLY selbaps + ADD CONSTRAINT selbaps_bapsloc_fkey FOREIGN KEY (bapsloc) REFERENCES public.baps_server(serverid); + + +-- +-- Name: selbaps_selaction_fkey; Type: FK CONSTRAINT; Schema: tracklist +-- + +ALTER TABLE ONLY selbaps + ADD CONSTRAINT selbaps_selaction_fkey FOREIGN KEY (selaction) REFERENCES public.selector_actions(action); + + +-- +-- Name: sourceid; Type: FK CONSTRAINT; Schema: tracklist +-- + +ALTER TABLE ONLY tracklist + ADD CONSTRAINT sourceid FOREIGN KEY (source) REFERENCES source(sourceid) ON UPDATE CASCADE; + + +-- +-- Name: stateid; Type: FK CONSTRAINT; Schema: tracklist +-- + +ALTER TABLE ONLY tracklist + ADD CONSTRAINT stateid FOREIGN KEY (state) REFERENCES state(stateid) ON UPDATE CASCADE ON DELETE SET NULL; + + +-- +-- Name: timeslotid; Type: FK CONSTRAINT; Schema: tracklist +-- + +ALTER TABLE ONLY tracklist + ADD CONSTRAINT timeslotid FOREIGN KEY (timeslotid) REFERENCES schedule.show_season_timeslot(show_season_timeslot_id) ON UPDATE CASCADE ON DELETE SET NULL; + + +-- +-- Name: track_notrec_audiologid_fkey; Type: FK CONSTRAINT; Schema: tracklist +-- + +ALTER TABLE ONLY track_notrec + ADD CONSTRAINT track_notrec_audiologid_fkey FOREIGN KEY (audiologid) REFERENCES tracklist(audiologid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: track_rec_audiologid_fkey; Type: FK CONSTRAINT; Schema: tracklist +-- + +ALTER TABLE ONLY track_rec + ADD CONSTRAINT track_rec_audiologid_fkey FOREIGN KEY (audiologid) REFERENCES tracklist(audiologid) ON UPDATE CASCADE ON DELETE CASCADE; + + +-- +-- Name: track_rec_trackid_fkey; Type: FK CONSTRAINT; Schema: tracklist +-- + +ALTER TABLE ONLY track_rec + ADD CONSTRAINT track_rec_trackid_fkey FOREIGN KEY (trackid) REFERENCES public.rec_track(trackid); + + +SET search_path = uryplayer, pg_catalog; + +-- +-- Name: package_id_refs_package_id_f71dbbff; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_package_entry + ADD CONSTRAINT package_id_refs_package_id_f71dbbff FOREIGN KEY (package_id) REFERENCES metadata.package(package_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_approvedid_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast + ADD CONSTRAINT podcast_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_credit_approvedid_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_credit + ADD CONSTRAINT podcast_credit_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_credit_credit_type_id_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_credit + ADD CONSTRAINT podcast_credit_credit_type_id_fkey FOREIGN KEY (credit_type_id) REFERENCES people.credit_type(credit_type_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_credit_creditid_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_credit + ADD CONSTRAINT podcast_credit_creditid_fkey FOREIGN KEY (creditid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_credit_memberid_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_credit + ADD CONSTRAINT podcast_credit_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_credit_podcast_id_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_credit + ADD CONSTRAINT podcast_credit_podcast_id_fkey FOREIGN KEY (podcast_id) REFERENCES podcast(podcast_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_image_metadata_approvedid_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_image_metadata + ADD CONSTRAINT podcast_image_metadata_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_image_metadata_memberid_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_image_metadata + ADD CONSTRAINT podcast_image_metadata_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_image_metadata_metadata_key_id_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_image_metadata + ADD CONSTRAINT podcast_image_metadata_metadata_key_id_fkey FOREIGN KEY (metadata_key_id) REFERENCES metadata.metadata_key(metadata_key_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_image_metadata_podcast_id_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_image_metadata + ADD CONSTRAINT podcast_image_metadata_podcast_id_fkey FOREIGN KEY (podcast_id) REFERENCES podcast(podcast_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_memberid_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast + ADD CONSTRAINT podcast_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_metadata_approvedid_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_metadata + ADD CONSTRAINT podcast_metadata_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_metadata_memberid_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_metadata + ADD CONSTRAINT podcast_metadata_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_metadata_metadata_key_id_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_metadata + ADD CONSTRAINT podcast_metadata_metadata_key_id_fkey FOREIGN KEY (metadata_key_id) REFERENCES metadata.metadata_key(metadata_key_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_metadata_podcast_id_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_metadata + ADD CONSTRAINT podcast_metadata_podcast_id_fkey FOREIGN KEY (podcast_id) REFERENCES podcast(podcast_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_package_entry_approvedid_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_package_entry + ADD CONSTRAINT podcast_package_entry_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_package_entry_memberid_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_package_entry + ADD CONSTRAINT podcast_package_entry_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: podcast_package_entry_podcast_id_fkey; Type: FK CONSTRAINT; Schema: uryplayer +-- + +ALTER TABLE ONLY podcast_package_entry + ADD CONSTRAINT podcast_package_entry_podcast_id_fkey FOREIGN KEY (podcast_id) REFERENCES podcast(podcast_id) DEFERRABLE INITIALLY DEFERRED; + + +SET search_path = webcam, pg_catalog; + +-- +-- Name: memberviews_memberid_fkey; Type: FK CONSTRAINT; Schema: webcam +-- + +ALTER TABLE ONLY memberviews + ADD CONSTRAINT memberviews_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) ON UPDATE CASCADE ON DELETE CASCADE; + + +SET search_path = website, pg_catalog; + +-- +-- Name: banner_banner_type_id_fkey; Type: FK CONSTRAINT; Schema: website +-- + +ALTER TABLE ONLY banner + ADD CONSTRAINT banner_banner_type_id_fkey FOREIGN KEY (banner_type_id) REFERENCES banner_type(banner_type_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: banner_campaign_approvedid_fkey; Type: FK CONSTRAINT; Schema: website +-- + +ALTER TABLE ONLY banner_campaign + ADD CONSTRAINT banner_campaign_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: banner_campaign_banner_id_fkey; Type: FK CONSTRAINT; Schema: website +-- + +ALTER TABLE ONLY banner_campaign + ADD CONSTRAINT banner_campaign_banner_id_fkey FOREIGN KEY (banner_id) REFERENCES banner(banner_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: banner_campaign_banner_location_id_fkey; Type: FK CONSTRAINT; Schema: website +-- + +ALTER TABLE ONLY banner_campaign + ADD CONSTRAINT banner_campaign_banner_location_id_fkey FOREIGN KEY (banner_location_id) REFERENCES banner_location(banner_location_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: banner_campaign_memberid_fkey; Type: FK CONSTRAINT; Schema: website +-- + +ALTER TABLE ONLY banner_campaign + ADD CONSTRAINT banner_campaign_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: banner_photo_id_fkey; Type: FK CONSTRAINT; Schema: website +-- + +ALTER TABLE ONLY banner + ADD CONSTRAINT banner_photo_id_fkey FOREIGN KEY (photoid) REFERENCES myury.photos(photoid); + + +-- +-- Name: banner_timeslot_approvedid_fkey; Type: FK CONSTRAINT; Schema: website +-- + +ALTER TABLE ONLY banner_timeslot + ADD CONSTRAINT banner_timeslot_approvedid_fkey FOREIGN KEY (approvedid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: banner_timeslot_banner_campaign_id_fkey; Type: FK CONSTRAINT; Schema: website +-- + +ALTER TABLE ONLY banner_timeslot + ADD CONSTRAINT banner_timeslot_banner_campaign_id_fkey FOREIGN KEY (banner_campaign_id) REFERENCES banner_campaign(banner_campaign_id) DEFERRABLE INITIALLY DEFERRED; + + +-- +-- Name: banner_timeslot_memberid_fkey; Type: FK CONSTRAINT; Schema: website +-- + +ALTER TABLE ONLY banner_timeslot + ADD CONSTRAINT banner_timeslot_memberid_fkey FOREIGN KEY (memberid) REFERENCES public.member(memberid) DEFERRABLE INITIALLY DEFERRED; + + +CREATE SCHEMA myradio; +SET search_path = myradio, pg_catalog; +CREATE TABLE schema ( + attr character varying NOT NULL, + value integer NOT NULL +); +INSERT INTO schema VALUES ('version', 0); +ALTER TABLE ONLY schema + ADD CONSTRAINT schema_pkey PRIMARY KEY (attr); + +SET search_path = public; +INSERT INTO public.l_college (descr) VALUES ('Unknown'); +INSERT INTO myury.services (name, enabled) VALUES ('MyRadio', true); +INSERT INTO l_status VALUES ('c', 'Current'); +INSERT INTO l_status VALUES ('h', 'Historic'); +INSERT INTO rec_statuslookup VALUES ('l', 'lost'); +INSERT INTO rec_statuslookup VALUES ('u', 'unknown'); +INSERT INTO rec_statuslookup VALUES ('o', 'OK'); +INSERT INTO rec_statuslookup VALUES ('d', 'digital only'); +INSERT INTO rec_medialookup VALUES ('c', 'CD'); +INSERT INTO rec_medialookup VALUES ('2', 'Vinyl 12"'); +INSERT INTO rec_medialookup VALUES ('7', 'Vinyl 7"'); +INSERT INTO rec_medialookup VALUES ('n', 'NIPSWeb MP3 Import'); +INSERT INTO rec_cleanlookup VALUES ('u', 'unknown'); +INSERT INTO rec_cleanlookup VALUES ('y', 'yes'); +INSERT INTO rec_cleanlookup VALUES ('n', 'NO!'); +INSERT INTO rec_formatlookup VALUES ('a', 'Album'); +INSERT INTO rec_formatlookup VALUES ('s', 'Single'); +INSERT INTO rec_genrelookup VALUES ('p', 'Pop'); +INSERT INTO rec_genrelookup VALUES ('r', 'Rock'); +INSERT INTO rec_genrelookup VALUES ('d', 'Dance'); +INSERT INTO rec_genrelookup VALUES ('c', 'Classical'); +INSERT INTO rec_genrelookup VALUES ('z', 'Production'); +INSERT INTO rec_genrelookup VALUES ('h', 'Rap / Hip-Hop'); +INSERT INTO rec_genrelookup VALUES ('o', 'Other'); + +-------------- +-- Set Credit Types +-------------- + +SET search_path = people, pg_catalog; + +-- +-- Data for Name: credit_type; Type: TABLE DATA; Schema: people +-- + +INSERT INTO credit_type VALUES (1, 'Presenter', 'Presenters', true); +INSERT INTO credit_type VALUES (2, 'Producer', 'Producers', false); +INSERT INTO credit_type VALUES (3, 'Voice Actor', 'Voice Actors', false); +INSERT INTO credit_type VALUES (4, 'Director', 'Directors', false); +INSERT INTO credit_type VALUES (5, 'Editor', 'Editors', false); +INSERT INTO credit_type VALUES (6, 'Trainer', 'Trainers', false); +INSERT INTO credit_type VALUES (7, 'Attendee', 'Attendees', false); +INSERT INTO credit_type VALUES (8, 'Reporter', 'Reporters', false); +INSERT INTO credit_type VALUES (9, 'Engineer', 'Engineers', false); + + +-- +-- Name: schedule.showcredittype_id_seq; Type: SEQUENCE SET; Schema: people +-- + +SELECT pg_catalog.setval('"schedule.showcredittype_id_seq"', 9, true); + +-------------- +-- Set Genres +-------------- +SET search_path = schedule, pg_catalog; + +-- +-- Data for Name: genre; Type: TABLE DATA; Schema: schedule +-- + +INSERT INTO genre VALUES (1, 'Anything Goes'); +INSERT INTO genre VALUES (2, 'Classical'); +INSERT INTO genre VALUES (3, 'Electronic'); +INSERT INTO genre VALUES (4, 'Experimental'); +INSERT INTO genre VALUES (5, 'Folk'); +INSERT INTO genre VALUES (6, 'Hip-Hop'); +INSERT INTO genre VALUES (7, 'International'); +INSERT INTO genre VALUES (8, 'Jazz'); +INSERT INTO genre VALUES (9, 'Novelty'); +INSERT INTO genre VALUES (10, 'Pop'); +INSERT INTO genre VALUES (11, 'Rock'); +INSERT INTO genre VALUES (12, 'Soul/R&B'); +INSERT INTO genre VALUES (13, 'Speech'); +INSERT INTO genre VALUES (14, 'Indie'); +INSERT INTO genre VALUES (15, 'Dance'); +INSERT INTO genre VALUES (16, 'Metal'); +INSERT INTO genre VALUES (17, 'Retro'); + + +-- +-- Name: genre_genre_id_seq; Type: SEQUENCE SET; Schema: schedule +-- + +SELECT pg_catalog.setval('genre_genre_id_seq', 17, true); + +INSERT INTO show_type VALUES (1, 'Show', true, true, '', true, false); +INSERT INTO show_type VALUES (2, 'Demo', false, true, '', false, false); +INSERT INTO show_type VALUES (3, 'Training Lecture', false, true, '', false, false); +INSERT INTO show_type VALUES (4, 'Meeting', false, true, '', false, false); +INSERT INTO show_type VALUES (7, 'Interview', false, true, '', false, false); +INSERT INTO show_type VALUES (6, 'Recording', false, true, '', false, false); +INSERT INTO show_type VALUES (8, 'Filler', true, false, 'Used by LASS to determine which show is the filler/jukebox/sustainer show. EXACTLY ONE SHOW AT ANY GIVEN TIME MUST BE OF THIS TYPE', false, true); +INSERT INTO show_type VALUES (9, 'Show Block', false, true, 'It''s for shows within a show - like the 40 Hour Show breaking down into little blocks.', false, false); + + +-- +-- Name: show_type_show_type_id_seq; Type: SEQUENCE SET; Schema: schedule; Owner: web +-- + +SELECT pg_catalog.setval('show_type_show_type_id_seq', 9, true); + +INSERT INTO location VALUES (1, 'Studio 1'); +SELECT pg_catalog.setval('location_location_id_seq', 2, true); + +SET search_path = metadata, pg_catalog; + +-- +-- Data for Name: metadata_key; Type: TABLE DATA; Schema: metadata +-- + +INSERT INTO metadata_key VALUES (5, 'guest', true, '', 300, NULL, false); +INSERT INTO metadata_key VALUES (3, 'ob_location', true, '', 300, NULL, false); +INSERT INTO metadata_key VALUES (6, 'image', false, '', 300, NULL, false); +INSERT INTO metadata_key VALUES (13, 'css-normal', true, 'The name of a CSS/HTML class that should be applied to representations of this item on the website. This class is intended for use in "normal" contexts, such as lists and detail pages, where any branding or styling should be moderated.', 300, NULL, false); +INSERT INTO metadata_key VALUES (14, 'css-emphasis', false, 'The name of a CSS/HTML class that should be applied to representations of this item on the website. This class is intended for use in "emphasised" contexts, such as on schedules or headers, where styling and branding should be prominent.', 300, NULL, false); +INSERT INTO metadata_key VALUES (7, 'singular', false, '(Applicable to group items only.) When applicable (the item defines a group of other items), the singular noun form of one of those items. For example, the ''singular'' of ''Station Management'' would be ''Station Manager''. See also ''title'' and ''plural''.', 300, NULL, false); +INSERT INTO metadata_key VALUES (8, 'plural', false, '(Applicable to group items only.) When applicable (the item defines a group of other items), the plural noun form of a subgroup of those items. For example, the ''plural'' of ''Station Management'' would be ''Station Managers''. See also ''title'' and ''singular''.', 300, NULL, false); +INSERT INTO metadata_key VALUES (9, 'title_image', false, 'When applicable and defined, title_image will be used instead of title when displaying the heading for this item. The expected dimensions of the image depend on the context.', 300, NULL, false); +INSERT INTO metadata_key VALUES (10, 'thumbnail_image', false, '(Image) When defined and applicable (for example when the item is a show or podcast or other listable), this image will appear as a thumbnail in media lists.', 300, NULL, false); +INSERT INTO metadata_key VALUES (11, 'player_image', false, '(Image) Image displayed on players (for podcasts this is jwplayer, for shows this is radioplayer). Dimensions depend on the item and which player it is to be shown on, but as a rule of thumb this is larger than thumbnail_image and square.', 300, NULL, false); +INSERT INTO metadata_key VALUES (12, 'internal_note', true, 'Metadata with this key will be saved with the item but not shown on the public site; this should be used to attach internal, private notes to items. For example, notes to the Programme Controller regarding show application detail should be tagged with this key.', 300, NULL, false); +INSERT INTO metadata_key VALUES (15, 'short_title', false, 'Like title, but shorter. Use this for abbreviated versions of long titles; on-site it''s used for website TITLE tags and suchlike.', 300, NULL, false); +INSERT INTO metadata_key VALUES (4, 'tag', true, '', 300, 'Tags', true); +INSERT INTO metadata_key VALUES (2, 'title', false, 'The publicly available title of the item. For items defining a group (for example, roles and credits) this is a singular, collective name (''Station Management'', ''Presentership''); see also the ''singular'' and ''plural'' keys.', 300, 'Titles', true); +INSERT INTO metadata_key VALUES (1, 'description', false, 'A human-readable, general public description of the item. Where this description is used depends on the item type, but this key usually defines the most detailed publicly available description text associated with the item.', 300, 'Descriptions', true); +INSERT INTO metadata_key VALUES (16, 'reject-reason', false, 'Reason for Season Application Rejection', 300, 'Reasons for Season Application Rejection', false); +INSERT INTO metadata_key VALUES (17, 'upload_state', false, 'When uploading data to services, this is a store of the upload state', 300, NULL, false); + + +-- +-- Name: metadata_key_metadata_key_id_seq; Type: SEQUENCE SET; Schema: metadata +-- + +SELECT pg_catalog.setval('metadata_key_metadata_key_id_seq', 17, true); + +CREATE TABLE music.explicit_checked ( + trackid integer NOT NULL +); + +ALTER TABLE ONLY music.explicit_checked ADD CONSTRAINT explicit_checked_pkey PRIMARY KEY (trackid); +ALTER TABLE music.explicit_checked ADD CONSTRAINT explicit_checked_fkey FOREIGN KEY (trackid) REFERENCES public.rec_track(trackid) ON DELETE CASCADE; + +SET search_path = myury, pg_catalog; + +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_Track', 'Track'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_Show', 'Show'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_Season', 'Season'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_Timeslot', 'Timeslot'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_Album', 'Album'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_Demo', 'Demo'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_List', 'List'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_Photo', 'Photo'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_Podcast', 'Podcast'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_Scheduler', 'Scheduler'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_TrackCorrection', 'TrackCorrection'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_TrainingStatus', 'Training'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_UserTrainingStatus', 'UserTraining'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_Selector', 'Selector'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_Alias', 'Alias'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_Officer', 'Officer'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_Team', 'Team'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_TracklistItem', 'TracklistItem'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_User', 'User'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_Swagger', 'resources'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\ServiceAPI\MyRadio_Artist', 'Artist'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\iTones\iTones_Playlist', 'Playlist'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\iTones\iTones_Utils', 'iTones'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\MyRadio\CoreUtils', 'Utils'); +INSERT INTO api_class_map (class_name, api_name) VALUES ('\MyRadio\MyRadio\AuthUtils', 'AuthUtils'); + +INSERT INTO api_method_auth (class_name, method_name, typeid) VALUES ('\MyRadio\ServiceAPI\MyRadio_Swagger', NULL, NULL); +INSERT INTO api_method_auth (class_name, method_name, typeid) VALUES ('\MyRadio\ServiceAPI\MyRadio_Timeslot', 'getWeekSchedule', NULL); + +SET search_path = tracklist, pg_catalog; +INSERT INTO tracklist.source (sourceid, source) VALUES ('b', 'BAPS'); +INSERT INTO tracklist.source (sourceid, source) VALUES ('m', 'Manual'); +INSERT INTO tracklist.source (sourceid, source) VALUES ('o', 'Other'); +INSERT INTO tracklist.source (sourceid, source) VALUES ('j', 'Jukebox'); + +SET search_path = public, pg_catalog; +CREATE TABLE myury.api_mixin_auth ( + api_mixin_auth_id SERIAL, + class_name CHARACTER VARYING NOT NULL, + mixin_name CHARACTER VARYING, + typeid INT REFERENCES l_action(typeid) +); + + +SET search_path = schedule, pg_catalog; +CREATE TABLE show_subtypes +( + show_subtype_id INTEGER NOT NULL PRIMARY KEY, + name text NOT NULL, + class text NOT NULL, + description text +); + +COMMENT ON TABLE show_subtypes IS 'The various subtypes of show (music, news etc.)'; +COMMENT ON COLUMN show_subtypes.name IS 'The publicly visible name of the subtype.'; +COMMENT ON COLUMN show_subtypes.class IS 'The CSS class of the subtype - similar to the name, but not intended for humans'; +COMMENT ON COLUMN show_subtypes.description IS 'A description of the shows that are in this subtype, for the subtype pages'; + +INSERT INTO show_subtypes (show_subtype_id, name, class) +VALUES (1, + 'Regular', + 'regular'), + (2, + 'Primetime', + 'primetime'), + (3, + 'Events', + 'event'), + (4, + 'News', + 'news'), + (5, + 'Speech', + 'speech'), + (6, + 'Music', + 'music'), + (7, + 'Alumni/Collaboration', + 'collab'); + +CREATE SEQUENCE show_subtype_id_seq + START WITH 8 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE show_subtype_id_seq OWNED BY show_subtypes.show_subtype_id; +ALTER TABLE ONLY show_subtypes ALTER COLUMN show_subtype_id SET DEFAULT nextval('show_subtype_id_seq'); + +CREATE TABLE show_season_subtype ( + show_season_subtype_id INTEGER NOT NULL PRIMARY KEY, + show_id INTEGER DEFAULT NULL, + season_id INTEGER DEFAULT NULL, + show_subtype_id INTEGER NOT NULL DEFAULT 1, + effective_from TIMESTAMP WITH TIME ZONE, + effective_to TIMESTAMP WITH TIME ZONE +); +ALTER TABLE show_season_subtype ADD CONSTRAINT chk_subtype_show_or_season_id CHECK (show_id IS NOT NULL OR season_id IS NOT NULL); +ALTER TABLE show_season_subtype ADD CONSTRAINT fk_show_subtype FOREIGN KEY (show_subtype_id) REFERENCES show_subtypes (show_subtype_id) ON DELETE SET DEFAULT; +ALTER TABLE show_season_subtype ADD CONSTRAINT fk_subtype_show FOREIGN KEY (show_id) REFERENCES show (show_id) ON DELETE CASCADE; +ALTER TABLE show_season_subtype ADD CONSTRAINT fk_subtype_season FOREIGN KEY (season_id) REFERENCES show_season (show_season_id) ON DELETE CASCADE; + +CREATE SEQUENCE show_season_subtype_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; +ALTER SEQUENCE show_season_subtype_id_seq OWNED BY show_season_subtype.show_season_subtype_id; +ALTER TABLE ONLY show_season_subtype ALTER COLUMN show_season_subtype_id SET DEFAULT nextval('show_season_subtype_id_seq'); + +INSERT INTO myury.api_method_auth (class_name, method_name, typeid) VALUES ('\MyRadio\ServiceAPI\MyRadio_ShowSubtype', 'getAll', NULL); diff --git a/schema/data-actions-min.json b/schema/data-actions-min.json new file mode 100644 index 000000000..00013143c --- /dev/null +++ b/schema/data-actions-min.json @@ -0,0 +1,16 @@ +[ + ["MyRadio", "StaticProxy"], + ["MyRadio", "default"], + ["MyRadio", "errorReport"], + ["MyRadio", "permissionAssignedTo"], + ["MyRadio", "config.js"], + ["MyRadio", "login"], + ["MyRadio", "logout"], + ["MyRadio", "pwChange"], + ["MyRadio", "pwReset"], + ["MyRadio", "chooseAuth"], + ["MyRadio", "addActionPermission"], + ["MyRadio", "actionPermissions"], + ["MyRadio", "listPermissions"], + ["MyRadio", "permissionUsage"] +] diff --git a/schema/data-actions.json b/schema/data-actions.json new file mode 100644 index 000000000..649daf6e5 --- /dev/null +++ b/schema/data-actions.json @@ -0,0 +1,176 @@ +[ + ["api", "default"], + ["Charts", "editQuote"], + ["Charts", "listChartReleases"], + ["Charts", "editChartRelease"], + ["Charts", "editChartType"], + ["Charts", "default"], + ["iTones", "editPlaylist"], + ["iTones", "default"], + ["iTones", "requestTrack"], + ["iTones", "configurePlaylist"], + ["iTones", "restorePlaylistRevision"], + ["iTones", "refreshLock"], + ["iTones", "listPlaylists"], + ["iTones", "allPlaylists"], + ["iTones", "viewPlaylistRevision"], + ["iTones", "viewPlaylistHistory"], + ["Library", "addTrack"], + ["Library", "search"], + ["Library", "rejectTrackCorrection"], + ["Library", "Search"], + ["Library", "viewTrackCorrection"], + ["Library", "deleteTrack"], + ["Library", "stats"], + ["Library", "editTrack"], + ["Library", "findDuplicate"], + ["Library", "findWrong"], + ["Library", "findMissing"], + ["Library", "gapFiller"], + ["Library", "default"], + ["Library", "acceptTrackCorrection"], + ["Logger", "hqfetch"], + ["Logger", "lqfetch"], + ["Mail", "search"], + ["Mail", "optin"], + ["Mail", "optout"], + ["Mail", "view"], + ["Mail", "default"], + ["Mail", "archive"], + ["Mail", "send"], + ["MyRadio", "a-endjoyride"], + ["MyRadio", "a-getuploadprogress"], + ["MyRadio", "StaticProxy"], + ["MyRadio", "a-findalbum"], + ["MyRadio", "permissionAssigned"], + ["MyRadio", "a-membernamefromid"], + ["MyRadio", "addNews"], + ["MyRadio", "default"], + ["MyRadio", "permissionAssignedTo"], + ["MyRadio", "config.js"], + ["MyRadio", "a-findartist"], + ["MyRadio", "a-findtrack"], + ["MyRadio", "login"], + ["MyRadio", "logout"], + ["MyRadio", "pwChange"], + ["MyRadio", "pwReset"], + ["MyRadio", "chooseAuth"], + ["MyRadio", "a-findmember"], + ["MyRadio", "timeslot"], + ["MyRadio", "a-readnews"], + ["MyRadio", "a-timeslotSignin"], + ["MyRadio", "addActionPermission"], + ["MyRadio", "removeActionPermission"], + ["MyRadio", "actionPermissions"], + ["MyRadio", "addPermission"], + ["MyRadio", "menu"], + ["MyRadio", "news"], + ["MyRadio", "impersonate"], + ["MyRadio", "listPermissions"], + ["MyRadio", "permissionUsage"], + ["MyRadio", "errorReport"], + ["MyRadio", "webstudio"], + ["MyRadio", "privacystatement"], + ["NIPSWeb", "default"], + ["NIPSWeb", "create_token"], + ["NIPSWeb", "secure_play"], + ["NIPSWeb", "managed_play"], + ["NIPSWeb", "load_aux_lib"], + ["NIPSWeb", "recv_ops"], + ["NIPSWeb", "manage_library"], + ["NIPSWeb", "upload_central"], + ["NIPSWeb", "confirm_central_upload"], + ["NIPSWeb", "load_auto_managed"], + ["NIPSWeb", "upload_aux"], + ["NIPSWeb", "confirm_aux_upload"], + ["NIPSWeb", "live"], + ["NIPSWeb", "import"], + ["NIPSWeb", "playout"], + ["Podcast", "setCover"], + ["Podcast", "editPodcast"], + ["Podcast", "allPodcast"], + ["Podcast", "default"], + ["Podcast", "suspendPodcast"], + ["Profile", "officers"], + ["Profile", "listOfficers"], + ["Profile", "markPaid"], + ["Profile", "timeline"], + ["Profile", "editOfficer"], + ["Profile", "View"], + ["Profile", "edit"], + ["Profile", "list"], + ["Profile", "listTrainers"], + ["Profile", "default"], + ["Profile", "addTrainingStatus"], + ["Profile", "lock"], + ["Profile", "view"], + ["Profile", "quickAdd"], + ["Profile", "bulkAdd"], + ["Quotes", "default"], + ["Quotes", "editQuote"], + ["Quotes", "addQuote"], + ["Resource", "default"], + ["Scheduler", "reject"], + ["Scheduler", "showPhoto"], + ["Scheduler", "cancelEpisode"], + ["Scheduler", "listTimeslots"], + ["Scheduler", "editShow"], + ["Scheduler", "listSeasons"], + ["Scheduler", "shows"], + ["Scheduler", "cancelSeason"], + ["Scheduler", "createSeason"], + ["Scheduler", "myShows"], + ["Scheduler", "editSeason"], + ["Scheduler", "a-findshowbytitle"], + ["Scheduler", "upgradeRefs"], + ["Scheduler", "stop"], + ["Scheduler", "upgrade"], + ["Scheduler", "createShow"], + ["Scheduler", "allocate"], + ["Scheduler", "attendance"], + ["Scheduler", "default"], + ["SIS", "remote"], + ["SIS", "messages.markread"], + ["SIS", "news.parse"], + ["SIS", "schedule.get"], + ["SIS", "tracklist.findTrack"], + ["SIS", "tracklist.checkTrack"], + ["SIS", "tracklist.delTrack"], + ["SIS", "stats.graph"], + ["SIS", "webcam.get"], + ["SIS", "help.hide"], + ["SIS", "webcam.set"], + ["SIS", "default"], + ["SIS", "selector.set"], + ["SIS", "selector.query"], + ["SIS", "news"], + ["Stats", "mostMessagedTimeslotYear"], + ["Stats", "mostListenedTimeslotYear"], + ["Stats", "mostListenedShowYear"], + ["Stats", "fullTrackist"], + ["Stats", "fullTracklist"], + ["Stats", "digitisation"], + ["Stats", "jukeboxPlayCounter"], + ["Stats", "mostMessagedShowYear"], + ["Stats", "trainingMap"], + ["Stats", "bapsPlayCounter"], + ["Training", "attendDemo"], + ["Training", "leaveDemo"], + ["Training", "listDemos"], + ["Training", "createDemo"], + ["Training", "finishDemo"], + ["Training", "listWaitingLists"], + ["Training", "joinList"], + ["Training", "leaveList"], + ["Webcam", "focus"], + ["Webcam", "archive"], + ["Webcam", "default"], + ["Website", "viewCampaigns"], + ["Website", "createBanner"], + ["Website", "default"], + ["Website", "banners"], + ["Website", "campaigns"], + ["Website", "editBanner"], + ["Website", "editCampaign"], + ["Website", "createCampaign"] +] \ No newline at end of file diff --git a/schema/data-actionsauth-min.json b/schema/data-actionsauth-min.json new file mode 100644 index 000000000..f88a691b0 --- /dev/null +++ b/schema/data-actionsauth-min.json @@ -0,0 +1,16 @@ +[ + ["MyRadio", "actionPermissions", "AUTH_ALTERPERMISSIONFLAGS"], + ["MyRadio", "addActionPermission", "AUTH_ALTERPERMISSIONFLAGS"], + ["MyRadio", "chooseAuth", null], + ["MyRadio", "config.js", "AUTH_NOLOGIN"], + ["MyRadio", "default", null], + ["MyRadio", "listPermissions", "AUTH_ALTERPERMISSIONFLAGS"], + ["MyRadio", "login", "AUTH_NOLOGIN"], + ["MyRadio", "logout", "AUTH_NOLOGIN"], + ["MyRadio", "permissionAssignedTo", "AUTH_ALTERPERMISSIONFLAGS"], + ["MyRadio", "permissionUsage", "AUTH_ALTERPERMISSIONFLAGS"], + ["MyRadio", "pwChange", "AUTH_NOLOGIN"], + ["MyRadio", "pwReset", "AUTH_NOLOGIN"], + ["MyRadio", "StaticProxy", null], + ["MyRadio", "errorReport", null] +] diff --git a/schema/data-actionsauth.json b/schema/data-actionsauth.json new file mode 100644 index 000000000..c39e14d67 --- /dev/null +++ b/schema/data-actionsauth.json @@ -0,0 +1,192 @@ +[ + ["api", "default", null], + ["Charts", null, "AUTH_EDIT_CHART"], + ["Deploy", "default", "AUTH_DEPLOY"], + ["Events", "a-getevents", null], + ["Events", "default", null], + ["Events", "editEvent", "AUTH_CREATEEVENT"], + ["Events", "viewEvent", null], + ["iTones", "default", "AUTH_JUKEBOX_MODIFYPLAYLISTS"], + ["iTones", "default", "AUTH_JUKEBOX_REQUESTTRACK"], + ["iTones", "editPlaylist", "AUTH_JUKEBOX_MODIFYPLAYLISTS"], + ["iTones", "editAvailability", "AUTH_JUKEBOX_MODIFYPLAYLISTS"], + ["iTones", "allPlaylists", "AUTH_JUKEBOX_MODIFYPLAYLISTS"], + ["iTones", "listPlaylists", "AUTH_JUKEBOX_MODIFYPLAYLISTS"], + ["iTones", "refreshLock", "AUTH_JUKEBOX_MODIFYPLAYLISTS"], + ["iTones", "configurePlaylist", "AUTH_JUKEBOX_MODIFYPLAYLISTS"], + ["iTones", "requestTrack", "AUTH_JUKEBOX_REQUESTTRACK"], + ["iTones", "viewPlaylistHistory", "AUTH_JUKEBOX_REVERTPLAYLIST"], + ["Library", "acceptTrackCorrection", "AUTH_EDITMUSIC"], + ["Library", "addTrack", "AUTH_UPLOADMUSIC"], + ["Library", "default", "AUTH_SEARCHCENTRALDB"], + ["Library", "editTrack", "AUTH_EDITMUSIC"], + ["Library", "findDuplicate", "AUTH_EDITMUSIC"], + ["Library", "findMissing", "AUTH_EDITMUSIC"], + ["Library", "findWrong", "AUTH_EDITMUSIC"], + ["Library", "gapFiller", "AUTH_EDITMUSIC"], + ["Library", "rejectTrackCorrection", "AUTH_EDITMUSIC"], + ["Library", "search", "AUTH_SEARCHCENTRALDB"], + ["Library", "stats", "AUTH_TRACKSTATISTICS"], + ["Library", "viewTrackCorrection", "AUTH_EDITMUSIC"], + ["Logger", "hqfetch", "AUTH_DOWNLOADHQ"], + ["Logger", "lqfetch", "AUTH_DOWNLOADLQ"], + ["Mail", "archive", null], + ["Mail", "default", null], + ["Mail", "default", null], + ["Mail", "optin", null], + ["Mail", "optout", null], + ["Mail", "send", null], + ["Mail", "view", null], + ["MyRadio", "actionPermissions", "AUTH_ALTERPERMISSIONFLAGS"], + ["MyRadio", "addActionPermission", "AUTH_ALTERPERMISSIONFLAGS"], + ["MyRadio", "removeActionPermission", "AUTH_ALTERPERMISSIONFLAGS"], + ["MyRadio", "addNews", "AUTH_EDITTECHNEWS"], + ["MyRadio", "addNews", "AUTH_PISS"], + ["MyRadio", "addNews", "AUTH_EDITMEMBERSNEWS"], + ["MyRadio", "a-endjoyride", null], + ["MyRadio", "a-findalbum", null], + ["MyRadio", "a-findartist", "AUTH_SEARCHCENTRALDB"], + ["MyRadio", "a-findartist", "AUTH_USENIPSWEB"], + ["MyRadio", "a-findmember", null], + ["MyRadio", "a-findtrack", "AUTH_SEARCHCENTRALDB"], + ["MyRadio", "a-findtrack", "AUTH_JUKEBOX_REQUESTTRACK"], + ["MyRadio", "a-findtrack", "AUTH_USENIPSWEB"], + ["MyRadio", "a-getuploadprogress", null], + ["MyRadio", "a-membernamefromid", null], + ["MyRadio", "a-readnews", null], + ["MyRadio", "a-timeslotSignin", null], + ["MyRadio", "chooseAuth", "AUTH_NOLOGIN"], + ["MyRadio", "config.js", "AUTH_NOLOGIN"], + ["MyRadio", "default", null], + ["MyRadio", "errorReport", null], + ["MyRadio", "impersonate", null], + ["MyRadio", "listPermissions", "AUTH_ALTERPERMISSIONFLAGS"], + ["MyRadio", "login", "AUTH_NOLOGIN"], + ["MyRadio", "logout", null], + ["MyRadio", "menu", "AUTH_MYURYMENU"], + ["MyRadio", "news", "AUTH_EDITMEMBERSNEWS"], + ["MyRadio", "news", "AUTH_PISS"], + ["MyRadio", "news", "AUTH_EDITTECHNEWS"], + ["MyRadio", "permissionAssignedTo", "AUTH_ALTERPERMISSIONFLAGS"], + ["MyRadio", "permissionUsage", "AUTH_ALTERPERMISSIONFLAGS"], + ["MyRadio", "addPermission", "AUTH_ALTERPERMISSIONFLAGS"], + ["MyRadio", "pwChange", "AUTH_NOLOGIN"], + ["MyRadio", "pwReset", "AUTH_NOLOGIN"], + ["MyRadio", "StaticProxy", null], + ["MyRadio", "timeslot", null], + ["MyRadio", "webstudio", "AUTH_ACCESS_WEBSTUDIO"], + ["MyRadio", "privacystatement", null], + ["NIPSWeb", "confirm_aux_upload", "AUTH_USENIPSWEB"], + ["NIPSWeb", "confirm_central_upload", "AUTH_USENIPSWEB"], + ["NIPSWeb", "create_token", "AUTH_USENIPSWEB"], + ["NIPSWeb", "default", "AUTH_USENIPSWEB"], + ["NIPSWeb", "live", "AUTH_USENWLIVE"], + ["NIPSWeb", "load_auto_managed", "AUTH_USENIPSWEB"], + ["NIPSWeb", "load_aux_lib", "AUTH_USENIPSWEB"], + ["NIPSWeb", "managed_play", null], + ["NIPSWeb", "manage_library", "AUTH_USENIPSWEB"], + ["NIPSWeb", "recv_ops", "AUTH_USENIPSWEB"], + ["NIPSWeb", "secure_play", null], + ["NIPSWeb", "upload_aux", "AUTH_USENIPSWEB"], + ["NIPSWeb", "upload_central", "AUTH_USENIPSWEB"], + ["NIPSWeb", "import", "AUTH_USENIPSWEB"], + ["NIPSWeb", "playout", "AUTH_USENIPSWEB"], + ["Podcast", "editPodcast", "AUTH_STANDALONEPODCAST"], + ["Podcast", "editPodcast", "AUTH_APPLYFORSHOW"], + ["Podcast", "suspendPodcast", "AUTH_APPLYFORSHOW"], + ["Podcast", "editPodcast", "AUTH_UPLOADPODCASTS"], + ["Podcast", "editPodcast", "AUTH_PODCASTANYSHOW"], + ["Podcast", "default", "AUTH_UPLOADPODCASTS"], + ["Podcast", "default", "AUTH_PODCASTANYSHOW"], + ["Podcast", "default", "AUTH_STANDALONEPODCAST"], + ["Podcast", "allPodcast", "AUTH_EDITANYPODCAST"], + ["Podcast", "setCover", "AUTH_UPLOADPODCASTS"], + ["Profile", "addTrainingStatus", null], + ["Profile", "bulkAdd", "AUTH_ADDMEMBER"], + ["Profile", "default", null], + ["Profile", "edit", null], + ["Profile", "editOfficer", "AUTH_CHANGEOFFICERSHIP"], + ["Profile", "list", "AUTH_LISTALLMEMBERS"], + ["Profile", "listOfficers", null], + ["Profile", "listTrainers", "AUTH_LISTALLMEMBERS"], + ["Profile", "markPaid", "AUTH_MARKPAYMENT"], + ["Profile", "officers", null], + ["Profile", "quickAdd", "AUTH_ADDMEMBER"], + ["Profile", "timeline", null], + ["Profile", "view", null], + ["Quotes", "addQuote", null], + ["Quotes", "addQuote", "AUTH_ADD_QUOTES"], + ["Quotes", "default", null], + ["Quotes", "editQuote", null], + ["Resource", "default", "AUTH_PERSONALPLAYLIST"], + ["Scheduler", "a-findshowbytitle", null], + ["Scheduler", "allocate", "AUTH_ALLOCATESLOTS"], + ["Scheduler", "autoViz", "AUTH_AUTOVIZ"], + ["Scheduler", "setAutoViz", "AUTH_AUTOVIZ"], + ["Scheduler", "autoVizClips", "AUTH_AUTOVIZ"], + ["Scheduler", "shows", "AUTH_VIEWMEMBERSHOWS"], + ["Scheduler", "attendance", "AUTH_VIEWSISLOGININFO"], + ["Scheduler", "tracking", "AUTH_VIEWSISLOGININFO"], + ["Scheduler", "cancelEpisode", "AUTH_APPLYFORSHOW"], + ["Scheduler", "cancelSeason", "AUTH_DELETESHOWS"], + ["Scheduler", "createSeason", "AUTH_APPLYFORSHOW"], + ["Scheduler", "createShow", "AUTH_APPLYFORSHOW"], + ["Scheduler", "default", "AUTH_ALLOCATESLOTS"], + ["Scheduler", "default", "AUTH_VIEWMEMBERSHOWS"], + ["Scheduler", "default", "AUTH_DELETESHOWS"], + ["Scheduler", "editSeason", "AUTH_APPLYFORSHOW"], + ["Scheduler", "editShow", "AUTH_APPLYFORSHOW"], + ["Scheduler", "editTerm", "AUTH_ALLOCATESLOTS"], + ["Scheduler", "listSeasons", "AUTH_VIEWMEMBERSHOWS"], + ["Scheduler", "listSeasons", "AUTH_APPLYFORSHOW"], + ["Scheduler", "listTerms", "AUTH_ALLOCATESLOTS"], + ["Scheduler", "listTimeslots", "AUTH_APPLYFORSHOW"], + ["Scheduler", "listTimeslots", "AUTH_VIEWMEMBERSHOWS"], + ["Scheduler", "myShows", "AUTH_APPLYFORSHOW"], + ["Scheduler", "reject", "AUTH_ALLOCATESLOTS"], + ["Scheduler", "showPhoto", null], + ["Scheduler", "stop", "AUTH_STOPBROADCAST"], + ["Scheduler", "upgrade", "AUTH_ALLOCATESLOTS"], + ["Scheduler", "upgradeRefs", "AUTH_ALLOCATESLOTS"], + ["SIS", "default", "AUTH_USESIS"], + ["SIS", "help.hide", "AUTH_USESIS"], + ["SIS", "messages.markread", "AUTH_USESIS"], + ["SIS", "news", "AUTH_USESIS"], + ["SIS", "news.parse", "AUTH_USESIS"], + ["SIS", "remote", "AUTH_USESIS"], + ["SIS", "schedule.get", "AUTH_USESIS"], + ["SIS", "selector.query", "AUTH_MODIFYSELECTOR"], + ["SIS", "selector.set", "AUTH_MODIFYSELECTOR"], + ["SIS", "stats.graph", "AUTH_USESIS"], + ["SIS", "tracklist.checkTrack", "AUTH_USESIS"], + ["SIS", "tracklist.delTrack", "AUTH_USESIS"], + ["SIS", "tracklist.findTrack", "AUTH_USESIS"], + ["SIS", "webcam.get", "AUTH_USESIS"], + ["SIS", "webcam.set", "AUTH_MODIFYWEBCAM"], + ["Stats", "bapsPlayCounter", "AUTH_TRACKSTATISTICS"], + ["Stats", "fullTracklist", "AUTH_TRACKSTATISTICS"], + ["Stats", "jukeboxPlayCounter", "AUTH_TRACKSTATISTICS"], + ["Stats", null, "AUTH_VIEWSTATS"], + ["Training", "attendDemo", "AUTH_ADDDEMOS"], + ["Training", "attendDemo", "AUTH_ATTENDDEMO"], + ["Training", "leaveDemo", "AUTH_ATTENDDEMO"], + ["Training", "createDemo", "AUTH_ADDDEMOS"], + ["Training", "finishDemo", "AUTH_ADDDEMOS"], + ["Training", "listWaitingLists", null], + ["Training", "joinList", null], + ["Training", "leaveList", null], + ["Training", "listDemos", "AUTH_ATTENDDEMO"], + ["Training", "listDemos", "AUTH_ADDDEMOS"], + ["Webcam", "archive", "AUTH_VIEWWEBCAMARCHIVE"], + ["Webcam", "default", "AUTH_VIEWEBCAM"], + ["Webcam", "focus", "AUTH_VIEWEBCAM"], + ["Website", "banners", "AUTH_EDIT_HOME_BANNERS"], + ["Website", "campaigns", "AUTH_EDIT_HOME_BANNERS"], + ["Website", "createBanner", "AUTH_EDIT_HOME_BANNERS"], + ["Website", "createCampaign", "AUTH_EDIT_HOME_BANNERS"], + ["Website", "default", "AUTH_EDIT_HOME_BANNERS"], + ["Website", "editBanner", "AUTH_EDIT_HOME_BANNERS"], + ["Website", "editCampaign", "AUTH_EDIT_HOME_BANNERS"], + ["Website", "editShortUrl", "AUTH_EDITSHORTURLS"], + ["Website", "shortUrls", "AUTH_EDITSHORTURLS"] +] \ No newline at end of file diff --git a/schema/data-apiauth.json b/schema/data-apiauth.json new file mode 100644 index 000000000..561249f28 --- /dev/null +++ b/schema/data-apiauth.json @@ -0,0 +1,20 @@ +[ + ["\\MyRadio\\ServiceAPI\\MyRadio_Timeslot", "get9DaySchedule", null], + ["\\MyRadio\\ServiceAPI\\MyRadio_Timeslot", "getCreditsNames", null], + ["\\MyRadio\\ServiceAPI\\MyRadio_Timeslot", "getCurrentAndNext", null], + ["\\MyRadio\\ServiceAPI\\MyRadio_Timeslot", "getWeekSchedule", null], + ["\\MyRadio\\ServiceAPI\\MyRadio_Timeslot", "getShowPlan", "AUTH_USENIPSWEB"], + ["\\MyRadio\\ServiceAPI\\MyRadio_Timeslot", "updateShowPlan", "AUTH_USENIPSWEB"], + ["\\MyRadio\\ServiceAPI\\MyRadio_Timeslot", "getPlayout", "AUTH_USENIPSWEB"], + ["\\MyRadio\\ServiceAPI\\MyRadio_Timeslot", "setPlayout", "AUTH_USENIPSWEB"], + ["\\MyRadio\\ServiceAPI\\MyRadio_User", "getShows", "AUTH_USENIPSWEB"], + ["\\MyRadio\\ServiceAPI\\MyRadio_Show", "getAllSeasons", "AUTH_USENIPSWEB"], + ["\\MyRadio\\ServiceAPI\\MyRadio_Season", "getAllTimeslots", "AUTH_USENIPSWEB"], + ["\\MyRadio\\iTones\\iTones_Playlist", "getTracks", "AUTH_USENIPSWEB"], + ["\\MyRadio\\iTones\\iTones_Playlist", "getTracks", "AUTH_SEARCHCENTRALDB"], + ["\\MyRadio\\ServiceAPI\\MyRadio_Track", "setIntro", "AUTH_USENIPSWEB"], + ["\\MyRadio\\ServiceAPI\\MyRadio_Track", "search", "AUTH_SEARCHCENTRALDB"], + ["\\MyRadio\\ServiceAPI\\MyRadio_Artist", "findByName", "AUTH_SEARCHCENTRALDB"], + ["\\MyRadio\\ServiceAPI\\MyRadio_Scheduler", "isTerm", "AUTH_APITERM"], + ["\\MyRadio\\ServiceAPI\\MyRadio_Webcam", "incrementViewCounter", "AUTH_VIEWEBCAM"] +] \ No newline at end of file diff --git a/schema/data-auth-min.json b/schema/data-auth-min.json new file mode 100644 index 000000000..cdb1935ce --- /dev/null +++ b/schema/data-auth-min.json @@ -0,0 +1,5 @@ +[ + ["Do not require login","AUTH_NOLOGIN"], + ["Edit Permission Definitions","AUTH_ALTERPERMISSIONFLAGS"], + ["Always show errors","AUTH_SHOWERRORS"] +] diff --git a/schema/data-auth.json b/schema/data-auth.json new file mode 100644 index 000000000..b8283fa04 --- /dev/null +++ b/schema/data-auth.json @@ -0,0 +1,91 @@ +[ + ["List All Members","AUTH_LISTALLMEMBERS", true], + ["Console Access: Computing","AUTH_COMPUTINGCONSOLE", true], + ["Edit Members News Feed","AUTH_EDITMEMBERSNEWS", true], + ["Edit Music Database","AUTH_EDITMUSIC", true], + ["Upload Music","AUTH_UPLOADMUSIC", true], + ["Edit Mailing Lists","AUTH_EDITMAILINGLISTS", true], + ["View SIS Login Information","AUTH_VIEWSISLOGININFO", true], + ["Administrate YUSU CMS","AUTH_YUSUADMIN", true], + ["List Old Members","AUTH_LISTOLDMEMBERS", true], + ["Lock Accounts","AUTH_LOCK", true], + ["View Any Profile","AUTH_VIEWOTHERMEMBERS", true], + ["Assign Officerships","AUTH_ASSIGNOFFICERSHIP", true], + ["Take Membership Payments","AUTH_MARKPAYMENT", true], + ["Add Members","AUTH_ADDMEMBER", true], + ["View Any Show","AUTH_VIEWMEMBERSHOWS", true], + ["Allocate Schedule Timeslots","AUTH_ALLOCATESLOTS", true], + ["Delete Shows","AUTH_DELETESHOWS", true], + ["Update Training Status","AUTH_TRAIN", true], + ["Upload Minutes","AUTH_UPLOADMINUTES", true], + ["Make Podcasts Standalone","AUTH_STANDALONEPODCAST", true], + ["Podcast For Any Show","AUTH_PODCASTANYSHOW", true], + ["Delete Wiki Pages","AUTH_WIKIADMIN", true], + ["Impersonate Other Users","AUTH_IMPERSONATE", true], + ["Edit the Chart","AUTH_EDIT_CHART", true], + ["Edit Recommended Listening","AUTH_EDIT_RECOMMENDED", true], + ["Edit Tech News Feed","AUTH_EDITTECHNEWS", true], + ["Block Impersonation","AUTH_BLOCKIMPERSONATE", true], + ["Request Jukebox Track","AUTH_JUKEBOX_REQUESTTRACK", true], + ["Revert Jukebox Playlists","AUTH_JUKEBOX_REVERTPLAYLIST", true], + ["Console Access: Jukebox","AUTH_JUKEBOX_CONSOLE", true], + ["Restart Jukebox Service","AUTH_JUKEBOX_RESTARTSVC", true], + ["View Jukebox Logs","AUTH_JUKEBOX_VIEWLOGS", true], + ["Impersonate Users with \"Block Impersonate\"","AUTH_IMPERSONATE_BLOCKED_USERS", true], + ["Revoke Training Status","AUTH_TRAIN_REVOKE", true], + ["Deploy New Services","AUTH_DEPLOY", true], + ["Edit Any Podcast","AUTH_EDITANYPODCAST", true], + ["Upload Podcasts","AUTH_UPLOADPODCASTS", true], + ["Download from HQ Logger","AUTH_DOWNLOADHQ", true], + ["Download from LQ Logger","AUTH_DOWNLOADLQ", true], + ["Manage Personal Playlists","AUTH_PERSONALPLAYLIST", true], + ["Access SIS","AUTH_USESIS", true], + ["Search Central Database","AUTH_SEARCHCENTRALDB", true], + ["View Webcam","AUTH_VIEWEBCAM", true], + ["View Track Statistics","AUTH_TRACKSTATISTICS", true], + ["Edit MyURY Menu","AUTH_MYURYMENU", true], + ["Always Show Errors","AUTH_SHOWERRORS", false], + ["Choose Version of a Web Service","AUTH_SELECTSERVICEVERSION", true], + ["Apply for a Show","AUTH_APPLYFORSHOW", true], + ["View Webcam Video Archives","AUTH_VIEWWEBCAMARCHIVE", true], + ["Add Demo Slots","AUTH_ADDDEMOS", true], + ["Attend a Demo","AUTH_ATTENDDEMO", true], + ["View URY Statistics","AUTH_VIEWSTATS", true], + ["[API Only] Unrestricted API Access","AUTH_APISUDO", false], + ["[API Only] Read Selector Status","AUTH_APIREADSEL", false], + ["[API Only] Check Scheduler Term Status","AUTH_APITERM", false], + ["[Deprecated] Change Any Password","AUTH_CHANGEANYPASSWORD", false], + ["Edit Any Profile","AUTH_EDITANYPROFILE", true], + ["Edit Any Show","AUTH_EDITSHOWS", true], + ["Edit Central Resource Lists","AUTH_EDITCENTRALRES", true], + ["Mail Hidden Mailing Lists","AUTH_MAILALLMEMBERS", true], + ["Edit Banners","AUTH_EDIT_HOME_BANNERS", true], + ["Edit Playlists","AUTH_JUKEBOX_MODIFYPLAYLISTS", true], + ["Edit Permission Definitions","AUTH_ALTERPERMISSIONFLAGS", true], + ["Edit Presenter Information Sheet","AUTH_PISS", true], + ["Edit Selector Source","AUTH_MODIFYSELECTOR", true], + ["Edit Webcam Output Source","AUTH_MODIFYWEBCAM", true], + ["Edit Member Permission Flags","AUTH_CHANGEMEMBERPERMISSIONS", true], + ["Edit Officer Permission Flags","AUTH_CHANGEOFFICERPERMISSIONS", true], + ["Edit Officership","AUTH_CHANGEOFFICERSHIP", true], + ["Edit Server Account Name","AUTH_CHANGESERVERACCOUNT", true], + ["Access Show Planner","AUTH_USENIPSWEB", true], + ["Access NIPSWeb Live","AUTH_USENWLIVE", true], + ["[API Only] View Aliases","AUTH_VIEWALIASES", false], + ["Access Restricted Passwords","AUTH_RESTRICTEDPASSWORDS", true], + ["Do not require login","AUTH_NOLOGIN", true], + ["[API Only] Test login credentials","AUTH_VALIDUSER", false], + ["Stop Broadcast","AUTH_STOPBROADCAST", true], + ["Add Quotes","AUTH_ADD_QUOTES", true], + ["Upload Music - Manual","AUTH_UPLOADMUSICMANUAL", true], + ["Tracklist own/current show","AUTH_TRACKLIST_OWN", true], + ["Tracklist for any show/timeslot","AUTH_TRACKLIST_ALL", true], + ["Access WebStudio", "AUTH_ACCESS_WEBSTUDIO", true], + ["View Messages for Any Show", "AUTH_ANY_SHOW_MESSAGES", true], + ["Give Anyone Any Training Status", "AUTH_AWARDANYTRAINING", true], + ["Create Events", "AUTH_CREATEEVENT", true], + ["Edit Any Event", "AUTH_EDITANYEVENT", true], + ["Edit Short URLs", "AUTH_EDITSHORTURLS", true], + ["Use Automatic Visualisation", "AUTH_AUTOVIZ", true], + ["Cancel Any Training Session", "AUTH_CANCELANYDEMO", true] +] diff --git a/schema/data-officers.json b/schema/data-officers.json new file mode 100644 index 000000000..2460933a1 --- /dev/null +++ b/schema/data-officers.json @@ -0,0 +1,87 @@ +{ + "teams": [ + ["Station Management", null, "management", 10, + [ + ["Station Manager", null, "station.manager", 1, "h"], + ["Assistant Station Manager", null, "assistant.station.manager", 2, "a"], + ["Secretary", null, "secretary", 3, "o"], + ["Treasurer", null, "treasurer", 20, "o"] + ] + ], + ["Presenting Team", "The Training and Programming Teams were merged into a Presenting team in November 2013.", "presenting", 15, + [ + ["Programme Controller", null, "programme.controller", 10, "h"], + ["Training Coordinator", null, "training.coordinator", 15, "a"] + ] + ], + ["Production Team", null, "production", 20, + [ + ["Head of Production", null, "head.of.production", 10, "h"], + ["Assistant Head of Production", null, "assistant.head.of.production", 20, "o"] + ] + ], + ["News Team", null, "news", 25, + [ + ["Head of News and Sport", null, "head.of.news", 10, "h"], + ["News Editor", null, "news.editor", 15, "a"], + ["Sports Editor", null, "sports", 20, "o"], + ["Reporter (News and Sport)", null, "reporter", 25, "o"] + ] + ], + ["Speech Team", null, "speech", 27, + [ + ["Head of Speech", null, "head.of.speech", 10, "h"], + ["Assistant Head of Speech", null, "assistant.head.of.speech", 15, "a"], + ["Cinema and Theatre Liason", null, "Cinema and Theatre Liason", 20, "o"], + ["Documentaries and Features Editor", null, "documentaries.editor", 25, "o"], + ["Speech Officer", null, "speech.officer", 30, "o"] + ] + ], + ["Marketing Team", "Marketing Team replaced the Business Team in November 2013.", "marketing", 30, + [ + ["Head of Marketing", null, "head.of.marketing", 10, "h"], + ["Assistant Head of Marketing", null, "assistant.head.of.marketing", 15, "a"] + ] + ], + ["Music Team", null, "music", 35, + [ + ["Head of Music", null, "head.of.music", 10, "h"], + ["Assistant Head of Music", null, "assistant.head.of.music", 15, "a"], + ["Sessions Manager", null, "sessions.manager", 20, "o"], + ["Interviewer", null, "interviewer", 20, "o"], + ["Librarian", null, "librarian", 30, "o"], + ["Chart Supremo", null, "chart.supremo", 35, "o"] + ] + ], + ["Engineering Team", null, "engineering", 40, + [ + ["Chief Engineer", null, "chief.engineer", 10, "h"], + ["Assistant Chief Engineer", null, "assistant.chief.engineer", 15, "a"], + ["Engineering Officer", null, "engineering.officer", 20, "o"], + ["Engineer", null, null, 100, "o"] + ] + ], + ["Computing Team", null, "computing", 50, + [ + ["Head of Computing", null, "head.of.computing", 10, "h"], + ["Assistant Head of Computing", null, "assistant.head.of.computing", 15, "a"], + ["Computing Officer", null, "computing.officer", 20, "o"], + ["Webmaster", null, "webmaster", 25, "o"], + ["Computing Member", null, null, 100, "o"] + ] + ], + ["Events Team", "Events Team", "events", 99, + [ + ["Head of Events", "Events Manager", "head.of.events", 10, "h"], + ["Assistant Head of Events", null, "assistant.head.of.events", 15, "a"] + ] + ], + ["Other Officers", "Officers Without Portfolio", null, 200, + [ + ["Social Secretary", null, "social.secretary", 10, "o"], + ["Officer Without Portfolio", null, "floater", 20, "o"], + ["Digital Content Manager", null, "digital.content.manager", 25, "o"] + ] + ] + ] +} diff --git a/schema/patches/1.sql b/schema/patches/1.sql new file mode 100644 index 000000000..c25b1d148 --- /dev/null +++ b/schema/patches/1.sql @@ -0,0 +1,32 @@ +BEGIN; + +CREATE TABLE jukebox.playlist_categories ( + id SERIAL PRIMARY KEY, + name TEXT, + description TEXT +); + +-- Split playlists into 2 categories: General (deny) and Jukebox (allow) +INSERT INTO jukebox.playlist_categories (id, name, description) +VALUES ( + 1, + 'General', + '

This category is for all playlists that don''t fit the others. They will not be played by Jukebox or Campus Playout.

' +), ( + 2, + 'Jukebox', + '

This category is for all playlists that should be played by Jukebox.

' +); + +-- Start sequence at 3 as ^ just defined the first 2 items +ALTER SEQUENCE jukebox.playlist_categories_id_seq + START 3; + +-- Force playlists to have a category, defaulting to General (deny) +ALTER TABLE jukebox.playlists +ADD COLUMN category INTEGER DEFAULT 1; + +ALTER TABLE jukebox.playlists +ADD CONSTRAINT fk_playlist_category FOREIGN KEY(category) REFERENCES jukebox.playlist_categories(id); + +COMMIT; diff --git a/schema/patches/1.sql.old b/schema/patches/1.sql.old new file mode 100644 index 000000000..781c1bbb7 --- /dev/null +++ b/schema/patches/1.sql.old @@ -0,0 +1,22 @@ +-- Change all instances of 'myury' to 'myradio' +ALTER TABLE myury.act_permission SET SCHEMA myradio; +ALTER TABLE myury.actions SET SCHEMA myradio; +ALTER TABLE myury.api_class_map SET SCHEMA myradio; +ALTER TABLE myury.api_key SET SCHEMA myradio; +ALTER TABLE myury.api_key_auth SET SCHEMA myradio; +ALTER TABLE myury.api_method_auth SET SCHEMA myradio; +ALTER TABLE myury.award_categories SET SCHEMA myradio; +ALTER TABLE myury.award_member SET SCHEMA myradio; +ALTER TABLE myury.error_rate SET SCHEMA myradio; +ALTER TABLE myury.menu_columns SET SCHEMA myradio; +ALTER TABLE myury.menu_links SET SCHEMA myradio; +ALTER TABLE myury.menu_module SET SCHEMA myradio; +ALTER TABLE myury.menu_sections SET SCHEMA myradio; +ALTER TABLE myury.menu_twigitems SET SCHEMA myradio; +ALTER TABLE myury.modules SET SCHEMA myradio; +ALTER TABLE myury.password_reset_token SET SCHEMA myradio; +ALTER TABLE myury.photos SET SCHEMA myradio; +ALTER TABLE myury.services SET SCHEMA myradio; +DROP TABLE myury.services_versions; +DROP TABLE myury.services_versions_member; +DROP SCHEMA myury; diff --git a/schema/patches/10.sql b/schema/patches/10.sql new file mode 100644 index 000000000..eb80dc6d5 --- /dev/null +++ b/schema/patches/10.sql @@ -0,0 +1,19 @@ +BEGIN; + +-- Create table to list short URLs e.g. "ury.org.uk/ern2021" +CREATE TABLE public.short_urls ( + short_url_id SERIAL PRIMARY KEY, + slug TEXT NOT NULL, + redirect_to TEXT NOT NULL +); + +-- Allow short URLs to collect analytics +CREATE TABLE public.short_url_clicks ( + click_id BIGSERIAL PRIMARY KEY, + short_url_id INTEGER REFERENCES public.short_urls (short_url_id) ON DELETE CASCADE, + click_time TIMESTAMPTZ NOT NULL, + user_agent TEXT DEFAULT NULL, + ip_address INET DEFAULT NULL +); + +COMMIT; diff --git a/schema/patches/11.sql b/schema/patches/11.sql new file mode 100644 index 000000000..351b94427 --- /dev/null +++ b/schema/patches/11.sql @@ -0,0 +1,3 @@ +-- Allow Jukebox playlists to be archived (true/false) +ALTER TABLE jukebox.playlists + ADD archived BOOL DEFAULT FALSE NOT NULL; diff --git a/schema/patches/12.sql b/schema/patches/12.sql new file mode 100644 index 000000000..a12595685 --- /dev/null +++ b/schema/patches/12.sql @@ -0,0 +1,9 @@ +BEGIN; + +ALTER TABLE schedule.demo + ADD COLUMN signup_cutoff_hours INT DEFAULT 0; + +ALTER TABLE schedule.demo + ADD COLUMN max_participants INT DEFAULT 2; + +COMMIT; \ No newline at end of file diff --git a/schema/patches/13.sql b/schema/patches/13.sql new file mode 100644 index 000000000..1cad8d7da --- /dev/null +++ b/schema/patches/13.sql @@ -0,0 +1,9 @@ +BEGIN; +CREATE TABLE schedule.autoviz_configuration ( + autoviz_config_id SERIAL PRIMARY KEY, + show_season_timeslot_id INTEGER UNIQUE REFERENCES schedule.show_season_timeslot(show_season_timeslot_id) ON DELETE CASCADE, + record BOOLEAN DEFAULT 'f', + stream_url TEXT NULL DEFAULT NULL, + stream_key TEXT NULL DEFAULT NULL +); +COMMIT; diff --git a/schema/patches/14.sql b/schema/patches/14.sql new file mode 100644 index 000000000..2f28f888b --- /dev/null +++ b/schema/patches/14.sql @@ -0,0 +1,11 @@ +BEGIN; +CREATE TABLE public.calendar_tokens ( + tokenid SERIAL PRIMARY KEY, + memberid INTEGER NOT NULL REFERENCES public.member(memberid), + token_str TEXT NOT NULL UNIQUE, + revoked BOOLEAN DEFAULT 'f' +); +UPDATE myradio.schema +SET value = 14 +WHERE attr='version'; +COMMIT; diff --git a/schema/patches/15.sql b/schema/patches/15.sql new file mode 100644 index 000000000..ad36ae445 --- /dev/null +++ b/schema/patches/15.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE public.l_presenterstatus + ADD COLUMN archived BOOLEAN DEFAULT 'f'; + +COMMIT; diff --git a/schema/patches/16.sql b/schema/patches/16.sql new file mode 100644 index 000000000..9235da49f --- /dev/null +++ b/schema/patches/16.sql @@ -0,0 +1,6 @@ +BEGIN; + +ALTER TABLE public.terms + ADD COLUMN weeks INT DEFAULT 10; + +COMMIT; \ No newline at end of file diff --git a/schema/patches/17.sql b/schema/patches/17.sql new file mode 100644 index 000000000..b7fc035f2 --- /dev/null +++ b/schema/patches/17.sql @@ -0,0 +1,2 @@ +ALTER TABLE IF EXISTS public.terms + ADD COLUMN week_names text; diff --git a/schema/patches/18.sql b/schema/patches/18.sql new file mode 100644 index 000000000..2ef38d4ee --- /dev/null +++ b/schema/patches/18.sql @@ -0,0 +1,10 @@ +CREATE TYPE deletion AS ENUM ('default', 'informed', 'optout', 'deleted'); + +ALTER TABLE public.member + ADD gdpr_accepted boolean default(false); + +ALTER TABLE Public.member +ADD data_removal deletion DEFAULT('default'); + +ALTER TABLE Public.member +ADD hide_profile boolean DEFAULT(false); diff --git a/schema/patches/2.sql b/schema/patches/2.sql new file mode 100644 index 000000000..232378359 --- /dev/null +++ b/schema/patches/2.sql @@ -0,0 +1,11 @@ +BEGIN; + +-- Give the option for podcasts to be explicit (true or false). +-- This is used when generating RSS feeds for iTunes. +ALTER TABLE schedule.show + ADD COLUMN podcast_explicit BOOLEAN DEFAULT 'f'; + +COMMENT ON COLUMN schedule.show.podcast_explicit IS +'If this show is a podcast, whether it contains explicit content.'; + +COMMIT; diff --git a/schema/patches/3.sql b/schema/patches/3.sql new file mode 100644 index 000000000..47d0c5cdf --- /dev/null +++ b/schema/patches/3.sql @@ -0,0 +1,68 @@ +BEGIN; + +-- Remake the 'analytics' table to add more advanced functionality +DROP TABLE IF EXISTS myradio.analytics; +DROP FUNCTION IF EXISTS myradio.create_analytics_record(TEXT, TEXT, INTEGER, VARCHAR); + +CREATE TABLE myradio.analytics ( + analytics_record_id BIGSERIAL PRIMARY KEY, + page TEXT, + ref TEXT DEFAULT NULL, + member_officerships integer[], + member_shows_bucketed integer, + session_id VARCHAR(32), + time timestamptz +); + +-- For a given page, take a given memberid and find its officerships and shows +-- Useful for finding analytics of a page, based on attributes of its viewers +-- Inserts this into the "myradio.analytics" table +-- Note: Read 'create_analytics_record.memberid' as the function's 'memberid' argument +-- It's not reading that value from a table - it's more like a local variable +CREATE FUNCTION myradio.create_analytics_record( + page TEXT, + ref TEXT, + memberid INTEGER, + session_id VARCHAR(32) +) +RETURNS VOID +AS $$ + DECLARE + officerships INTEGER[]; + num_shows INTEGER; + BEGIN + -- Find the officerships this member currently has + SELECT array_agg(officerid) INTO officerships + FROM public.member_officer + WHERE member_officer.memberid = create_analytics_record.memberid + AND from_date <= NOW() + AND (till_date IS NULL OR till_date >= (NOW() + '28 days'::INTERVAL)); + + -- Find the number of shows this member has + SELECT COUNT(*) INTO num_shows + FROM schedule.show + WHERE show.memberid = create_analytics_record.memberid + OR show_id IN ( + SELECT show_id FROM schedule.show_credit + WHERE creditid = create_analytics_record.memberid AND + (effective_to >= NOW() OR effective_to IS NULL) + ); + + -- Right, we've got what we need so dump output into analytics table + -- Preserve reference data (like session_id, current datetime) for comparison + -- Note that no identifiable details are preserved (hence why num_shows is rounded) + INSERT INTO myradio.analytics + (page, ref, member_officerships, member_shows_bucketed, session_id, time) + VALUES ( + create_analytics_record.page, + create_analytics_record.ref, + officerships, + CEIL(num_shows::decimal / 5), + create_analytics_record.session_id, + NOW() + ); + END; + $$ +LANGUAGE plpgsql; + +COMMIT; diff --git a/schema/patches/4.sql b/schema/patches/4.sql new file mode 100644 index 000000000..9ec46f5b2 --- /dev/null +++ b/schema/patches/4.sql @@ -0,0 +1,14 @@ +BEGIN; + +-- Mailing lists used to be show/hide. Now you can define their listed order +-- To hide, set the "ordering" to a negative number. +ALTER TABLE public.mail_list + ADD COLUMN ordering INTEGER DEFAULT 0; + +COMMENT ON COLUMN public.mail_list.ordering + IS 'The order in which to display these lists in MyRadio. Ascending, equal values sorted by ID. Set to negative to hide from MyRadio.'; + +ALTER TABLE public.mail_list + DROP COLUMN IF EXISTS current; + +COMMIT; diff --git a/schema/patches/5.sql b/schema/patches/5.sql new file mode 100644 index 000000000..2dba7af82 --- /dev/null +++ b/schema/patches/5.sql @@ -0,0 +1,15 @@ +BEGIN; + +-- Create a table for calendar events, with possible parent/child relation +CREATE TABLE public.events ( + eventid SERIAL PRIMARY KEY, + title TEXT, + description_html TEXT, + start_time TIMESTAMPTZ, + end_time TIMESTAMPTZ, + hostid INTEGER REFERENCES member (memberid), + rrule TEXT DEFAULT '', + master_id INTEGER REFERENCES events (eventid) NULL DEFAULT NULL +); + +COMMIT; diff --git a/schema/patches/6.sql b/schema/patches/6.sql new file mode 100644 index 000000000..50f8bd443 --- /dev/null +++ b/schema/patches/6.sql @@ -0,0 +1,44 @@ +BEGIN; + +-- Create table for scheduling training sessions +CREATE TABLE schedule.demo +( + demo_id SERIAL + CONSTRAINT demo_pk + PRIMARY KEY, + presenterstatusid INT NOT NULL + CONSTRAINT demo_l_presenterstatus_presenterstatusid_fk + REFERENCES l_presenterstatus, + demo_time TIMESTAMP WITH time zone NOT NULL, + demo_link TEXT, + memberid INT + CONSTRAINT demo_member_memberid_fk + REFERENCES member +); + +-- Allow arbitrary training attendees +CREATE TABLE schedule.demo_attendee +( + demo_id INT NOT NULL + CONSTRAINT demo_attendee_demo_demo_id_fk + REFERENCES schedule.demo, + memberid INT NOT NULL + CONSTRAINT demo_attendee_member_memberid_fk + REFERENCES member +); + +-- Allow people to express interest in getting trained +CREATE TABLE schedule.demo_waiting_list +( + memberid INT + CONSTRAINT demo_waiting_list_member_memberid_fk + REFERENCES member (memberid), + presenterstatusid INT + CONSTRAINT demo_waiting_list_l_presenterstatus_presenterstatusid_fk + REFERENCES l_presenterstatus (presenterstatusid), + date_added TIMESTAMP WITH time zone +); + + + +COMMIT; diff --git a/schema/patches/7.sql b/schema/patches/7.sql new file mode 100644 index 000000000..1b7788823 --- /dev/null +++ b/schema/patches/7.sql @@ -0,0 +1,17 @@ +BEGIN; + +-- Force users to declare where a show is, and who is attending +-- This was required for shows during Covid19 +ALTER TABLE sis2.member_signin + ADD COLUMN location INTEGER REFERENCES schedule.location (location_id); + +CREATE TABLE sis2.guest_signin ( + guest_signin_id SERIAL PRIMARY KEY, + signerid INTEGER REFERENCES public.member (memberid), + show_season_timeslot_id INTEGER REFERENCES schedule.show_season_timeslot (show_season_timeslot_id), + location INTEGER REFERENCES schedule.location (location_id), + sign_time TIMESTAMP DEFAULT now(), -- would like to use TIMESTAMPTZ but member_signin doesn't, consistency :( + guest_info TEXT +); + +COMMIT; diff --git a/schema/patches/8.sql b/schema/patches/8.sql new file mode 100644 index 000000000..38430f81f --- /dev/null +++ b/schema/patches/8.sql @@ -0,0 +1,4 @@ +-- Is a show set to play automatically? (true/false) +-- Basis for the "autoplayout" functionality +ALTER TABLE schedule.show_season_timeslot + ADD playout bool DEFAULT FALSE; diff --git a/schema/patches/9.sql b/schema/patches/9.sql new file mode 100644 index 000000000..638b01f26 --- /dev/null +++ b/schema/patches/9.sql @@ -0,0 +1,18 @@ +-- Link show tracklists to selector sources +-- Moved from base.sql into a separate patch +-- TODO Explain this better +CREATE TABLE selsources ( + selaction integer NOT NULL, + sourceid character(1) NOT NULL +); +COMMENT ON TABLE selsources IS 'Marries selector actions with tracklist sources'; +-- +-- Name: selsources_sourceid_fkey; Type: FK CONSTRAINT; Schema: tracklist +-- +ALTER TABLE ONLY selsources +ADD CONSTRAINT selsources_sourceid_fkey FOREIGN KEY (sourceid) REFERENCES tracklist.source(sourceid); +-- +-- Name: selsources_selaction_fkey; Type: FK CONSTRAINT; Schema: tracklist +-- +ALTER TABLE ONLY selsources +ADD CONSTRAINT selsources_selaction_fkey FOREIGN KEY (selaction) REFERENCES public.selector_actions(action); \ No newline at end of file diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh new file mode 100755 index 000000000..d12fa5c78 --- /dev/null +++ b/scripts/bootstrap.sh @@ -0,0 +1,103 @@ +#!/usr/bin/env sh + +# Bootstrapping script just for vagrant + +set -eux + +# Prevents this script being run outside of vagrant +if [ ! -d /vagrant ]; then + echo "This script should only ever be run on a vagrant virtual machine" + echo "Seriously, don't run this anywhere other than vagrant, it will ruin your day"; + exit 1; +fi + +# Base packages and Apache setup +apt-get update +apt-get install -y apache2 \ + libapache2-mod-php \ + php-common \ + postgresql-12 \ + postgresql-client-12 \ + memcached \ + php-curl \ + php-geoip \ + php-gd \ + php-ldap \ + php-pgsql \ + php-dev \ + php-memcached \ + php-xdebug \ + php-mbstring \ + php-xsl \ + openssl \ + ffmpeg \ + zip \ + unzip \ + composer +a2enmod ssl +a2enmod rewrite +service apache2 stop + +cat <> /etc/php/7.4/mods-available/xdebug.ini +xdebug.default_enable=1 +xdebug.remote_enable=1 +xdebug.remote_autostart=0 +xdebug.remote_port=9000 +xdebug.remote_log="/var/log/xdebug/xdebug.log" +xdebug.remote_host=10.0.2.2 +xdebug.idekey="MyRadio vagrant" +xdebug.remote_handler=dbgp +EOF + +su -c "adduser www-data vagrant" + +# Composer +cd /vagrant +mkdir -p /vagrant/src/vendor +su vagrant -c 'composer --no-progress update' + +# Link convenient folders to /vagrant inside the VM +ln -sf /vagrant/src /var/www/myradio/src +ln -sf /vagrant/sample_configs/apache.conf /etc/apache2/sites-available/myradio.conf +a2ensite myradio +a2dissite 000-default + +# Generate a random SSL cert +export PASSPHRASE=$(head -c 500 /dev/urandom | tr -dc a-z0-9A-Z | head -c 128; echo) +subj=" +C=UK +ST=Bar +O=MyRadio +localityName=RadioTown +commonName=myradio.local +organizationalUnitName=MyRadio +emailAddress=someone@example.com +" +openssl req -newkey rsa:2048 -nodes -subj "$(echo -n "$subj" | tr "\n" "/")" -keyout /etc/apache2/myradio.key -x509 -days 365 -out /etc/apache2/myradio.crt \ +-addext extendedKeyUsage=serverAuth -addext subjectAltName=DNS:localhost + +# Start httpd back up +update-rc.d apache2 defaults +service apache2 start + +# Create DB cluster/database/user +pg_dropcluster 12 main --stop || true # Seriously, don't use this anywhere other than vagrant +if ! `pg_lsclusters | grep -q myradio`; then pg_createcluster 12 myradio -p 5432; fi +systemctl start postgresql@12-myradio +su - postgres -c "cat /vagrant/sample_configs/postgres.sql | psql" + +rm -f /vagrant/src/MyRadio_Config.local.php # Remove any existing config + +# Create folders to store audio uploads +# Don't put spaces in folder names +music_dirs="records membersmusic beds jingles podcasts" +for i in ${music_dirs}; do + mkdir -p /music/$i + chown www-data:www-data /music/$i +done + +# And logs +mkdir -p /var/log/myradio +chown www-data:www-data /var/log/myradio + +echo "MyRadio is now installed in your Vagrant VM. Go to https://localhost:4443/myradio/ :)" diff --git a/scripts/gdprdeleteall.php b/scripts/gdprdeleteall.php new file mode 100644 index 000000000..b0234d430 --- /dev/null +++ b/scripts/gdprdeleteall.php @@ -0,0 +1,49 @@ +#!/usr/local/bin/php -q +query( + 'UPDATE public.member + SET data_removal=\'default\' + WHERE data_removal=\'informed\' and last_login >= $1 ', + [$date] +); + +$db->query( + 'UPDATE public.member + SET college=10, phone=DEFAULT, receive_email=false, endofcourse=DEFAULT, wheelchair=DEFAULT, data_removal=\'deleted\' + WHERE data_removal=\'informed\'', + [] +); +?> \ No newline at end of file diff --git a/scripts/gdprdeleteuser.php b/scripts/gdprdeleteuser.php new file mode 100644 index 000000000..00ac3618d --- /dev/null +++ b/scripts/gdprdeleteuser.php @@ -0,0 +1,122 @@ +#!/usr/local/bin/php -q +query( + 'INSERT INTO public.member( + memberid, fname, sname, college, receive_email, data_removal) + VALUES ($1, \'deleted\', \'user\', 10, false, \'deleted\')', + [$deletedUserId] + ); +} catch (exception $e) { + echo 'deleting user\n'; +} + +$db->query( + 'UPDATE schedule.show_credit SET memberid=$1 WHERE memberid=$2', + [$deletedUserId,$userid] +); + +$db->query( + 'UPDATE uryplayer.podcast_credit SET creditid=$1 WHERE creditid=$2', + [$deletedUserId,$userid] +); + +$db->query( + 'UPDATE bapsplanner.managed_items SET memberid=$1 WHERE memberid=$2', + [$deletedUserId,$userid] +); + +$db->query( + 'UPDATE schedule.timeslot_metadata SET memberid=$1 WHERE memberid=$2', + [$deletedUserId,$userid] +); + +$db->query( + 'UPDATE public.mail_alias_member SET memberid=$1 WHERE memberid=$2', + [$deletedUserId,$userid] +); + +$db->query( + 'UPDATE public.member_year SET memberid=$1 WHERE memberid=$2', + [$deletedUserId,$userid] +); + +$db->query( + 'UPDATE public.member_presenterstatus SET memberid=$1 WHERE memberid=$2', + [$deletedUserId,$userid] +); + +$db->query( + 'UPDATE public.member_pass SET memberid=$1 WHERE memberid=$2', + [$deletedUserId,$userid] +); + +$db->query( + 'UPDATE uryplayer.podcast_metadata SET memberid=$1 WHERE memberid=$2', + [$deletedUserId,$userid] +); + +$db->query( + 'UPDATE mail.email_recipient_member SET memberid=$1 WHERE memberid=$2', + [$deletedUserId,$userid] +); + +$db->query( + 'UPDATE uryplayer.podcast SET memberid=$1 WHERE memberid=$2', + [$deletedUserId,$userid] +); + +$db->query( + 'UPDATE public.mail_subscription SET memberid=$1 WHERE memberid=$2', + [$deletedUserId,$userid] +); + +$db->query( + 'UPDATE mail.alias_member SET destination=$1 WHERE destination=$2', + [$deletedUserId,$userid] +); + +$db->query( + 'UPDATE public.member + SET college=10, phone=DEFAULT, email=DEFAULT, receive_email=false, local_name=DEFAULT, local_alias=DEFAULT, account_locked=true, last_login=DEFAULT, endofcourse=DEFAULT, eduroam=DEFAULT, usesmtppassword=DEFAULT, joined=DEFAULT, require_password_change=DEFAUlT, profile_photo=DEFAULT, bio=DEFAULT, auth_provider=DEFAULT, contract_signed=DEFAULT, gdpr_accepted=DEFAULT, wheelchair=DEFAULT, data_removal=\'deleted\' + WHERE memberid=$1', + [$deletedUserId,$userid] +); +?> \ No newline at end of file diff --git a/scripts/gdpremail.php b/scripts/gdpremail.php new file mode 100644 index 000000000..637098840 --- /dev/null +++ b/scripts/gdpremail.php @@ -0,0 +1,81 @@ +#!/usr/local/bin/php -q +You are getting this email because you have not logged into MyRadio in over a year

+

In one week all of your personally identifiable data that is not required for webstudio or our public facing websites to function as advertised will be deleted.

+

If you wish to avoid this you can opt out of deletion by logging into your myradio account or by contact the ury computing team.

+

If you are happy for your personal data to be deleted feel free to ignore this eamil.

+--
+The URY Computing team
+
+University Radio York 1350AM 88.3FM
+---------------------------------------------
+
head.of.computing@ury.org.uk
+---------------------------------------------
+On Air | Online | On Tap
+ury.org.uk +EOT; + +$time = strtotime("-1 year", time()); +$date = date("Y-m-d", $time); + +echo "This script will Email all users that have not logged in for over a year\n Are you sure you want to continue? (y/n)"; +$cmdinput = trim(fgets(STDIN)); +if($cmdinput != 'Y'){ + return; +} +echo "Emailing users\n"; + +$memebersToEmail = $db->fetchAll( + 'SELECT memberid, last_login + FROM public.member WHERE last_login <= $1 and joined <= $1', + [$date] +); + +$db->query( + 'UPDATE public.member + SET data_removal=\'informed\' + WHERE data_removal=\'default\' and last_login <= $1 and joined <= $1', + [$date] +); + +$db->query( + 'UPDATE public.member + SET data_removal=\'informed\' + WHERE data_removal=\'default\' and last_login IS NULL', + [] +); + +foreach($memebersToEmail as $member){ + MyRadioEmail::sendEmailToUser( + $member["memberid"], + 'MyRadio account deletion', + $warning_email + ); +} +?> \ No newline at end of file diff --git a/scripts/migrate-show-colours.php b/scripts/migrate-show-colours.php new file mode 100644 index 000000000..798b401fd --- /dev/null +++ b/scripts/migrate-show-colours.php @@ -0,0 +1,35 @@ +query('BEGIN'); + for ($i = $startOfBatch; $i < $startOfBatch + $batchSize; $i++) { + $show = $shows[$i]; + $subtype = CoreUtils::getSubtypeForShow($show->getMeta('title')); + Database::getInstance()->query('INSERT INTO schedule.show_season_subtype + (show_id, show_subtype_id, effective_from) + (SELECT $1, (SELECT show_subtype_id + FROM schedule.show_subtypes WHERE schedule.show_subtypes.class = $2 + ), NOW())', [$show->getID(), $subtype]); + } + Database::getInstance()->query('COMMIT'); + echo "Processed."; +} + +echo "Done."; + diff --git a/scripts/reset-db-v2.sh b/scripts/reset-db-v2.sh new file mode 100755 index 000000000..176f1e1a3 --- /dev/null +++ b/scripts/reset-db-v2.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env sh +# Resets the main database - do not run this anywhere important +# For safety, this defaults to running on the 'myradio_test' database +# To run on the 'myradio' db, give an arbitrary argument e.g. './###.sh 1' +# This is useful for testing the myradio setup stages + +[[ -z $1 ]] && db="myradio_test" || db="myradio" + +# Deletes the given database and recreates it with the 'myradio' user +dropdb --if-exists $db; +createdb -O myradio $db diff --git a/scripts/reset-db.sh b/scripts/reset-db.sh new file mode 100755 index 000000000..a979714e3 --- /dev/null +++ b/scripts/reset-db.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env sh + +# Resets the test database to an initial state, for travis and local testing + +set -eux +export PGUSER=myradio +export PGPASSWORD=myradio +export PGHOST=127.0.0.1 + +# Reset database +dropdb --if-exists myradio_test; + +# (Re)init database +createdb -O myradio myradio_test + +psql myradio_test < `dirname $0`/../schema/base.sql > /dev/null +# Disabled until patch files are implemented (there's a 1.sql which renames schema myury to myradio) +#psql myradio_test < schema/patches/*.sql +psql myradio_test < `dirname $0`/../sample_configs/travis-auth.sql + +sed -e '/Config::$db_name/s/myradio/myradio_test/' `dirname $0`/../sample_configs/travis-config.php > `dirname $0`/../src/MyRadio_Config.local.php diff --git a/src/Classes/APCProvider.php b/src/Classes/APCProvider.php deleted file mode 100644 index a8f37dc04..000000000 --- a/src/Classes/APCProvider.php +++ /dev/null @@ -1,118 +0,0 @@ - - * @version 20130709 - * @package MyRadio_Core - */ -class APCProvider implements CacheProvider { - /** - * A variable to store the singleton instance - * @var APCProvider storage for the only APCProvider instance - */ - private static $me; - - /** - * Stores whether caching should be used. If not, it does not do anything on function calls - * @var boolean - */ - private $enable; - - /** - * Constructs the Unique instance of the CacheProvider for use. Private so that instances cannot be used in ways - * other than those intended - * @param boolean $enable Whether caching is actually enabled in this request. Default true - * @throws MyRadioException Will throw a MyRadioException if the APC extension is not loaded - */ - private function __construct($enable = true) { - $this->enable = $enable; - if ($enable && !function_exists('apc_store')) { - //Functions not available. If this is caught upstream, just disable - throw new MyRadioException('Cache is enabled but selected CacheProvider does not have required prerequisites (Is APC Extension installed and loaded?)'); - $this->enable = false; - } - } - - /** - * Stores an object in the APC User Object Cache - * - * @param String $key The unique name of the object to store. Ideally, this would use myury_{module}_{name} - * @param mixed $value The data to store - * @param int $expires The number of seconds this cache entry is valid for. Default is to last forever (0) - * @return boolean Whether the operation was successful (returns false if caching disabled) - * @assert ('myradio_core_test', 'test value', 0) == true - */ - public function set($key, $value, $expires = 0) { - if (!$this->enable) return false; - return apc_store($this->getKeyPrefix().$key, $value, $expires); - } - - /** - * Reads a previously stored value from the APC User Object Cache and returns it - * - * @param String $key The unique name of the object to fetch - * @return mixed The value of the store, or false on failure - * @assert ('myradio_core_test') == 'test value' - */ - public function get($key) { - if (!$this->enable) return false; - return apc_fetch($this->getKeyPrefix().$key); - } - - /** - * Deletes a previously stored value from the APC User Object Cache - * - * @param String $key The unique name of the object to delete - * @return boolean Returns whether the operaion was a success - * @assert ('myradio_core_test') == true - */ - public function delete($key) { - if (!$this->enable) return false; - return apc_delete($this->getKeyPrefix().$key); - } - - /** - * This will completely wipe the APC User Object Cache - * - */ - public function purge() { - if (!$this->enable) return false; - apc_clear_cache('user'); - return true; - } - - /** - * Returns the Singleton instance of this class, creating it if necessary - * - * @return APCProvider - */ - public static function getInstance() { - if (!self::$me) { - self::$me = new self(Config::$cache_enable); - } - return self::$me; - } - - /** - * Prevent copies being unintentionally made - * @throws MyRadioException - */ - public function __clone() { - throw new MyRadioException('Attempted to clone a singleton'); - } - - public function getKeyPrefix() { - return 'MyRadioCache-'; - } -} - diff --git a/src/Classes/Autoloader.php b/src/Classes/Autoloader.php new file mode 100644 index 000000000..8d304f9ce --- /dev/null +++ b/src/Classes/Autoloader.php @@ -0,0 +1,189 @@ +register(); + * + * // register the base directories for the namespace prefix + * $loader->addNamespace('Foo\Bar', '/path/to/packages/foo-bar/src'); + * $loader->addNamespace('Foo\Bar', '/path/to/packages/foo-bar/tests'); + * + * The following line would cause the autoloader to attempt to load the + * \Foo\Bar\Qux\Quux class from /path/to/packages/foo-bar/src/Qux/Quux.php: + * + * prefixes[$prefix]) === false) { + $this->prefixes[$prefix] = array(); + } + + // retain the base directory for the namespace prefix + if ($prepend) { + array_unshift($this->prefixes[$prefix], $base_dir); + } else { + array_push($this->prefixes[$prefix], $base_dir); + } + } + + /** + * Loads the class file for a given class name. + * + * @param string $class The fully-qualified class name. + * + * @return mixed The mapped file name on success, or boolean false on + * failure. + */ + public function loadClass($class) + { + // the current namespace prefix + $prefix = $class; + + // work backwards through the namespace names of the fully-qualified + // class name to find a mapped file name + while (($pos = strrpos($prefix, '\\')) !== false) { + // retain the trailing namespace separator in the prefix + $prefix = substr($class, 0, $pos + 1); + + // the rest is the relative class name + $relative_class = substr($class, $pos + 1); + + // try to load a mapped file for the prefix and relative class + $mapped_file = $this->loadMappedFile($prefix, $relative_class); + if ($mapped_file) { + return $mapped_file; + } + + // remove the trailing namespace separator for the next iteration + // of strrpos() + $prefix = rtrim($prefix, '\\'); + } + + // never found a mapped file + return false; + } + + /** + * Load the mapped file for a namespace prefix and relative class. + * + * @param string $prefix The namespace prefix. + * @param string $relative_class The relative class name. + * + * @return mixed Boolean false if no mapped file can be loaded, or the + * name of the mapped file that was loaded. + */ + protected function loadMappedFile($prefix, $relative_class) + { + // are there any base directories for this namespace prefix? + if (isset($this->prefixes[$prefix]) === false) { + return false; + } + + // look through base directories for this namespace prefix + foreach ($this->prefixes[$prefix] as $base_dir) { + // replace the namespace prefix with the base directory, + // replace namespace separators with directory separators + // in the relative class name, append with .php + $file = $base_dir + .str_replace('\\', '/', $relative_class) + .'.php'; + + // if the mapped file exists, require it + if ($this->requireFile($file)) { + // yes, we're done + return $file; + } + } + + // never found it + return false; + } + + /** + * If a file exists, require it from the file system. + * + * @param string $file The file to require. + * + * @return bool True if the file exists, false if not. + */ + protected function requireFile($file) + { + if (file_exists($file)) { + require_once $file; + + return true; + } + + return false; + } +} diff --git a/src/Classes/BRA/BRA_Utils.php b/src/Classes/BRA/BRA_Utils.php deleted file mode 100644 index c8d32ab44..000000000 --- a/src/Classes/BRA/BRA_Utils.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @package MyRadio_BRA - */ -class BRA_Utils extends ServiceAPI { - - public static function getInstance($id = 0) { - return new self(); - } - - public function __construct() {} - - public function getAllChannelInfo() { - return json_decode(file_get_contents(Config::$bra_uri.'/channels'),true); - } - -} diff --git a/src/Classes/Config.php b/src/Classes/Config.php index 85952cb79..0d311bc02 100644 --- a/src/Classes/Config.php +++ b/src/Classes/Config.php @@ -1,618 +1,805 @@ - * @version 20130711 - * @package MyRadio_Core + * This file provides the Config class for MyRadio. */ -final class Config { - /** - * The ID of the "MyRadio" Service. This *should* be the only Service, and therefore never change. - * It's technically a remnant of the originally slightly overengineered modularisation structure. - * @var int - */ - public static $service_id = 1; - /** - * The Module to assume if one is not explicitly provided. This is usually the MyRadio module, but if you're feeling - * special, you can make Stats or Library the default... - * @var String - */ - public static $default_module = 'MyRadio'; - /** - * The Action to assume if one is not explicitly provided. This is usually default, but if you're feeling special, - * you could make it index or home, but it'd break a lot of stuff. - * @var String - */ - public static $default_action = 'default'; - /** - * The hostname of the PostgreSQL database server - * @var String - */ - public static $db_hostname = 'localhost'; - public static $db_name = 'membership'; - /** - * The username to use connecting to the PostgreSQL database server - * @var String - */ - public static $db_user = 'root'; - /** - * The password to use connecting to the PostgreSQL database server - * @var String - */ - public static $db_pass = 'password'; - /** - * The public timezone of the installation - * @todo Full review and ensure UTC is used where appropriate (i.e. Scheduler) - * @var String - */ - public static $timezone = 'Europe/London'; - - /** - * The base URL of the MyRadio installation - * @var String - */ - public static $base_url = '//ury.org.uk/myury/'; - - /** - * The base URL of Shibbobleh - it has CSS and JS resources used by MyRadio - * @var String - */ - public static $shib_url = '//ury.org.uk/portal/'; - - /** - * Whether nice URL rewrites are enabled - * If true, then urls will be myury/[module]/[action] - * If false, then urls will be myury/?module=[module]&action=[action] - * @var boolean - */ - public static $rewrite_url = true; - - /** - * Whether to enable the Caching system - * Development value: false - * Production value: true - * @var boolean - */ - public static $cache_enable = true; - /** - * The name of the class that will provide the MyRadio Caching mechanism. - * Must be the name of a valid class located in classes/[classname].php and implements CacheProvider - * @var String - */ - public static $cache_provider = 'APCProvider'; - /** - * How long MyRadio_Track items should be cached before the data is invalidated. This is Configurable as due to a lot - * of external edit sources, it is reasonable to asssume the cache may become stale due to other systems. - * @var int - */ - public static $cache_track_timeout = 7200; //2 hours - - /** - * Whether MyRadio errors should be displayed in the browser. If this is set to false, users with the - * AUTH_SHOWERRORS permission will still see errors. - * Development value: true - * Production value: false - * @var boolean - */ - public static $display_errors = false; - - /** - * Whether MyRadio Exceptions should be emailed to Computing - * @var boolean - */ - public static $email_exceptions = true; - - /** - * Prevent an exception surge by failing if this many are thrown. - * @var int - */ - public static $exception_limit = 10; - - /** - * Whether template debugging should be enabled - * Development value: true - * Production value: false - * @var boolean - */ - public static $template_debug = true; - - /** - * The default number of results from an AJAX Search Query - * This can be overriden on a per-request basis - * @var int - */ - public static $ajax_limit_default = 25; - - /** - * The photoid to use for a Joined URY Timeline Event - * @var int - */ - public static $photo_joined = 1; - /** - * The photoid to use for a Gained Officership URY Timeline Event - * @var int - */ - public static $photo_officership_get = 2; - /** - * The photoid to use for a Lost Officership URY Timeline Event - * @var int - */ - public static $photo_officership_down = 3; - /** - * The photoid to use for a Got Show URY Timeline Event - * @var int - */ - public static $photo_show_get = 4; - /** - * The photoid to use for a Got Award URY Timeline Event - * @var int - */ - public static $photo_award_get = 5; - - /** - * The id of the news feed to use for news events - * @var int - */ - public static $news_feed = 1; - - /** - * The id of the news feed to use for presenter infomation - * @var int - */ - public static $piss_feed = 4; - - /** - * The location of the Memcached server used for the Website. - * This is so it can be cleared where necessary. - * @var String - */ - public static $django_cache_server = 'localhost'; - - /** - * The path to the motion Webcam logs. This must be a file path, but may be NFS/Samba mounter - * @var String - */ - public static $webcam_archive_path = '/home/motion/videos'; - - /** - * The url of the webcam server status - * @var String - */ - public static $webcam_current_url = "http://copperbox.york.ac.uk:9090/current?noprotocol=true"; - - /** - * The url of the webcam server setter - * @var String - */ - public static $webcam_set_url = "http://copperbox.york.ac.uk:9090/set?newcam="; - - /** - * The path to store the original, unencoded copies of URYPlayer Podcasts. - * The originals are archived here for future reencoding. - * @var String - */ - public static $podcast_archive_path = '/music/podcasts'; - - /** - * The URL where media should be stored. Used for podcasts, banners and images. - * @var String - */ - public static $public_media_path = '/home/django/virtualenvs/urysite/assets/media'; - /** - * This is the HTTP-accessible version of the above directory. Should be absolute or relative to domain, but not - * protocol-specific, e.g. /media or //ury.org.uk/media - * @var String - */ - public static $public_media_uri = '/media'; - - /** - * The full web address to the image that will be served for a show if there - * is not a photo for that show. - * @var String - */ - public static $default_show_uri = '/media/image_meta/ShowImageMetadata/22.png'; - - /** - * The full web address to the image that will be served on a member's profile page if they do not have a profile - * photo. The original value, /static/img/default_show_player.png is the main website's placeholder for shows - * @var String - */ - public static $default_person_uri = '/static/img/default_show_player.png'; - - /** - * The full web address of the image that will be shown for a vacant officer position - * @var String - */ - public static $vacant_officer_uri = '/media/image_meta/MyRadioImageMetadata/32.jpeg'; - - /** - * The file system path to the Central Database. Must be absolute. Can not be smb://, but may be a network share - * mounted to the file system mountpoint. - * @var String - */ - public static $music_central_db_path = '/music'; - - /** - * The file to be played if the obit procedure is triggered. - * @var String - */ - public static $jukebox_obit_file = '/jukebox/OBIT.mp3'; - - /** - * The Samba File Share path to the Central Database. This is used for BAPS compatibility features. - * @var String - */ - public static $music_smb_path = '\\\\musicstore.ury.york.ac.uk'; - - /** - * A path to temporarially store uploaded audio files. Recommend somewhere in /tmp, MyRadio needs full r/w access to it. - * @var String - */ - public static $audio_upload_tmp_dir = '/tmp/myradioaudiouploadcache'; - - /** - * The API key to access last.fm's resources. - * @var String - */ - public static $lastfm_api_key; - - /** - * The API Secret to write last.fm's resources. - * @var String - */ - public static $lastfm_api_secret; - - /** - * The array of different versions of tracks one can expect to find in the Central Database. Used for file servers - * and other systems to ensure the file requested seems legit. - * @var Array[String] - */ - public static $music_central_db_exts = array('mp3', 'ogg', 'mp3.orig'); - - /** - * Mailing list to send reporting info to - * @var String - * @todo Make this point to a MyRadio_List ID? - */ - public static $reporting_list = 'alerts.myury'; - - /** - * The IP/hostname of the iTones Liquidsoap Telnet Service - * @var String - */ - public static $itones_telnet_host = '144.32.64.167'; - /** - * The port of the iTones Liquidsoap Telnet Service - * @var int - */ - public static $itones_telnet_port = 1234; - - /** - * The maximum number of requests in one $itones_request_period per user. - * @var int - */ - public static $itones_request_maximum = 5; - - /** - * The period in which a user can use up to $itones_request_maximum requests. - * - * This is evaluated as a PostgreSQL INTERVAL: examples of valid values are - * '1 hour', '5 minutes' or '10:00:00'. - * - * @var String - */ - public static $itones_request_period = '1 hour'; - - /** - * The IP/hostname of the Studio Selector Telnet Service - * @var String - */ - public static $selector_telnet_host = '144.32.64.167'; - - /** - * The port of the Studio Selector Telnet Service - * @var int - */ - public static $selector_telnet_port = 1354; - - /** - * The path to the file that reports the state of the remote OB feeds - * @var String - */ - public static $ob_remote_status_file = '/music/ob_state.conf'; - - /**** ERROR REPORTING ****/ - - /** - * The file to store MyRadio Error Logs - * @var String - */ - public static $log_file = '/var/log/ury-org-uk/myradio_errors.log'; - /** - * A lock file on the MyRadio Error Logs. Prevents email spam. - * @var String - */ - public static $log_file_lock = '/var/log/ury-org-uk/myradio_errors.lock'; - /** - * The email to send error reports to. This is different from reporting_email, which does statistical reports. - * @var String - */ - public static $error_report_email = 'alerts.myury'; - - /** - * The number of seconds an iTones Playlist lock is valid for before it expires. - * @var int - */ - public static $playlist_lock_time = 30; - - /** - * The User that MyRadio assumes when doing things as a background task - * @var int Mr Website - */ - public static $system_user = 779; - - /** - * This key enables automated access to the YUSU CMS information about URY's members - */ - public static $yusu_api_key; - - /** - * The default college for new users that do not specify one. - * 10 is Unknown. - */ - public static $default_college = 10; - - /** - * A path to the file system (preferably in /tmp) that the MyRadio Daemon tools can have write access to. It stores - * state information about the service that should not be permanent but presist after a reload of the service. - * @var String - */ - public static $daemon_lock_file = '/tmp/myradio_daemon.lock'; - - /** - * The root URL to URY's API - * - * Must be absolute. - * @var String - */ - public static $api_url = 'https://ury.org.uk/api'; - - /** - * The URL prefix to URY's webcam - * - * Must be absolute. With trailing / - * @var String - */ - public static $webcam_prefix = '//ury.org.uk/webcam/'; - - /** - * BRA Server - * @var String - */ - public static $bra_uri = 'ury.org.uk/bra'; - public static $bra_user = ''; - public static $bra_pass = ''; - - /** - * Relative path to the API. Must have trailing / - * @var String - */ - public static $api_uri = '/api/'; - - /** - * Recaptcha settings - * http://recaptcha.net - */ - public static $recaptcha_public_key = 'YOUR_API_KEY'; - public static $recaptcha_private_key = 'YOUR_PRIVATE_KEY'; - - /** - * Relative path to the SIS plugins. - * @var String - */ - public static $sis_plugin_folder = 'Models/SIS/plugins'; - - /** - * Relative path to the SIS tabs. - * @var String - */ - public static $sis_tab_folder = 'Models/SIS/tabs'; + +namespace MyRadio; /** - * Studio data - * name is the name that is shown if it is detected as the current output - * authenticated_machines is an array of IP addresses which will have all rights in SIS, even if they are non-officer - * colour is the colour of any alements identifying the studio. Any valid CSS color will work here - * @var Array + * Stores configuration settings. */ - public static $studios = array( - array( - 'name' => 'Campus Jukebox', - 'authenticated_machines' => array(), - 'colour' => '#0F0' - ), - array( - 'name' => 'Studio 1', - 'authenticated_machines' => array('144.32.64.181', '144.32.64.183'), - 'colour' => 'red' - ), - array( - 'name' => 'Studio 2', - 'authenticated_machines' => array('144.32.64.184', '144.32.64.185'), - 'colour' => '#0044BA' - ), - array( - 'name' => 'Outside Broadcast', - 'authenticated_machines' => array(), //TODO: Add the OB Machines here - 'colour' => '#bb00dc' - ), - ); - - /** - * URL of the news provider - * @var string - */ - public static $news_provider = "http://www.irn.co.uk/"; - - /** - * Host that the news provider must be accessed from - * @var string - */ - public static $news_proxy = "wc10.york.ac.uk:8080"; - - /** - * URY's Membership Fee - * @var float - */ - public static $membership_fee = 7.00; - - /** - * If enabled, the Members' News feature on the home page is active - */ - public static $members_news_enable = false; - - /** - * Authentication - * LDAP requires the ldap plugin (net/php5-ldap) - * The Authenticators are tried in order when completing user authentication - * operations. - */ - //public static $authenticators = ['MyRadioLDAPAuthenticator', 'MyRadioDefaultAuthenticator']; - public static $authenticators = ['MyRadioDefaultAuthenticator']; - public static $auth_ldap_server = 'ldap://ldap.york.ac.uk'; - public static $auth_ldap_root = 'ou=people,ou=csrv,ou=nos,dc=york,dc=ac,dc=uk'; - public static $auth_db_user = 'shibbobleh'; - public static $auth_db_pass = ''; - public static $eduroam_domain = 'york.ac.uk'; - public static $auth_ldap_friendly_name = 'IT Services'; - public static $auth_ldap_reset_url = 'https://idm.york.ac.uk/'; - - /** - * If true, users will be bound to a single Authenticator. Users whose - * authenticator is NULL will be asked to set an Authenticator after login. - * - * If it is false, all authenticators will be valid for all users. - * - * @var boolean - */ - public static $single_authenticator = false; - - /** - * If false, MyRadioDefaultAuthenticator will never pass, passwords will not - * be set for new users, and the Change Password functionality will not be - * available. - * - * @var boolean - */ - public static $enable_local_passwords = true; - - /** - * The number of days before the start of the academic year when accounts are inactivated - * The current choice should mean it resets results week. - */ - public static $account_expiry_before = 49; - - /**** DAEMON CONFIGURATION ****/ - public static $d_BAPSSync_enabled = false; - public static $d_EmailQueue_enabled = true; - public static $d_Fingerprinter_enabled = false; - public static $d_LabelFinder_enabled = false; - public static $d_MemberSync_enabled = false; - public static $d_Playlists_enabled = true; - public static $d_Podcast_enabled = true; - public static $d_StatsGen_enabled = true; - public static $d_Explicit_enabled = false; - - /**** STRINGS ****/ - public static $short_name = 'URY'; - public static $long_name = 'University Radio York'; - public static $founded = '1967'; - public static $email_domain = 'ury.org.uk'; - public static $welcome_email = <<Hi #NAME!

- -

Thanks for showing an interest in URY, your official student radio station.

- -

My name's Al, and I'm the Station Manager here at URY. It's my job to make it as -easy as possible to get on the air or join any of our other teams.

- -

If you're interested in getting involved in any of our teams (there's 11 of -them!), then reply to this email and I'll sort you out, or email the address -listed for that team on our website. -

- -

For more information about these, and everything else we do, you can: -

- -

Finally, URY has a lot of online -resources that are useful for all sorts of things, so you'll need your login - details:

-

Username: #USER
-Password: #PASS

- -

If you have any questions, feel free to ask by visiting us at our station in -Vanbrugh, or emailing training@ury.org.uk.

- -Hope to see you soon. -

---
-Al Riddell
-Station Manager
-
-University Radio York 1350AM
-Most Awarded Student Radio Station 2013
----------------------------------------------
-al.riddell@ury.org.uk -
----------------------------------------------
-On Air | Online | On Demand
-ury.org.uk +final class Config +{ + /** + * If true, MyRadio will open the setup wizard when accessed. + * This should be false in production unless you want bad things to happen. + * + * @var bool + */ + public static $setup = false; + /** + * The ID of the "MyRadio" Service. This *should* be the only Service, and therefore never change. + * It's technically a remnant of the originally slightly overengineered modularisation structure. + * + * @var int + */ + public static $service_id = 1; + /** + * The Module to assume if one is not explicitly provided. This is usually the MyRadio module, but if you're feeling + * special, you can make Stats or Library the default... + * + * @var string + */ + public static $default_module = 'MyRadio'; + /** + * The Action to assume if one is not explicitly provided. This is usually default, but if you're feeling special, + * you could make it index or home, but it'd break a lot of stuff. + * + * @var string + */ + public static $default_action = 'default'; + /** + * The hostname of the PostgreSQL database server. + * + * @var string + */ + public static $db_hostname = 'localhost'; + public static $db_name = 'membership'; + /** + * The username to use connecting to the PostgreSQL database server. + * + * @var string + */ + public static $db_user = 'root'; + /** + * The password to use connecting to the PostgreSQL database server. + * + * @var string + */ + public static $db_pass = 'password'; + /** + * The public timezone of the installation. + * + * @todo Full review and ensure UTC is used where appropriate (i.e. Scheduler) + * + * @var string + */ + public static $timezone = 'Europe/London'; + + /** + * The base URL of the MyRadio installation. + * + * @var string + */ + public static $base_url = '//ury.org.uk/myradio/'; + + /** + * The base URL of the schedule - has some JS resources from MyRadio. + * + * @var string + */ + public static $schedule_url = '//ury.org.uk/schedule'; + + /** + * The full URL for the off-air studio booking microservice site. + * + * @var string + */ + public static $booking_url = '//booking.ury.org.uk/'; + + /** + * The base URL of the radio home pages. + * + * @var string + */ + public static $website_url = '//ury.org.uk/'; + + /** + * Whether nice URL rewrites are enabled + * If true, then urls will be myradio/[module]/[action] + * If false, then urls will be myradio/?module=[module]&action=[action]. + * + * @var bool + */ + public static $rewrite_url = true; + + /** + * Whether to enable the Caching system + * Development value: false + * Production value: true. + * + * @var bool + */ + public static $cache_enable = true; + /** + * The name of the class that will provide the MyRadio Caching mechanism. + * Must be the name of a valid class located in classes/[classname].php and implements CacheProvider. + * + * @var string + */ + public static $cache_provider = '\MyRadio\MemcachedProvider'; + /** + * If using the Memcached CacheProvider, set a list of servers to use here. + * e.g. [['localhost', 11211]] (optionally, a third option, a weighting). + */ + public static $cache_memcached_servers = [['localhost', 11211]]; + /** + * How long ServiceAPI items should be cached for by default. Turn this down if you + * get a lot of edits from other sources. + * + * @var int + */ + public static $cache_default_timeout = 86400; + + /** + * A path to a GeoLite2 or GeoIP2 "City" database file. + * @var string + */ + public static $geoip_database_path = ''; + + /** + * Whether MyRadio errors should be displayed in the browser. If this is set to false, users with the + * AUTH_SHOWERRORS permission will still see errors. + * Development value: true + * Production value: false. + * + * @var bool + */ + public static $display_errors = false; + + /** + * Whether MyRadio Exceptions should be emailed to Computing. + * + * @var bool + */ + public static $email_exceptions = true; + + /** + * Prevent an exception surge by failing if this many are thrown. + * + * @var int + */ + public static $exception_limit = 10; + + /** + * Whether template debugging should be enabled + * Development value: true + * Production value: false. + * + * @var bool + */ + public static $template_debug = true; + + /** + * The default number of results from an AJAX Search Query + * This can be overriden on a per-request basis. + * + * @var int + */ + public static $ajax_limit_default = 25; + + /** + * The photoid to use for a Joined URY Timeline Event. + * + * @var int + */ + public static $photo_joined = 1; + /** + * The photoid to use for a Gained Officership URY Timeline Event. + * + * @var int + */ + public static $photo_officership_get = 2; + /** + * The photoid to use for a Lost Officership URY Timeline Event. + * + * @var int + */ + public static $photo_officership_down = 3; + /** + * The photoid to use for a Got Show URY Timeline Event. + * + * @var int + */ + public static $photo_show_get = 4; + /** + * The photoid to use for a Got Award URY Timeline Event. + * + * @var int + */ + public static $photo_award_get = 5; + + /** + * The id of the news feed to use for news events. + * + * @var int + */ + public static $news_feed = 1; + + /** + * The id of the news feed to use for presenter infomation. + * + * @var int + */ + public static $presenterinfo_feed = 4; + + /** + * The path to the motion Webcam logs. This must be a file path, but may be NFS/Samba mounter. + * + * @var string + */ + public static $webcam_archive_path = '/home/motion/videos'; + + /** + * The url of the webcam server status. + * + * @var string + */ + public static $webcam_current_url; + + /** + * The url of the webcam server setter. + * + * @var string + */ + public static $webcam_set_url; + + /** + * The path to store the original, unencoded copies of MyRadio Podcasts. + * The originals are archived here for future reencoding. + * + * @var string + */ + public static $podcast_archive_path = '/music/podcasts'; + + /** + * The file system path where media should be stored. Used for podcasts, banners and images. + * + * @var string + */ + public static $public_media_path = '/home/django/virtualenvs/urysite/assets/media'; + /** + * This is the HTTP-accessible version of the above directory. Should be absolute or relative to domain, but not + * protocol-specific, e.g. /media or //ury.org.uk/media. + * + * @var string + */ + public static $public_media_uri = '/media'; + + /** + * The full web address to the image that will be served for a show if there + * is not a photo for that show. + * + * @var string + */ + public static $default_show_uri = '/media/image_meta/ShowImageMetadata/22.png'; + + /** + * The full web address to the image that will be served outside of term time. + * + * @var string + */ + public static $offair_uri = '/media/image_meta/ShowImageMetadata/offair.png'; + + /** + * The full web address to the image that will be served on a member's profile page if they do not have a profile + * photo. The original value, /images/default_show_profile.png is the main website's placeholder for shows. + * + * @var string + */ + public static $default_person_uri = '/images/default_show_profile.png'; + + /** + * The full web address of the image that will be shown for a vacant officer position. + * + * @var string + */ + public static $vacant_officer_uri = '/media/image_meta/MyRadioImageMetadata/32.jpeg'; + + /** + * The full web address of a copy of the Presenters Contract. + * + * @note If this variable is empty, then contracts are disabled. + * + * @var string + */ + public static $contract_uri = ''; + + /** + * The file system path to the Central Database. Must be absolute. Can not be smb://, but may be a network share + * mounted to the file system mountpoint. + * + * @var string + */ + public static $music_central_db_path = '/music'; + + /** + * The file to be played if the obit procedure is triggered. + * This is only used if you use the MyRadio iTones Liquidsoap playout tools. + * + * @var string + */ + public static $jukebox_obit_file = '/jukebox/OBIT.mp3'; + + /** + * The category of playlists that Jukebox should get songs from. + * @var int + */ + public static $jukebox_playlist_category_id = 2; + + /** + * The Samba File Share path to the Central Database. + * This is used for BAPS compatibility features. + * + * If you are not URY, you will likely not need this setting. + * However, if you have a studio audio playout tool that needs + * samba paths to files stored in a database, set this here. + * This data will then be stored in the public.baps_ table. + * + * @var string + */ + public static $music_smb_path = '\\\\musicstore.ury.york.ac.uk'; + + /** + * A path to temporarily store uploaded audio files. Recommend somewhere in /tmp, + * MyRadio needs full r/w access to it to enable Library management. + * + * @var string + */ + public static $audio_upload_tmp_dir = '/tmp/myradioaudiouploadcache'; + + /** + * The maximum allowed size of a single Track upload, in MB. + * Still bound by php.ini settings. + * + * @var string + */ + public static $audio_upload_max_size = 15; + + /** + * The API key to access last.fm's resources. + * + * You will need one of these to enable Library management. + * + * @var string + */ + public static $lastfm_api_key; + + /** + * The API Secret to write last.fm's resources. + * + * @var string + */ + public static $lastfm_api_secret; + + /** + * The last.fm nation of choice, at least for us. If using + * this aspect of the code you probably want to change this bit. + */ + public static $lastfm_geo = 'United+Kingdom'; + + /** + * The array of different versions of tracks one can expect to find in the + * Central Database. Used for file servers and other systems to ensure + * the file requested seems legit. + * + * @var Array[String] + */ + public static $music_central_db_exts = ['mp3', 'ogg', 'mp3.orig']; + + /** + * Mailing list to send reporting info to. + * + * @var string + * + * @todo Make this support non-MyRadio managed addresses + */ + public static $reporting_list = 'alerts.myradio'; + + /** + * The IP/hostname of the iTones Liquidsoap Telnet Service. + * + * @var string + */ + public static $itones_telnet_host = '144.32.64.167'; + /** + * The port of the iTones Liquidsoap Telnet Service. + * + * @var int + */ + public static $itones_telnet_port = 1234; + + /** + * The maximum number of requests in one $itones_request_period per user. + * + * @var int + */ + public static $itones_request_maximum = 5; + + /** + * The period in which a user can use up to $itones_request_maximum requests. + * + * This is evaluated as a PostgreSQL INTERVAL: examples of valid values are + * '1 hour', '5 minutes' or '10:00:00'. + * + * @var string + */ + public static $itones_request_period = '1 hour'; + + /** + * The IP/hostname of the Studio Selector Telnet Service. + * + * @var string + */ + public static $selector_telnet_host = '144.32.64.167'; + + /** + * The port of the Studio Selector Telnet Service. + * + * @var int + */ + public static $selector_telnet_port = 1354; + + /** + * The path to the file that reports the state of the remote OB feeds. + * + * @var string + */ + public static $ob_remote_status_file = '/music/ob_state.conf'; + + /**** ERROR REPORTING ****/ + + /** + * The file to store MyRadio Error Logs. + * + * @var string + */ + public static $log_file = '/var/log/myradio/errors.log'; + /** + * A lock file on the MyRadio Error Logs. Prevents email spam. + * + * @var string + */ + public static $log_file_lock = '/tmp/myradio_errors.lock'; + /** + * The email to send error reports to. This is different from reporting_list, + * which does statistical reports, if enabled. + * + * @var string + */ + public static $error_report_email = 'alerts.myradio'; + + /** + * The number of seconds an iTones Playlist lock is valid for before it expires. + * + * @var int + */ + public static $playlist_lock_time = 30; + + /** + * The User that MyRadio assumes when doing things as a background task. + * + * @var int Mr Website + */ + public static $system_user = 779; + + /** + * The URL for the SU page allowing people to pay and join the society. + */ + public static $yusu_payment_url; + + /** + * This key enables automated access to the YUSU CMS information about URY's members. + * + * This is literally only useful if you are URY. + */ + public static $yusu_api_key; + + /** + * The web address (up to the endpoint) where the YUSU API lives. It changes from time + * to time so check that the API calls are actually succeeding now and then. + */ + public static $yusu_api_website; + + /** + * The default college for new users that do not specify one. + * 1 is Unknown. (Unless you're URY, in which case it's 10 because legacy). + */ + public static $default_college = 1; + + /** + * A path to the file system (preferably in /tmp) that the MyRadio Daemon tools can have write access to. It stores + * state information about the service that should not be permanent but presist after a reload of the service. + * + * @var string + */ + public static $daemon_lock_file = '/tmp/myradio_daemon.lock'; + + /** + * The root URL to the API. + * + * Must be absolute. + * + * @var string + */ + public static $api_url = '/api'; + + /** + * The URL prefix to URY's webcam. + * + * Must be absolute. With trailing / + * + * @var string + */ + public static $webcam_prefix = '//ury.org.uk/webcam/'; + + /** + * Relative path to the API. Must have trailing /. + * + * @var string + */ + public static $api_uri = '/api/'; + + /** + * Recaptcha settings. Used for password resets. + * + * http://recaptcha.net + * + * @var string + */ + public static $recaptcha_public_key = 'YOUR_API_KEY'; + public static $recaptcha_private_key = 'YOUR_PRIVATE_KEY'; + public static $jwt_signing_secret = 'SECRET_HERE'; + + public static $autoviz_clips_path = ''; + public static $autoviz_public_clips_base = '/media/autoviz-clips'; + + /** + * Array of tabs and plugins to be used by SIS. They will be loaded in order. + */ + public static $sis_modules = [ + 'presenterinfo', + 'messages', + 'schedule', + 'tracklist', + 'links', + 'selector', + 'webcam', + 'obit', + ]; + + /** + * Array of spam strings to check messages for. + * + * @var string[] + */ + public static $spam = []; + + /** + * Array of social engineering strings to check messages for. + * + * @var string[] + */ + public static $social_engineering_trigger = []; + + /** + * Warning text to display on suspected social engineering attacks. + * + * @var string + */ + public static $social_engineering_warning = 'Beware of Social Engineering, someone may be trying to spoil your show. + Management and Computing will never send official communication through SIS.'; + + /** + * URL of the news provider. + * + * @var string + */ + public static $news_provider = 'http://www.irn.co.uk/'; + + /** + * Host that the news provider must be accessed from. + * + * @var string + */ + public static $news_proxy = ''; + + /** + * URY's Membership Fee. + * + * @var float + */ + public static $membership_fee = 7.00; + + /** + * If enabled, the Members' News feature on the home page is active. + */ + public static $members_news_enable = false; + + /** + * Authentication + * LDAP requires the ldap plugin (net/php5-ldap) + * The Authenticators are tried in order when completing user authentication + * operations. + */ + public static $authenticators = ['\MyRadio\MyRadio\MyRadioDefaultAuthenticator']; + public static $auth_ldap_server = 'ldap://ldap.york.ac.uk'; + public static $auth_ldap_root = 'ou=people,ou=csrv,ou=nos,dc=york,dc=ac,dc=uk'; + public static $auth_db_user = ''; + public static $auth_db_pass = ''; + public static $auth_ldap_regex = '/[a-z]{2,}[0-9]+(@york\.ac\.uk|)$/'; + + /** + * Optional eduroam auth domain (probably .ac.uk). + * + * @var string + */ + public static $eduroam_domain = 'york.ac.uk'; + public static $auth_ldap_friendly_name = 'IT Services'; + public static $auth_ldap_reset_url = 'https://idm.york.ac.uk/'; + + /** + * Email configuration. + */ + + //All email domains handled by MyRadio + public static $local_email_domains = []; + + /** + * Primary email domain. MyRadio will send emails from this domain. + * + * @var string + */ + public static $email_domain = 'ury.org.uk'; + + /** + * If true, users will be bound to a single Authenticator. Users whose + * authenticator is NULL will be asked to set an Authenticator after login. + * + * If it is false, all authenticators will be valid for all users. + * + * @var bool + */ + public static $single_authenticator = false; + + /** + * If false, MyRadioDefaultAuthenticator will never pass, passwords will not + * be set for new users, and the Change Password functionality will not be + * available. + * + * @var bool + */ + public static $enable_local_passwords = true; + + /** + * The number of days before the start of the academic year when accounts are inactivated + * The current choice should mean it resets results week. + */ + public static $account_expiry_before = 49; + + /** + * The email list to send Obit activation notifications to. + */ + public static $obit_list_id = 36; + + /** + * @var bool Enable analytics? + */ + public static $enable_analytics = false; + + /** + * @var string[] a list of slug prefixes that should not be accepted for short URLs + */ + public static $short_url_forbidden_slugs = []; + + /**** DAEMON CONFIGURATION ****/ + public static $d_BAPSSync_enabled = false; + public static $d_EmailQueue_enabled = true; + public static $d_Fingerprinter_enabled = false; + public static $d_LabelFinder_enabled = false; + public static $d_MemberSync_enabled = false; + public static $d_Playlists_enabled = true; + public static $d_Podcast_enabled = true; + public static $d_StatsGen_enabled = true; + public static $d_TrackAndTrace_enabled = true; + public static $d_Explicit_enabled = false; + public static $d_DemoCleanup_enabled = true; + + /**** STRINGS ****/ + public static $short_name = 'URY'; + public static $long_name = 'University Radio York'; + public static $founded = '1967'; + public static $facebook = 'https://www.facebook.com/URY1350'; + + /**** SIGNUP EMAILS ****/ + public static $welcome_email_sender_memberid = null; + public static $welcome_email = << self::$api_url, - 'ajax_limit_default' => self::$ajax_limit_default, - 'base_url' => self::$base_url, - 'rewrite_url' => self::$rewrite_url, - 'shib_url' => self::$shib_url, - 'timezone' => self::$timezone, - 'default_module' => self::$default_module, - 'default_action' => self::$default_action, - 'webcam_prefix' => self::$webcam_prefix, - 'bra_uri' => self::$bra_uri, - 'bra_user' => self::$bra_user, - 'bra_pass' => self::$bra_pass, - 'short_name' => self::$short_name, - 'long_name' => self::$long_name, - 'founded' => self::$founded, - 'facebook' => self::$facebook - ); - } + + /** + * The MyRadio features that are under maintenance. + * + * This should be an associative array of arrays, where the keys are module names + * and the values are the actions in that module that are under maintenance and should be disabled for users. + * + * For example, + * + * [ + * 'scheduler' => ['editShow', 'editSeason'] + * ] + * + * would disable editing (and creation) of shows and seasons. + * + * Instead of passing an array of action names, you can pass '*', which would treat all actions in that module + * as under maintenance. + * + * You can also set $maintenance_modules to '*' to shut down MyRadio altogether (don't do that). + * + */ + public static $maintenance_modules = []; + + /** + * The constructor doesn't do anything practical. + * + * By making the constructor private, even though it does not do anything, we are prohibiting code elsewhere from + * creating instances of this class, making it essentially static + */ + private function __construct() + { + } + + /** + * "Public" config is the configuration variables that should be made exposed to JavaScript within the mConfig + * object. + * + * @return array + */ + public static function getPublicConfig() + { + return [ + 'api_url' => self::$api_url, + 'ajax_limit_default' => self::$ajax_limit_default, + 'base_url' => self::$base_url, + 'rewrite_url' => self::$rewrite_url, + 'schedule_url' => self::$schedule_url, + 'booking_url' => self::$booking_url, + 'website_url' => self::$website_url, + 'timezone' => self::$timezone, + 'default_module' => self::$default_module, + 'default_action' => self::$default_action, + 'webcam_prefix' => self::$webcam_prefix, + 'short_name' => self::$short_name, + 'long_name' => self::$long_name, + 'founded' => self::$founded, + 'email_domain' => self::$email_domain, + 'facebook' => self::$facebook, + 'audio_upload_max_size' => self::$audio_upload_max_size, + 'payment_url' => self::$yusu_payment_url, + ]; + } } diff --git a/src/Classes/Daemons/MyRadio_BAPSSyncDaemon.php b/src/Classes/Daemons/MyRadio_BAPSSyncDaemon.php old mode 100644 new mode 100755 index ad0a26fb5..d77ca8c9d --- a/src/Classes/Daemons/MyRadio_BAPSSyncDaemon.php +++ b/src/Classes/Daemons/MyRadio_BAPSSyncDaemon.php @@ -1,106 +1,145 @@ - * @version 20130711 - * @package MyRadio_Daemon */ -class MyRadio_BAPSSyncDaemon extends MyRadio_Daemon { - /** - * If this method returns true, the Daemon host should run this Daemon. If it returns false, it must not. - * It is currently disabled because it hasn't actuall finished being ported from NIPSWeb 1. - * @return boolean - */ - public static function isEnabled() { return Config::$d_BAPSSync_enabled; } - - /** - * Once an hour, this takes each of the NIPSWebs Managed and Resource Lists, - * and converts them into BAPS Recommended Audio shows. - * This is a Legacy tool to support the old BAPS system. DO NOT TOUCH. BAPS IS SILLY. - */ - public static function run() { - $hourkey = __CLASS__.'_last_run_hourly'; - if (self::getVal($hourkey) > time() - 3500) return; - - $special_date = '2034-05-06 07:08:09'; //All shows created by this are identified by this time. - $db = Database::getInstance(); - - //This needs to appear atomic to end users - $db->query('BEGIN'); - $db->query('DELETE FROM public.baps_show WHERE broadcastdate=$1', array($special_date)); - //Start with the Jukebox Playlists - foreach (iTones_Playlist::getAlliTonesPlaylists() as $list) { - /** - * Create the playlist. '61' is system, which appears to be how BAPS chooses what shows are recommended listening - */ - $r = $db->fetch_column('INSERT INTO public.baps_show (userid, name, broadcastdate, viewable) - VALUES (61, $1, $2, true) RETURNING showid', array($list->getTitle(), $special_date)); - $showid = $r[0]; - - if (empty($r)) - return; - - /** - * Create a single channel for the show, containing the items - */ - $r = $db->fetch_one('INSERT INTO public.baps_listing (showid, name, channel) VALUES - ($1, $2, 0) RETURNING listingid', array($showid, $list->getTitle())); - $listingid = $r[0]; - - if (empty($r)) - return; - - $i = 0; - foreach ($this->getManagedPlaylist($list['playlistid']) as $item) { - $track = $this->getTrackDetails($item['trackid'], $item['recordid']); - $i++; - pg_query_params('INSERT INTO public.baps_item (listingid, name1, name2, position, libraryitemid) - VALUES ($1, $2, $3, $4, $5)', array($listingid, $item['title'], $item['artist'], $i, $track['libraryitemid'])); - echo pg_last_error(); - } +class MyRadio_BAPSSyncDaemon extends \MyRadio\MyRadio\MyRadio_Daemon +{ + /** + * If this method returns true, the Daemon host should run this Daemon. If it returns false, it must not. + * It is currently disabled because it hasn't actuall finished being ported from NIPSWeb 1. + * + * @return bool + */ + public static function isEnabled() + { + return Config::$d_BAPSSync_enabled; } - //And now the Aux Resource Playlists - foreach ($this->getCentralResourceLists() as $list) { - /** - * Create the playlist. '61' is system, which appears to be how BAPS chooses what shows are recommended listening - */ - $r = pg_fetch_row(pg_query_params('INSERT INTO public.baps_show (userid, name, broadcastdate, viewable) - VALUES (61, \'Managed::\'||$1, $2, true) RETURNING showid', array($list['name'], $special_date))); - $showid = $r[0]; - echo pg_last_error(); - if (!$r) - return; - - /** - * Create a single channel for the show, containing the items - */ - $r = pg_fetch_row(pg_query_params('INSERT INTO public.baps_listing (showid, name, channel) VALUES - ($1, $2, 0) RETURNING listingid', array($showid, $list['name']))); - $listingid = $r[0]; - echo pg_last_error(); - if (!$r) - return; - - $i = 0; - foreach ($this->getAuxItems($list['folder']) as $item) { - $track = $this->getFileItemFromManagedID($item['manageditemid']); - $i++; - pg_query_params('INSERT INTO public.baps_item (listingid, name1, name2, position, fileitemid) - VALUES ($1, $2, $3, $4, $5)', array($listingid, $item['title'], '', $i, $track)); + /** + * Once an hour, this takes each of the NIPSWebs Managed and Resource Lists, + * and converts them into BAPS Recommended Audio shows. + * This is a Legacy tool to support the old BAPS system. DO NOT TOUCH. BAPS IS SILLY. + */ + public static function run() + { + $hourkey = __CLASS__.'_last_run_hourly'; + if (self::getVal($hourkey) > time() - 3500) { + return; + } + + $special_date = '2034-05-06 07:08:09'; //All shows created by this are identified by this time. + $db = Database::getInstance(); + + //This needs to appear atomic to end users + $db->query('BEGIN'); + $db->query('DELETE FROM public.baps_show WHERE broadcastdate=$1', [$special_date]); + //Start with the Jukebox Playlists + foreach (iTones_Playlist::getAlliTonesPlaylists() as $list) { + /* Create the playlist. '61' is system, which appears to be how + * BAPS chooses what shows are recommended listening */ + $r = $db->fetchColumn( + 'INSERT INTO public.baps_show (userid, name, broadcastdate, viewable) + VALUES (61, $1, $2, true) RETURNING showid', + [$list->getTitle(), $special_date] + ); + $showid = $r[0]; + + if (empty($r)) { + return; + } + + /* + * Create a single channel for the show, containing the items + */ + $r = $db->fetchOne( + 'INSERT INTO public.baps_listing (showid, name, channel) VALUES ($1, $2, 0) RETURNING listingid', + [$showid, $list->getTitle()] + ); + $listingid = $r['listingid']; + + if (empty($r)) { + return; + } + + $i = 0; + + foreach ($list->getTracks() as $track) { + ++$i; + $libraryitemid = NIPSWeb_BAPSUtils::getTrackDetails( + $track->getID(), + $track->getAlbum()->getID() + )['libraryitemid']; + pg_query_params( + 'INSERT INTO public.baps_item (listingid, name1, name2, position, libraryitemid) + VALUES ($1, $2, $3, $4, $5)', + [$listingid, $track->getTitle(), $track->getArtist(), $i, $libraryitemid] + ); + echo pg_last_error(); + } + } + + //And now the Aux Resource Playlists + foreach (NIPSWeb_ManagedPlaylist::getAllManagedPlaylists() as $list) { + /* Create the playlist. '61' is system, which appears to be how + * BAPS chooses what shows are recommended listening */ + $r = pg_fetch_row( + pg_query_params( + 'INSERT INTO public.baps_show (userid, name, broadcastdate, viewable) + VALUES (61, \'Managed::\'||$1, $2, true) RETURNING showid', + [$list->getTitle(), $special_date] + ) + ); + + $showid = $r[0]; + echo pg_last_error(); + if (!$r) { + return; + } + + /* + * Create a single channel for the show, containing the items + */ + $r = pg_fetch_row( + pg_query_params( + 'INSERT INTO public.baps_listing (showid, name, channel) VALUES + ($1, $2, 0) RETURNING listingid', + [$showid, $list->getTitle()] + ) + ); + + $listingid = $r[0]; + echo pg_last_error(); + if (!$r) { + return; + } + + $i = 0; + foreach ($list->getItems() as $item) { + $track = NIPSWeb_BAPSUtils::getFileItemFromManagedID($item->getID()); + ++$i; + pg_query_params( + 'INSERT INTO public.baps_item (listingid, name1, name2, position, fileitemid) + VALUES ($1, $2, $3, $4, $5)', + [$listingid, $item->getTitle(), '', $i, $track] + ); + echo pg_last_error(); + } + } + echo pg_last_error(); - } - } + pg_query('COMMIT'); - echo pg_last_error(); - pg_query('COMMIT'); - - self::setVal($hourkey, time()); - } -} \ No newline at end of file + self::setVal($hourkey, time()); + } +} diff --git a/src/Classes/Daemons/MyRadio_DemoCleanupDaemon.php b/src/Classes/Daemons/MyRadio_DemoCleanupDaemon.php new file mode 100644 index 000000000..1b18987b0 --- /dev/null +++ b/src/Classes/Daemons/MyRadio_DemoCleanupDaemon.php @@ -0,0 +1,59 @@ + time() - 3600) { + return; + } + + try { + self::cleanUp(); + } finally { + // Done + self::setVal($runKey, time()); + } + } + + private static function cleanUp() + { + $db = Database::getInstance(); + $records = $db->fetchAll(' + SELECT da.demo_id, da.memberid + FROM schedule.demo_attendee da + INNER JOIN schedule.demo d on d.demo_id = da.demo_id + LEFT JOIN public.member_presenterstatus mps ON mps.memberid = da.memberid AND mps.presenterstatusid = d.presenterstatusid + WHERE d.demo_time < (NOW() - \'1 hour\'::interval) + AND mps.memberpresenterstatusid IS NULL + '); + foreach ($records as $row) { + $db->query(' + DELETE FROM schedule.demo_attendee + WHERE demo_id = $1 AND memberid = $2 + ', [$row['demo_id'], $row['memberid']]); + // No need to clear cache, as the "is already attending" check is done through SQL. + } + } +} + diff --git a/src/Classes/Daemons/MyRadio_EmailQueueDaemon.php b/src/Classes/Daemons/MyRadio_EmailQueueDaemon.php old mode 100644 new mode 100755 index 2cd272646..1c80062d0 --- a/src/Classes/Daemons/MyRadio_EmailQueueDaemon.php +++ b/src/Classes/Daemons/MyRadio_EmailQueueDaemon.php @@ -1,20 +1,33 @@ fetch_column('SELECT email_id FROM mail.email WHERE - (email_id IN (SELECT email_id FROM mail.email_recipient_member WHERE sent=\'f\') - OR email_id IN (SELECT email_id FROM mail.email_recipient_list WHERE sent=\'f\')) - AND timestamp <= NOW() LIMIT 5'); - - foreach ($result as $email) { - echo "Sending email $email\n"; - MyRadioEmail::getInstance($email)->send(); +namespace MyRadio\Daemons; + +use MyRadio\Config; +use MyRadio\Database; +use MyRadio\MyRadioEmail; + +class MyRadio_EmailQueueDaemon +{ + public static function isEnabled() + { + return Config::$d_EmailQueue_enabled; + } + + public static function run() + { + //Get up to 5 unsent emails + $db = Database::getInstance(); + + $result = $db->fetchColumn( + 'SELECT email_id FROM mail.email WHERE + (email_id IN (SELECT email_id FROM mail.email_recipient_member WHERE sent=\'f\') + OR email_id IN (SELECT email_id FROM mail.email_recipient_list WHERE sent=\'f\')) + AND timestamp <= NOW() LIMIT 5' + ); + + foreach ($result as $email) { + echo "Sending email $email\n"; + MyRadioEmail::getInstance($email)->send(); + } } - } -} \ No newline at end of file +} diff --git a/src/Classes/Daemons/MyRadio_ExplicitDaemon.php b/src/Classes/Daemons/MyRadio_ExplicitDaemon.php old mode 100644 new mode 100755 index df765540e..ad6f2bba6 --- a/src/Classes/Daemons/MyRadio_ExplicitDaemon.php +++ b/src/Classes/Daemons/MyRadio_ExplicitDaemon.php @@ -1,56 +1,94 @@ - * @version 20130711 - * @package MyRadio_Daemon */ -class MyRadio_ExplicitDaemon extends MyRadio_Daemon { - - /** - * If this method returns true, the Daemon host should run this Daemon. If it returns false, it must not. - * It is currently enabled because we have a lot of labels that needed filling in for Tracklisting. - * @return boolean - */ - public static function isEnabled() { - return Config::$d_Explicit_enabled; - } - - static function run() { - $tracks = MyRadio_Track::findByOptions(['clean' => 'u', 'limit' => 10, - 'random' => true, 'digitised' => false]); - - foreach ($tracks as $track) { - $q = str_replace(' ', '+', $track->getTitle() . ' ' . $track->getArtist()); - $data = json_decode( - file_get_contents('http://itunes.apple.com/search?term=' - . $q . '&entity=song&limit=5'), true); - - for ($i = 0; $i < $data['resultCount']; $i++) { - /** - * explicit (explicit lyrics, possibly explicit album cover), cleaned - * (explicit lyrics "bleeped out"), notExplicit (no explicit lyrics) - */ - if ($data['results'][$i]['trackName'] == $track->getTitle() && - $data['results'][$i]['artistName'] == $track->getArtist()) { - - $clean = $data['results'][$i]['trackExplicitness'] == 'explicit' - ? 'n' : 'y'; - $track->setClean($clean); - - dlog($track->getTitle() . ' (' . $track->getAlbum()->getID() - . '/' . $track->getID() . ') is ' . $clean, 2); - break; - } - } +class MyRadio_ExplicitDaemon extends \MyRadio\MyRadio\MyRadio_Daemon +{ + private static $digitised_only = true; + + /** + * If this method returns true, the Daemon host should run this Daemon. If it returns false, it must not. + * It is currently enabled because we have a lot of labels that needed filling in for Tracklisting. + * + * @return bool + */ + public static function isEnabled() + { + return Config::$d_Explicit_enabled; } - } -} \ No newline at end of file + public static function run($force = false) + { + + $hourkey = __CLASS__.'_last_run_hourly'; + if (!$force && self::getVal($hourkey) > time() - 3500) { + return; + } + + $db = Database::getInstance(); + + $tracks = MyRadio_Track::findByOptions( + [ + 'clean' => 'u', + 'limit' => 25, + 'random' => true, + 'digitised' => self::$digitised_only, + 'custom' => 'trackid NOT IN (SELECT trackid FROM music.explicit_checked)', + ] + ); + + if (empty($tracks)) { + self::$digitised_only = false; + } + + foreach ($tracks as $track) { + $q = trim($track->getTitle().' '.$track->getArtist()); + $data = json_decode( + file_get_contents( + 'http://itunes.apple.com/search?term=' + .urlencode($q).'&entity=song&limit=5' + ), + true + ); + + dlog('Checking '.$q.' ('.$data['resultCount'].' matches)', 4); + + for ($i = 0; $i < $data['resultCount']; ++$i) { + /* + * explicit (explicit lyrics, possibly explicit album cover), cleaned + * (explicit lyrics "bleeped out"), notExplicit (no explicit lyrics) + */ + if ($data['results'][$i]['trackName'] == $track->getTitle() + && $data['results'][$i]['artistName'] == $track->getArtist() + ) { + $clean = $data['results'][$i]['trackExplicitness'] == 'explicit' + ? 'n' : 'y'; + $track->setClean($clean); + + dlog( + 'Setting Explicicity of '. + $track->getTitle().' ('.$track->getAlbum()->getID() + .'/'.$track->getID().') as '.$clean, + 2 + ); + break; + } + } + + $db->query('INSERT INTO music.explicit_checked VALUES ($1)', [$track->getID()]); + } + + //Done + self::setVal($hourkey, time()); + } +} diff --git a/src/Classes/Daemons/MyRadio_FingerprinterDaemon.php b/src/Classes/Daemons/MyRadio_FingerprinterDaemon.php old mode 100644 new mode 100755 index c2273f687..f1e4faf40 --- a/src/Classes/Daemons/MyRadio_FingerprinterDaemon.php +++ b/src/Classes/Daemons/MyRadio_FingerprinterDaemon.php @@ -1,91 +1,119 @@ - * @version 20130711 - * @package MyRadio_Daemon */ -class MyRadio_FingerprinterDaemon extends MyRadio_Daemon { - /** - * If this method returns true, the Daemon host should run this Daemon. If it returns false, it must not. - * It is currently enabled for a full scan of the music library. Generally, it may often be disabled as it - * generates a fare amount of load and actually making the changes is a manual process anyway. - * @return boolean - */ - public static function isEnabled() { return Config::$d_Fingerprinter_enabled; } - - /** - * Process a batch of tracks that are currently not verified as correct, and sees if Last.FM has - * metadata for it. - * - * This function can be have the batch size changed by changing the first line - * and can have less reliable proposals stored by modifying the rank comparison. - * Change the levenshtein comparisons to tweak what amount of change is - * automatically approved. - * - * @todo While this logs Last.fm albums, it does not compare them. - */ - public static function run() { - //Get 5 unverified tracks. Tune the "limit" to change this - $tracks = MyRadio_Track::findByOptions(array('lastfmverified' => false, 'random' => true, 'digitised' => true, - 'nocorrectionproposed' => true, 'limit' => 5)); - - foreach ($tracks as $track) { - /** - * Run the last.fm Fingerprinter on the Track to see what they think it - * is. - */ - $info = MyRadio_Track::identifyUploadedTrack($track->getPath()); - - /** - * We use two metrics to identify if the information is reliable - * 1. Is the rank high (> 0.8)? - * 2. Does is have a short levenshtein difference from the current value? - */ - if (empty($info[0]) or $info[0]['rank'] < 0.8) { - echo 'Fingerprint data for '.$track->getID().' unreliable (p='.(empty($info[0]) ? '0' : $info[0]['rank'])."). Skipping.\n"; - continue; - } - - if ($info[0]['title'] !== $track->getTitle() && levenshtein($info[0]['title'], $track->getTitle()) <= 2) { - echo "Minor title correction made - {$track->getTitle()} to {$info[0]['title']}\n"; - $track->setTitle($info[0]['title']); - } - - if ($info[0]['artist'] !== $track->getArtist() && levenshtein($info[0]['artist'], $track->getArtist()) <= 2) { - echo "Minor artist correction made - {$track->getArtist()} to {$info[0]['artist']}\n"; - $track->setArtist($info[0]['artist']); - } - - if ($track->getTitle() == $info[0]['title'] - && $track->getArtist() == $info[0]['artist']) { - echo "Track {$track->getID()} verified as correct.\n"; - - $track->setLastfmVerified(); - continue; - } - - $album = MyRadio_Track::getAlbumDurationAndPositionFromLastfm($info[0]['title'], $info[0]['artist'])['album']->getTitle(); - - if (levenshtein($info[0]['title'], $track->getTitle()) < 8 - or levenshtein($info[0]['artist'], $track->getArtist()) < 5 - or levenshtein($album, $track->getAlbum()->getTitle()) < 5) { - MyRadio_TrackCorrection::create($track, $info[0]['title'], $info[0]['artist'], $album, MyRadio_TrackCorrection::LEVEL_RECOMMEND); - echo "Correction recommended for {$track->getID()}.\n"; - } else { - MyRadio_TrackCorrection::create($track, $info[0]['title'], $info[0]['artist'], $album, MyRadio_TrackCorrection::LEVEL_SUGGEST); - echo "Correction suggested {$track->getID()}.\n"; - } - - //The Daemons slowly leaks memory if we don't clean up Track objects here - you can have a GB or so in a day - $track->removeInstance(); +class MyRadio_FingerprinterDaemon extends \MyRadio\MyRadio\MyRadio_Daemon +{ + /** + * If this method returns true, the Daemon host should run this Daemon. If it returns false, it must not. + * It is currently enabled for a full scan of the music library. Generally, it may often be disabled as it + * generates a fare amount of load and actually making the changes is a manual process anyway. + * + * @return bool + */ + public static function isEnabled() + { + return Config::$d_Fingerprinter_enabled; + } + + /** + * Process a batch of tracks that are currently not verified as correct, and sees if Last.FM has + * metadata for it. + * + * This function can be have the batch size changed by changing the first line + * and can have less reliable proposals stored by modifying the rank comparison. + * Change the levenshtein comparisons to tweak what amount of change is + * automatically approved. + * + * @todo While this logs Last.fm albums, it does not compare them. + */ + public static function run() + { + //Get 5 unverified tracks. Tune the "limit" to change this + $tracks = MyRadio_Track::findByOptions( + ['lastfmverified' => false, 'random' => true, 'digitised' => true, + 'nocorrectionproposed' => true, 'limit' => 5, ] + ); + + foreach ($tracks as $track) { + /* + * Run the last.fm Fingerprinter on the Track to see what they think it + * is. + */ + $info = MyRadio_Track::identifyUploadedTrack($track->getPath()); + + /* + * We use two metrics to identify if the information is reliable + * 1. Is the rank high (> 0.8)? + * 2. Does is have a short levenshtein difference from the current value? + */ + if (empty($info[0]) || $info[0]['rank'] < 0.8) { + echo 'Fingerprint data for '.$track->getID() . + ' unreliable (p=' . (empty($info[0]) ? '0' : $info[0]['rank']) . "). Skipping.\n"; + continue; + } + + if ($info[0]['title'] !== $track->getTitle() && levenshtein($info[0]['title'], $track->getTitle()) <= 2) { + echo "Minor title correction made - {$track->getTitle()} to {$info[0]['title']}\n"; + $track->setTitle($info[0]['title']); + } + + if ($info[0]['artist'] !== $track->getArtist() + && levenshtein($info[0]['artist'], $track->getArtist()) <= 2 + ) { + echo "Minor artist correction made - {$track->getArtist()} to {$info[0]['artist']}\n"; + $track->setArtist($info[0]['artist']); + } + + if ($track->getTitle() == $info[0]['title'] + && $track->getArtist() == $info[0]['artist'] + ) { + echo "Track {$track->getID()} verified as correct.\n"; + + $track->setLastfmVerified(); + continue; + } + + $album = MyRadio_Track::getAlbumDurationAndPositionFromLastfm( + $info[0]['title'], + $info[0]['artist'] + )['album']->getTitle(); + + if (levenshtein($info[0]['title'], $track->getTitle()) < 8 + || levenshtein($info[0]['artist'], $track->getArtist()) < 5 + || levenshtein($album, $track->getAlbum()->getTitle()) < 5 + ) { + MyRadio_TrackCorrection::create( + $track, + $info[0]['title'], + $info[0]['artist'], + $album, + MyRadio_TrackCorrection::LEVEL_RECOMMEND + ); + echo "Correction recommended for {$track->getID()}.\n"; + } else { + MyRadio_TrackCorrection::create( + $track, + $info[0]['title'], + $info[0]['artist'], + $album, + MyRadio_TrackCorrection::LEVEL_SUGGEST + ); + echo "Correction suggested {$track->getID()}.\n"; + } + + //The Daemons slowly leaks memory if we don't clean up Track objects here - you can have a GB or so in a day + $track->removeInstance(); + } } - } -} \ No newline at end of file +} diff --git a/src/Classes/Daemons/MyRadio_LabelFinderDaemon.php b/src/Classes/Daemons/MyRadio_LabelFinderDaemon.php old mode 100644 new mode 100755 index 6aaf33e76..a60c9f19e --- a/src/Classes/Daemons/MyRadio_LabelFinderDaemon.php +++ b/src/Classes/Daemons/MyRadio_LabelFinderDaemon.php @@ -1,48 +1,65 @@ - * @version 20130711 - * @package MyRadio_Daemon */ -class MyRadio_LabelFinderDaemon extends MyRadio_Daemon { - /** - * If this method returns true, the Daemon host should run this Daemon. If it returns false, it must not. - * It is currently enabled because we have a lot of labels that needed filling in for Tracklisting. - * @return boolean - */ - public static function isEnabled() { return Config::$d_LabelFinder_enabled; } - - /** - * THE DISCOGS API IS RATE LIMITED TO ONE REQUEST PER SECOND. - */ - public static function run() { - //Get 5 albums without labels - $albums = Database::getInstance()->fetch_all('SELECT recordid, title, artist FROM public.rec_record - WHERE recordlabel=\'\' ORDER BY RANDOM() LIMIT 5'); - - foreach ($albums as $album) { - dlog('Checking record '.$album['recordid'].' for label metadata', 4); - $data = json_decode(file_get_contents('http://api.discogs.com/database/search?artist='.urlencode($album['artist']) - .'&release_title='.urlencode($album['title']).'&type=release'), true); - - if (!empty($data['results'])) { - $label = $data['results'][0]['label'][0]; - - dlog("Setting {$album['recordid']} label to {$label}", 2); - Database::getInstance()->query('UPDATE public.rec_record SET recordlabel=$1 WHERE recordid=$2', - array($label, $album['recordid'])); - } else { - dlog('No record label data improvement available for ' - .$album['recordid'], 4); - } - sleep(1); +class MyRadio_LabelFinderDaemon extends \MyRadio\MyRadio\MyRadio_Daemon +{ + /** + * If this method returns true, the Daemon host should run this Daemon. If it returns false, it must not. + * It is currently enabled because we have a lot of labels that needed filling in for Tracklisting. + * + * @return bool + */ + public static function isEnabled() + { + return Config::$d_LabelFinder_enabled; + } + + /** + * THE DISCOGS API IS RATE LIMITED TO ONE REQUEST PER SECOND. + */ + public static function run() + { + //Get 5 albums without labels + $albums = Database::getInstance()->fetchAll( + 'SELECT recordid, title, artist FROM public.rec_record + WHERE recordlabel=\'\' ORDER BY RANDOM() LIMIT 5' + ); + + foreach ($albums as $album) { + dlog('Checking record '.$album['recordid'].' for label metadata', 4); + $data = json_decode( + file_get_contents( + 'http://api.discogs.com/database/search?artist='.urlencode($album['artist']) + .'&release_title='.urlencode($album['title']).'&type=release' + ), + true + ); + + if (!empty($data['results'])) { + $label = $data['results'][0]['label'][0]; + + dlog("Setting {$album['recordid']} label to {$label}", 2); + Database::getInstance()->query( + 'UPDATE public.rec_record SET recordlabel=$1 WHERE recordid=$2', + [$label, $album['recordid']] + ); + } else { + dlog( + 'No record label data improvement available for ' + .$album['recordid'], + 4 + ); + } + sleep(1); + } } - } -} \ No newline at end of file +} diff --git a/src/Classes/Daemons/MyRadio_MemberSyncDaemon.php b/src/Classes/Daemons/MyRadio_MemberSyncDaemon.php old mode 100644 new mode 100755 index 600ee21f8..f0cbb7af7 --- a/src/Classes/Daemons/MyRadio_MemberSyncDaemon.php +++ b/src/Classes/Daemons/MyRadio_MemberSyncDaemon.php @@ -1,32 +1,44 @@ time() - 300) { - return; +class MyRadio_MemberSyncDaemon extends \MyRadio\MyRadio\MyRadio_Daemon +{ + public static function isEnabled() + { + return Config::$d_MemberSync_enabled; } - - $members = CoreUtils::callYUSU('ListMembers'); - - foreach ($members as $member) { - dlog('Checking YUSU Member '.$member['EmailAddress'], 4); - $result = MyRadio_User::findByEmail($member['EmailAddress']); - - if (empty($result)) { - dlog('Member '.$member['EmailAddress'].' does not exist.', 3); - } elseif ($member['Paid'] != null) { - dlog('Member '.$member['EmailAddress'].' matches '.$result->getID().'.', 4); - dlog('Setting '.$result->getID().' payment to '.Config::$membership_fee.'.', 3); - $result->setPayment(Config::$membership_fee); - } + + public static function run() + { + $hourkey = __CLASS__.'_last_run'; + if (self::getVal($hourkey) > time() - 300) { + return; + } + + $members = CoreUtils::callYUSU('ListMembers'); + + foreach ($members as $member) { + dlog('Checking YUSU Member '.$member['EmailAddress'], 4); + $result = MyRadio_User::findByEmail($member['EmailAddress']); + + if (empty($result)) { + dlog('Member '.$member['EmailAddress'].' does not exist.', 3); + } elseif ($member['Paid'] != null) { + dlog('Member '.$member['EmailAddress'].' matches '.$result->getID().'.', 4); + dlog('Setting '.$result->getID().' payment to '.Config::$membership_fee.'.', 3); + $result->setPayment(Config::$membership_fee); + } + } + + //Done + self::setVal($hourkey, time()); } - - //Done - self::setVal($hourkey, time()); - } -} \ No newline at end of file +} diff --git a/src/Classes/Daemons/MyRadio_PlaylistsDaemon.php b/src/Classes/Daemons/MyRadio_PlaylistsDaemon.php old mode 100644 new mode 100755 index c71640c5a..1f724657c --- a/src/Classes/Daemons/MyRadio_PlaylistsDaemon.php +++ b/src/Classes/Daemons/MyRadio_PlaylistsDaemon.php @@ -1,71 +1,355 @@ - * @package MyRadio_Tracklist - * @uses \Database - * + * + * @uses \Database */ -class MyRadio_PlaylistsDaemon extends MyRadio_Daemon { +class MyRadio_PlaylistsDaemon extends \MyRadio\MyRadio\MyRadio_Daemon +{ + private static $locks = []; - public static function isEnabled() { + public static function isEnabled() + { return Config::$d_Playlists_enabled; } - public static function run() { - $hourkey = __CLASS__ . '_last_run_hourly'; - if (self::getVal($hourkey) > time() - 3500) { + public static function run($force = false) + { + $hourkey = __CLASS__.'_last_run_hourly'; + if (!$force && self::getVal($hourkey) > time() - 3500) { return; } - self::updateMostPlayedPlaylist(); self::updateNewestUploadsPlaylist(); + self::updateRandomTracksPlaylist(); + self::updateLastFMGeoPlaylist(); + self::updateLastFMTopPlaylist(); + self::updateLastFMHypePlaylist(); //Done self::setVal($hourkey, time()); } - private static function updateMostPlayedPlaylist() { - $pobj = iTones_Playlist::getInstance('semantic-auto'); - $lockstr = $pobj->acquireOrRenewLock(null, MyRadio_User::getInstance(Config::$system_user)); + private static function playlistGenPrepare($playlistid) + { + $playlist = iTones_Playlist::getInstance($playlistid); + $lock = $playlist->acquireOrRenewLock(null, MyRadio_User::getInstance(Config::$system_user)); + self::$locks[$playlistid] = [ + $playlist, + $lock, + ]; + if ($lock === false) { + dlog('ERROR updating playlist: could not get lock on '.$playlistid, 3); + } + + return $lock !== false; + } + + private static function playlistGenCommit($playlistid, $data) + { + if (empty(self::$locks[$playlistid][1])) { + dlog('ERROR updating playlist: lock not acquired '.$playlistid, 3); - /** - * @todo This is 120 days for testing (It was Summer when I wrote this...) - */ - $most_played = MyRadio_TracklistItem::getTracklistStatsForBAPS(time() - (86400 * 120)); //Track play stats for last week + return false; + } - $playlist = array(); - for ($i = 0; $i < 20; $i++) { - if (!isset($most_played[$i])) { - break; //If there aren't that many, oh well. + if (empty($data)) { + dlog('Warning: Saving empty playlist '.$playlistid, 3); + } else { + dlog('Saving '.sizeof($data).' items to '.$playlistid, 5); + } + + self::$locks[$playlistid][0]->setTracks( + $data, + self::$locks[$playlistid][1], + null + ); + self::$locks[$playlistid][0]->releaseLock(self::$locks[$playlistid][1]); + self::$locks[$playlistid][1] = false; + } + + /** + * @param $data array of ['title': title, 'artist': artist, 'count': value] + * Where count is only required if $threshold is set + * @param $limit int The maximum number of matched tracks to return (0 == no limit) + * @param $threshold int The minimum value of `count` to consider + * @param $include_similar bool Whether to include similar tracks or just the track itself + */ + private static function dataSimilarIterator( + $data, + $limit = 0, + $threshold = null, + $include_similar = true + ) { + $playlist = []; + $count = 0; + foreach ($data as $item) { + if ($threshold === null or $item['count'] >= $threshold) { + $similar = self::getTrackAndSimilar( + $item['title'], + $item['artist'], + $include_similar + ); + + if (!empty($similar)) { + $playlist = array_merge($playlist, $similar); + ++$count; + if ($limit !== 0 && $count >= $limit) { + break; + } + } } - $track = MyRadio_Track::getInstance($most_played[$i]['trackid']); + } + + return $playlist; + } + + private static function getTrackAndSimilar($title, $artist, $include_similar) + { + //Try to find an exact match + $c = MyRadio_Track::findByOptions( + [ + 'title' => $title, + 'artist' => $artist, + 'limit' => 1, + 'digitised' => true, + 'precise' => true, + ] + ); + + //Try and find a not-so-exact match + if (empty($c)) { + $c = MyRadio_Track::findByOptions( + [ + 'title' => $title, + 'artist' => $artist, + 'limit' => 1, + 'digitised' => true, + 'precise' => false, + ] + ); + } + + //Whelp, nothing + if (empty($c)) { + return []; + } elseif ($include_similar) { + //Whoop, something! + $similar = $c[0]->getSimilar(); + dlog('Found '.sizeof($similar).' similar tracks for '.$c[0]->getTitle().' - '.$c[0]->getArtist(), 4); + // Unshift edits array in-place + array_unshift($similar, $c[0]); + + return $similar; + } else { + return $c; + } + } + + private static function trackCountListGenerator($tracks) + { + //Sort array by play count + arsort($tracks); + //Get the trackids out + $keys = array_keys($tracks); + $playlist = []; + //Take the top 20 from this list + for ($i = 0; $i < min(20, sizeof($tracks)); ++$i) { + $key = $keys[$i]; + $track = MyRadio_Track::getInstance($key); + //Ask last.fm for similar songs that are in our library $similar = $track->getSimilar(); - dlog('Found ' . sizeof($similar) . ' similar tracks for ' . $track->getID(), 4); - $playlist = array_merge($playlist, $similar); + dlog('Found '.sizeof($similar).' similar tracks for '.$track->getTitle().' - '.$track->getArtist(), 4); + //Add these to the playlist, along with the popular track $playlist[] = $track; + $playlist = array_merge($playlist, $similar); } - $pobj->setTracks(array_unique($playlist), $lockstr, null, MyRadio_User::getInstance(Config::$system_user)); - $pobj->releaseLock($lockstr); + return $playlist; } - private static function updateNewestUploadsPlaylist() { - $pobj = iTones_Playlist::getInstance('newest-auto'); - $lockstr = $pobj->acquireOrRenewLock(null, MyRadio_User::getInstance(Config::$system_user)); + private static function updateMostPlayedPlaylist() + { + if (self::playlistGenPrepare('semantic-auto')) { + /* + * Daytime Track play stats for last 14 days + */ + $most_played = []; + //Get track statistics for every daytime window + for ($i = 0; $i < 14; ++$i) { + $stats = MyRadio_TracklistItem::getTracklistStatsForBAPS( + strtotime("6am -{$i} days"), + strtotime("9pm -{$i} days") + ); + //Accumulate the results + foreach ($stats as $track) { + if (!isset($most_played[$track['trackid']])) { + $most_played[$track['trackid']] = 0; + } + $most_played[$track['trackid']] += $track['num_plays']; + } + } - $newest_tracks = NIPSWeb_AutoPlaylist::findByName('Newest Tracks')->getTracks(); + self::playlistGenCommit( + 'semantic-auto', + self::trackCountListGenerator($most_played) + ); + } - $pobj->setTracks($newest_tracks, $lockstr, null, MyRadio_User::getInstance(Config::$system_user)); - $pobj->releaseLock($lockstr); + //Aaaand repeat + if (self::playlistGenPrepare('semantic-spec')) { + /* + * Specialist Track play stats for last 14 days + */ + $most_played = []; + for ($i = 0; $i < 14; ++$i) { + $j = $i + 1; + $stats = MyRadio_TracklistItem::getTracklistStatsForBAPS( + strtotime("9pm -{$j} days"), + strtotime("6am -{$i} days") + ); + foreach ($stats as $track) { + if (!isset($most_played[$track['trackid']])) { + $most_played[$track['trackid']] = 0; + } + $most_played[$track['trackid']] += $track['num_plays']; + } + } + + self::playlistGenCommit( + 'semantic-spec', + self::trackCountListGenerator($most_played) + ); + } } -} \ No newline at end of file + private static function updateNewestUploadsPlaylist() + { + if (!self::playlistGenPrepare('newest-auto')) { + return; + } + self::playlistGenCommit( + 'newest-auto', + NIPSWeb_AutoPlaylist::findByName('Newest Tracks')->getTracks() + ); + } + + private static function updateRandomTracksPlaylist() + { + if (!self::playlistGenPrepare('random-auto')) { + return; + } + + self::playlistGenCommit( + 'random-auto', + NIPSWeb_AutoPlaylist::findByName('Random Tracks')->getTracks() + ); + } + + private static function updateLastFMGeoPlaylist() + { + if (!self::playlistGenPrepare('lastgeo-auto')) { + return; + } + + $data = json_decode( + file_get_contents( + 'https://ws.audioscrobbler.com/2.0/?method=geo.getTopTracks&api_key=' + .Config::$lastfm_api_key + .'&country='.Config::$lastfm_geo + .'&limit=151&format=json' + ), + true + ); + + $items = array_map( + function ($m) { + return [ + 'title' => $m['name'], + 'artist' => $m['artist']['name'], + ]; + }, + $data['tracks']['track'] + ); + + self::playlistGenCommit( + 'lastgeo-auto', + self::dataSimilarIterator($items, 0, null, false) + ); + } + + private static function updateLastFMTopPlaylist() + { + if (!self::playlistGenPrepare('lasttop-auto')) { + return; + } + + $data = json_decode( + file_get_contents( + 'https://ws.audioscrobbler.com/2.0/?method=chart.getTopTracks&api_key=' + .Config::$lastfm_api_key + .'&limit=151&format=json' + ), + true + ); + + $items = array_map( + function ($m) { + return [ + 'title' => $m['name'], + 'artist' => $m['artist']['name'], + ]; + }, + $data['tracks']['track'] + ); + + self::playlistGenCommit( + 'lasttop-auto', + self::dataSimilarIterator($items, 0, null, false) + ); + } + + private static function updateLastFMHypePlaylist() + { + if (!self::playlistGenPrepare('lasthype-auto')) { + return; + } + + $data = json_decode( + file_get_contents( + 'https://ws.audioscrobbler.com/2.0/?method=chart.getHypedTracks&api_key=' + .Config::$lastfm_api_key + .'&limit=151&format=json' + ), + true + ); + + $items = array_map( + function ($m) { + return [ + 'title' => $m['name'], + 'artist' => $m['artist']['name'], + ]; + }, + $data['tracks']['track'] + ); + + self::playlistGenCommit( + 'lasthype-auto', + self::dataSimilarIterator($items, 0, null, false) + ); + } +} diff --git a/src/Classes/Daemons/MyRadio_PodcastDaemon.php b/src/Classes/Daemons/MyRadio_PodcastDaemon.php old mode 100644 new mode 100755 index 960c9d01e..83e13995b --- a/src/Classes/Daemons/MyRadio_PodcastDaemon.php +++ b/src/Classes/Daemons/MyRadio_PodcastDaemon.php @@ -1,32 +1,37 @@ - * @version 20130817 - * @package MyRadio_Daemon */ -class MyRadio_PodcastDaemon extends MyRadio_Daemon { - /** - * If this method returns true, the Daemon host should run this Daemon. If it returns false, it must not. - * @return boolean - */ - public static function isEnabled() { return Config::$d_Podcast_enabled; } - - public static function run() { - dlog('Checking for pending Podcasts...', 4); - $podcasts = MyRadio_Podcast::getPending(); - - if (!empty($podcasts)) { - //Encode the first podcast. - dlog('Converting Podcast '.$podcasts[0]->getID().'...', 3); - $podcasts[0]->convert(); - dlog('Converstion complete.', 3); +class MyRadio_PodcastDaemon extends \MyRadio\MyRadio\MyRadio_Daemon +{ + /** + * If this method returns true, the Daemon host should run this Daemon. If it returns false, it must not. + * + * @return bool + */ + public static function isEnabled() + { + return Config::$d_Podcast_enabled; + } + + public static function run() + { + $podcasts = MyRadio_Podcast::getPending(); + + if (!empty($podcasts)) { + //Encode the first podcast. + dlog('Converting Podcast '.$podcasts[0]->getMeta('title').'...', 3); + $podcasts[0]->convert(); + dlog('Conversion complete.', 3); + } } - } -} \ No newline at end of file +} diff --git a/src/Classes/Daemons/MyRadio_StatsGenDaemon.php b/src/Classes/Daemons/MyRadio_StatsGenDaemon.php old mode 100644 new mode 100755 index 86aa5b30f..f3259b4af --- a/src/Classes/Daemons/MyRadio_StatsGenDaemon.php +++ b/src/Classes/Daemons/MyRadio_StatsGenDaemon.php @@ -1,86 +1,101 @@ time() - 3500) { - return; - } - - //Generate Training Graph - self::generateTrainingGraph(); - - //Do dailies? - if (self::getVal($daykey) <= time() - 86300) { - - self::generateJukeboxReport(); - - self::setVal($daykey, time()); - } - - //Done - self::setVal($hourkey, time()); - } - - private static function generateTrainingGraph() { - $trained = MyRadio_User::findAllTrained(); - $demoed = MyRadio_User::findAllDemoed(); - $trainers = MyRadio_User::findAllTrainers(); - - $dotstr = 'digraph { overlap=false; splines=false; '; - - foreach ($trained as $user) { - $dotstr .= '"'.$user->getEmail().'" -> "'.$user->getStudioTrainedBy()->getEmail().'"; '; +namespace MyRadio\Daemons; + +use MyRadio\Config; +use MyRadio\MyRadio\CoreUtils; +use MyRadio\ServiceAPI\MyRadio_User; +use MyRadio\ServiceAPI\MyRadio_TracklistItem; +use MyRadio\ServiceAPI\MyRadio_List; +use MyRadio\ServiceAPI\MyRadio_TrainingStatus; +use MyRadio\MyRadioEmail; + +class MyRadio_StatsGenDaemon extends \MyRadio\MyRadio\MyRadio_Daemon +{ + public static function isEnabled() + { + return Config::$d_StatsGen_enabled; } - - //Red for demos - $dotstr .= 'edge [color=red]; '; - - foreach ($demoed as $user) { - $dotstr .= '"'.$user->getEmail().'" -> "'.$user->getStudioDemoedBy()->getEmail().'"; '; + + public static function run() + { + $hourkey = __CLASS__.'_last_run_hourly'; + $daykey = __CLASS__.'_last_run_daily'; + if (self::getVal($hourkey) > time() - 3500) { + return; + } + + //Generate Training Graph + self::generateTrainingGraph(); + + //Do dailies? + if (self::getVal($daykey) <= time() - 86300) { + self::generateJukeboxReport(); + + self::setVal($daykey, time()); + } + + //Done + self::setVal($hourkey, time()); } - - //Green for trainers - $dotstr .= 'edge [color=green]; '; - - foreach ($trainers as $user) { - $dotstr .= '"'.$user->getEmail().'" -> "'.$user->getTrainerTrainedBy()->getEmail().'"; '; + + private static function generateTrainingGraph() + { + $outputbase = __DIR__ . '/../../Public/img/stats_training_'; + $statuses = MyRadio_TrainingStatus::getAll(); + + foreach ($statuses as $status) { + $awards = $status->getAwardedTo(); + $dotstr = 'digraph { overlap=false; splines=false; '; + + foreach ($awards as $award) { + $by = $award->getAwardedBy()->getEmail(); + $to = $award->getAwardedTo()->getEmail(); + $date = date('Y-m-d', $award->getAwardedTime()); + $dotstr .= '"' . $by . '" -> "' . $to . '" [label="' . $date . '"]; '; + } + + $dotstr .= '}'; + + passthru("echo '$dotstr' | /usr/bin/env sfdp -Tsvg > $outputbase" . $status->getID() . '.svg'); + } } - - $dotstr .= '}'; - - passthru("echo '$dotstr' | /usr/local/bin/sfdp -Tsvg > ".__DIR__.'/../../Public/img/stats_training.svg'); - } - - /** - * Once a day, this emails the Reporting List with a table of all tracks iTones has played in the last 24 hours - * It's useful to see if it's got into bad habits such as playing the same song 3 million times. - */ - private static function generateJukeboxReport() { - //Review of whole week on Sundays - if (date('N') == 7) $info = MyRadio_TracklistItem::getTracklistStatsForJukebox(time()-(86400*7)); - else $info = MyRadio_TracklistItem::getTracklistStatsForJukebox(time()-86400); - - $totalplays = 0; - $totaltracks = 0; - $totaltime = 0; - $table = ''; - - foreach ($info as $row) { - $table .= ''."\r\n"; - $totalplays += $row['num_plays']; - $totaltracks++; - $totaltime += $row['total_playtime']; + + /** + * Once a day, this emails the Reporting List with a table of all tracks iTones has played in the last 24 hours + * It's useful to see if it's got into bad habits such as playing the same song 3 million times. + */ + private static function generateJukeboxReport() + { + //Review of whole week on Sundays + if (date('N') == 7) { + $info = MyRadio_TracklistItem::getTracklistStatsForJukebox(time() - (86400 * 7)); + } else { + $info = MyRadio_TracklistItem::getTracklistStatsForJukebox(time() - 86400); + } + + $totalplays = 0; + $totaltracks = 0; + $totaltime = 0; + $table = '
Number of PlaysTitleTotal PlaytimePlaylist Membership
'.$row['num_plays'].''.$row['title'].''.$row['total_playtime'].'' - . $row['in_playlists'] .'
'; + $table .= ''; + + foreach ($info as $row) { + $table .= ''."\r\n"; + $totalplays += $row['num_plays']; + ++$totaltracks; + $totaltime += $row['total_playtime']; + } + + $table .= ''; + $table .= '
Number of PlaysTitleTotal PlaytimePlaylist Membership
'.$row['num_plays'].''.$row['title'].'' + .$row['total_playtime'].''.$row['in_playlists'].'
'.$totalplays.''.$totaltracks + .''.CoreUtils::intToTime($totaltime).'
'; + + MyRadioEmail::sendEmailToList( + MyRadio_List::getByName(Config::$reporting_list), + 'Jukebox Playout Report', + $table + ); } - - $table .= ''.$totalplays.''.$totaltracks.''.CoreUtils::intToTime($totaltime).''; - $table .= ''; - - MyRadioEmail::sendEmailToList(MyRadio_List::getByName(Config::$reporting_list), 'Jukebox Playout Report', $table); - } -} \ No newline at end of file +} diff --git a/src/Classes/Daemons/MyRadio_TrackAndTraceDaemon.php b/src/Classes/Daemons/MyRadio_TrackAndTraceDaemon.php new file mode 100644 index 000000000..9b70ebadb --- /dev/null +++ b/src/Classes/Daemons/MyRadio_TrackAndTraceDaemon.php @@ -0,0 +1,93 @@ + time() - 604700) { + return; + } + + try { + self::generateTrackAndTraceReport(); + } finally { + // Done + self::setVal($weekkey, time()); + } + } + + private static function generateTrackAndTraceReport() + { + $table = ""; + $table .= ""; + + $data = []; + $no_track = MyRadio_Timeslot::getLocationName(5); //WebStudio + + foreach (MyRadio_Season::getAllSeasonsInLatestTerm() as $season) { + foreach ($season->getAllTimeslots() as $timeslot) { + if ($timeslot->getStartTime() < time() + && $timeslot->getStartTime() > time() - 604800 + ) { + foreach ($timeslot->getSigninInfo() as $info) { + if (isset($info["location"]) && $info["location"] != $no_track) { + if (isset($info["user"])) { + $eduroam = $info["user"]->getEduroam(); + $data[] = [ + "type" => "URY Member", + "info" => $info["user"]->getName() . ($eduroam ? " ($eduroam)" : ""), + "location" => $info["location"], + "time" => CoreUtils::happyTime($info["time"]), + "unix" => $info["time"] + ]; + } elseif ($info["guest_info"]) { + $data[] = [ + "type" => "Guest", + "info" => nl2br($info["guest_info"]), + "location" => $info["location"], + "time" => CoreUtils::happyTime($info["time"]), + "unix" => $info["time"] + ]; + } + } + } + } + } + } + + usort($data, function ($a, $b) { + return $a["unix"] - $b["unix"]; + }); + + foreach ($data as $row) { + $table .= "\r\n"; + } + + $table .= "
TypeInformationLocationTime
" . $row["type"] + . "" . $row["info"] + . "" . $row["location"] + . "" . $row["time"] + . "
"; + + MyRadioEmail::sendEmailToList( + MyRadio_List::getByName("Management Team"), + "Track and Trace Report", + $table + ); + } +} diff --git a/src/Classes/Database.php b/src/Classes/Database.php index 684caf8c1..5c86aa558 100644 --- a/src/Classes/Database.php +++ b/src/Classes/Database.php @@ -1,247 +1,295 @@ + * This singleton class handles actual database connection. + * + * This is a Critical include! + * * @depends Config - * @package MyRadio_Core */ -class Database { - - /** - * Stores the singleton instance of the Database object - * @var Database - */ - private static $me; - - /** - * Stores the resource id of the connection to the PostgreSQL database - * @var Resource - */ - protected $db; - - /** - * Stores the number of queries executed - * @var int - */ - private $counter = 0; - - /** - * Rememebers if a transaction is in progress. - * @var bool - */ - private $in_transaction = false; - - /** - * Constructs the singleton database connector - */ - private function __construct() { - $this->db = pg_connect('host=' . Config::$db_hostname . ' port=5432 dbname='.Config::$db_name.' - user=' . Config::$db_user . ' password=' . Config::$db_pass); - if (!$this->db) { - //Database isn't working. Throw an EVERYTHING IS BROKEN Exception - throw new MyRadioException('Database Connection Failed!', - MyRadioException::FATAL); +class Database +{ + /** + * Stores the singleton instance of the Database object. + * + * @var Database + */ + private static $me; + + /** + * Stores the resource id of the connection to the PostgreSQL database. + * + * @var resource + */ + protected $db; + + /** + * Stores the number of queries executed. + * + * @var int + */ + private $counter = 0; + + /** + * Rememebers if a transaction is in progress. + * + * @var bool + */ + private $in_transaction = false; + + /** + * Constructs the singleton database connector. + */ + private function __construct() + { + $this->db = @pg_connect( + 'host='.Config::$db_hostname + .' port=5432 dbname='.Config::$db_name + .' user='.Config::$db_user + .' password='.Config::$db_pass + ); + if (!$this->db) { + //Database isn't working. Throw an EVERYTHING IS BROKEN Exception + throw new MyRadioException( + 'Database Connection Failed!', + MyRadioException::FATAL + ); + } } - } - - /** - * Attempts to reset connection to the database server - */ - public function reconnect() { - return pg_connection_reset($this->db); - } - - /** - * Check if the connection to the database server is still alive. - * @return boolean Whether the connection is OK. - */ - public function status() { - return pg_connection_status($this->db) === PGSQL_CONNECTION_OK; - } - - /** - * Generic function that just runs a pg_query_params - * @param String $sql The query string to execute - * @param Array $params Parameters for the query - * @param bool $rollback Deprecated. - * @return A pg result reference - * @throws MyRadioException If the query fails - * @assert ('SELECT * FROM public.tableethatreallydoesntexist') throws MyRadioException - * @assert ('SELECT * FROM public.member') != false - */ - public function query($sql, $params = array(), $rollback = false) { - if ($sql === 'BEGIN') { - $this->in_transaction = true; - } elseif ($sql === 'COMMIT' or $sql === 'ROLLBACK') { - $this->in_transaction = false; + + /** + * Attempts to reset connection to the database server. + */ + public function reconnect() + { + return pg_connection_reset($this->db); } - - foreach ($params as $k => $v) { - if (is_bool($v)) { - $params[$k] = ($v? 't' : 'f'); - } + + /** + * Check if the connection to the database server is still alive. + * + * @return bool Whether the connection is OK. + */ + public function status() + { + return pg_connection_status($this->db) === PGSQL_CONNECTION_OK; } - - if (isset($_REQUEST['dbdbg']) && CoreUtils::hasPermission(AUTH_SHOWERRORS)) { - //Debug output - echo $sql.' '.print_r($params,true).'
'; + + /** + * Generic function that just runs a pg_query_params. + * + * @param string $sql The query string to execute + * @param array $params Parameters for the query + * @param bool $rollback Deprecated. + * + * @return A pg result reference + * + * @throws MyRadioException If the query fails + * @assert ('SELECT * FROM public.tableethatreallydoesntexist') throws MyRadioException + * @assert ('SELECT * FROM public.member') != false + */ + public function query($sql, $params = [], $rollback = false) + { + if ($sql === 'BEGIN') { + $this->in_transaction = true; + } elseif ($sql === 'COMMIT' or $sql === 'ROLLBACK') { + $this->in_transaction = false; + } + + foreach ($params as $k => $v) { + if (is_bool($v)) { + $params[$k] = ($v ? 't' : 'f'); + } + if (is_array($v) || is_object($v)) { + throw new MyRadioException( + 'Query failure: '.$sql.'
' + .'Params: '.var_export($params, true) + .'Tried to pass array to query
', + 400 + ); + } + } + + if (defined('DB_PROFILE')) { + //Debug output + echo $sql.' '.print_r($params, true).'...'; + $timer = microtime(true); + } + + if (empty($params)) { + pg_send_query($this->db, $sql); + } else { + pg_send_query_params($this->db, $sql, $params); + } + $result = pg_get_result($this->db); + if ($result === FALSE) { + $errmsg = pg_last_error($this->db); + } else { + $errmsg = pg_result_error($result); + } + if ($errmsg != "") { + if ($this->in_transaction) { + pg_query($this->db, 'ROLLBACK'); + } + throw new MyRadioException( + 'Query failure: '.$sql.'
' + .'Params: '.var_export($params, true).'
' + .$errmsg, + 500 + ); + } + ++$this->counter; + + if (defined('DB_PROFILE')) { + echo(microtime(true) - $timer)."s\n"; + } + + return $result; } - - $result = @pg_query_params($this->db, $sql, $params); - if (!$result) { - if ($this->in_transaction) { - pg_query($this->db, 'ROLLBACK'); - } - throw new MyRadioException('Query failure: ' . $sql . '
' - . pg_errormessage($this->db).'
Params: '.print_r($params,true), 500); + + /** + * Equates to a pg_num_rows($result). + * + * @param resource $result a reference to a postgres result set + * + * @return int The number of rose in the result set + */ + public function numRows($result) + { + return pg_num_rows($result); } - $this->counter++; - return $result; - } - - /** - * Equates to a pg_num_rows($result) - * @param Resource $result a reference to a postgres result set - * @return int The number of rose in the result set - */ - public function num_rows($result) { - return pg_num_rows($result); - } - - /** - * The most commonly used database function - * Equates to a pg_fetch_all(pg_query) - * @param String|Resource $sql The query string to execute - * or a psql result resource - * @param Array $params Parameters for the query - * @return Array An array of result rows (potentially empty) - * @throws MyRadioException - */ - public function fetch_all($sql, $params = array()) { - if (is_resource($sql)) { - return pg_fetch_all($sql); - } elseif (is_string($sql)) { - try { - $result = $this->query($sql, $params); - } catch (MyRadioException $e) { - return array(); - } - if (pg_num_rows($result) === 0) { - return array(); - } - return pg_fetch_all($result); - } else { - throw new MyRadioException('Invalid Request for $sql'); + + /** + * The most commonly used database function + * Equates to a pg_fetch_all(pg_query). + * + * @param string|resource $sql The query string to execute or a psql result resource + * @param array $params Parameters for the query + * + * @return array An array of result rows (potentially empty) + * + * @throws MyRadioException + */ + public function fetchAll($sql, $params = []) + { + if (is_resource($sql)) { + return pg_fetch_all($sql); + } elseif (is_string($sql)) { + try { + $result = $this->query($sql, $params); + } catch (MyRadioException $e) { + return []; + } + if (pg_num_rows($result) === 0) { + return []; + } + + return pg_fetch_all($result); + } else { + throw new MyRadioException('Invalid Request for $sql'); + } + } + + /** + * Equates to a pg_fetch_assoc(pg_query). Returns the first row. + * + * @param string $sql The query string to execute or a psql result resource + * @param array $params Parameters for the query + * + * @return array The requested result row, or an empty array on failure + * + * @throws MyRadioException + */ + public function fetchOne($sql, $params = []) + { + if (!is_resource($sql)) { + try { + $sql = $this->query($sql, $params); + } catch (MyRadioException $e) { + return []; + } + } + + return pg_fetch_assoc($sql); + } + + /** + * Equates to a pg_fetch_all_columns(pg_query,0). Returns all first column entries. + * + * @param string $sql The query string to execute + * @param array $params Paramaters for the query + * @param bool $rollback deprecated. + * + * @return array The requested result column, or an empty array on failure + * + * @throws MyRadioException + */ + public function fetchColumn($sql, $params = [], $rollback = false) + { + try { + $result = $this->query($sql, $params, $rollback); + } catch (MyRadioException $e) { + // TODO: temporary, uncomment this - marks.polakovs + //return []; + throw $e; + } + if (pg_num_rows($result) === 0) { + return []; + } + + return pg_fetch_all_columns($result, 0); + } + + /** + * Used to create the object, or return a reference to it if it already exists. + * + * @return Database One of these things + */ + public static function getInstance() + { + if (!self::$me) { + self::$me = new self(); + } + + return self::$me; } - } - - /** - * Equates to a pg_fetch_assoc(pg_query). Returns the first row - * @param String $sql The query string to execute - * @param Array $params Paramaters for the query - * @return Array The requested result row, or an empty array on failure - * @throws MyRadioException - */ - public function fetch_one($sql, $params = array()) { - try { - $result = $this->query($sql, $params); - } catch (MyRadioException $e) { - return array(); + + /** + * Prevent copies being unintentionally made. + * + * @throws MyRadioException + */ + public function __clone() + { + throw new MyRadioException('Attempted to clone a singleton'); + } + + public function intervalToTime($interval) + { + return strtotime('1970-01-01 '.$interval.'+00'); } - return pg_fetch_assoc($result); - } - - /** - * Equates to a pg_fetch_all_columns(pg_query,0). Returns all first column entries - * @param String $sql The query string to execute - * @param Array $params Paramaters for the query - * @param bool $rollback deprecated. - * @return Array The requested result column, or an empty array on failure - * @throws MyRadioException - */ - public function fetch_column($sql, $params = array(), $rollback = false) { - try { - $result = $this->query($sql, $params, $rollback); - } catch (MyRadioException $e) { - return array(); + + public function getCounter() + { + return $this->counter; } - if (pg_num_rows($result) === 0) { - return array(); + + public function resetCounter() + { + $this->counter = 0; } - return pg_fetch_all_columns($result, 0); - } - - /** - * Used to create the object, or return a reference to it if it already exists - * @return Database One of these things - */ - public static function getInstance() { - if (!self::$me) { - self::$me = new self(); + + public function clean($val) + { + return pg_escape_string($val); } - return self::$me; - } - - /** - * Prevent copies being unintentionally made - * @throws MyRadioException - */ - public function __clone() { - throw new MyRadioException('Attempted to clone a singleton'); - } - - /** - * Converts a postgresql array to a php array - * json_decode *nearly* works in some cases, but this tends to be more reliable - * - * Based on http://stackoverflow.com/questions/3068683/convert-postgresql-array-to-php-array - */ - public function decodeArray($text) { - $limit = strlen($text) - 1; - $output = array(); - $offset = 1; - - if ('{}' != $text) { - do { - if ('{' != $text{$offset}) { - preg_match("/(\\{?\"([^\"\\\\]|\\\\.)*\"|[^,{}]+)+([,}]+)/", $text, $match, 0, $offset); - $offset += strlen($match[0]); - $output[] = ( '"' != $match[1]{0} ? $match[1] : stripcslashes(substr($match[1], 1, -1)) ); - if ('},' == $match[3]) { - return $offset; - } - } else { - $offset = pg_array_parse($text, $output[], $limit, $offset + 1); - } - } - while ($limit > $offset); + + public function getInTransaction() + { + return $this->in_transaction; } - return $output; - } - - public function intervalToTime($interval) { - return strtotime('1970-01-01 '.$interval.'+00'); - } - - public function getCounter() { - return $this->counter; - } - - public function resetCounter() { - $this->counter = 0; - } - - public function clean($val) { - return pg_escape_string($val); - } - -} \ No newline at end of file +} diff --git a/src/Classes/MemcachedProvider.php b/src/Classes/MemcachedProvider.php new file mode 100644 index 000000000..9365cc1e2 --- /dev/null +++ b/src/Classes/MemcachedProvider.php @@ -0,0 +1,195 @@ +enable = $enable; + if ($enable) { + if (!class_exists('\Memcached')) { + //Functions not available. If this is caught upstream, just disable + trigger_error('Cache is enabled but selected CacheProvider does not have required prerequisites'); + $this->enable = false; + } elseif (empty($servers)) { + trigger_error('No Memcached servers are configured.'); + $this->enable - false; + } else { + $this->memcached = new Memcached(); + $this->memcached->addServers($servers); + } + } + } + + /** + * Stores an object in Memcached. + * + * @param string $key The unique name of the object to store. Ideally, this would use myradio_{module}_{name} + * @param mixed $value The data to store + * @param int $expires The number of seconds this cache entry is valid for.Default is value of + * MyRadio_Config::$cache_default_timeout + * + * @return bool Whether the operation was successful (returns false if caching disabled) + * @assert ('myradio_core_test', 'test value', 0) == true + * + * @todo Consider using Memcached::cas + */ + public function set($key, $value, $expires = 0) + { + if (!$this->enable) { + return false; + } + + if ($expires === 0) { + $expires = \MyRadio\Config::$cache_default_timeout; + } + // Values > 30 days are assumed to be epoch times + // http://php.net/manual/en/memcached.expiration.php + if ($expires > 60 * 60 * 24 * 30) { + $expires = time() + $expires; + } + + return $this->memcached->set($this->getKeyPrefix().$key, $value, $expires); + } + + /** + * Reads a previously stored value from Memcached and returns it. + * + * @param string $key The unique name of the object to fetch + * + * @return mixed The value of the store, or false on failure + * @assert ('myradio_core_test') == 'test value' + */ + public function get($key) + { + if (!$this->enable) { + return false; + } + + return $this->memcached->get($this->getKeyPrefix().$key); + } + + /** + * Fetch all objects from the cache assosicated with the given keys. + * + * @param array $keys cache keys to be fetched + * + * @return mixed[] array of objects relating to provided keys + */ + public function getAll($keys) + { + if (!$this->enable) { + return []; + } + + $prefix = $this->getKeyPrefix(); + foreach ($keys as &$key) { + $key = $prefix.$key; + } + + //Don't use $this->get as it'll append the prefix twice + $result = $this->memcached->getMulti($keys); + + return $result; + } + + /** + * Deletes a previously stored value from Memcached. + * + * @param string $key The unique name of the object to delete + * + * @return bool Returns whether the operaion was a success + * @assert ('myradio_core_test') == true + */ + public function delete($key) + { + if (!$this->enable) { + return false; + } + + return $this->memcached->delete($this->getKeyPrefix().$key); + } + + /** + * This will completely wipe Memcached. This is not limited to MyRadio items. + */ + public function purge() + { + if (!$this->enable) { + return false; + } + + return $this->memcached->flush(); + } + + /** + * Returns the Singleton instance of this class, creating it if necessary. + * + * @return MemcachedProvider + */ + public static function getInstance() + { + if (!self::$me) { + self::$me = new self( + Config::$cache_enable, + Config::$cache_memcached_servers + ); + } + + return self::$me; + } + + /** + * Prevent copies being unintentionally made. + * + * @throws MyRadioException + */ + public function __clone() + { + throw new \MyRadio\MyRadioException('Attempted to clone a singleton'); + } + + private function getKeyPrefix() + { + return 'MyRadioCache-'; + } +} diff --git a/src/Classes/MyRadio/AuthUtils.php b/src/Classes/MyRadio/AuthUtils.php new file mode 100644 index 000000000..8881624de --- /dev/null +++ b/src/Classes/MyRadio/AuthUtils.php @@ -0,0 +1,524 @@ + description mappings. + * + * @var array + */ + private static $typeid_descr = []; + + /** + * Sets up the Authentication Constants. + * + * @assert () == null + */ + public static function setUpAuth() + { + if (self::$auth_cached) { + return; + } + + $db = Database::getInstance(); + $result = $db->fetchAll('SELECT typeid, phpconstant, descr FROM l_action'); + foreach ($result as $row) { + define($row['phpconstant'], $row['typeid']); + self::$typeid_descr[$row['typeid']] = $row['descr']; + } + + self::$auth_cached = true; + } + + /** + * Returns the Actions and API Endpoints that utilise a given type. + * + * @param int $typeid + * + * @return [[action,...], [api method,...]] + */ + public static function getAuthUsage($typeid) + { + $db = Database::getInstance(); + $actions = $db->fetchAll( + 'SELECT modules.name AS module, actions.name AS action + FROM myury.act_permission + LEFT JOIN myury.modules USING (moduleid) + LEFT JOIN myury.actions USING (actionid) + WHERE typeid=$1', + [$typeid] + ); + + $apis = $db->fetchAll( + 'SELECT api_name, method_name + FROM myury.api_method_auth + LEFT JOIN myury.api_class_map USING (class_name) + WHERE typeid=$1', + [$typeid] + ); + + return [$actions, $apis]; + } + + /** + * Gets the description (friendly name) of the given permission. + * + * @param int $typeid + * + * @return string + */ + public static function getAuthDescription($typeid) + { + self::setUpAuth(); + + return self::$typeid_descr[$typeid]; + } + + /** + * Checks using cached permissions whether the current member has the specified permission. + * + * @param int $permission The ID of the permission, resolved by using an AUTH_ constant + * + * @return bool Whether the member has the requested permission + */ + public static function hasPermission($permission) + { + if (isset($_SESSION['memberid'])) { + return MyRadio_User::getInstance()->hasAuth($permission); + } else { + $apiCaller = MyRadio_Swagger2::getAPICaller(); + if (!empty($apiCaller)) { + return $apiCaller->hasAuth($permission); + } else { + return false; + } + } + } + + /** + * Checks if the user has the given permission. Or, alternatiely, if we are currently running CLI, reutrns true. + * + * @param int $permission A permission constant to check + */ + public static function requirePermission($permission) + { + if (php_sapi_name() === 'cli') { + return true; //Non-interactive version has God Rights. + } + if (!self::hasPermission($permission)) { + //Load the 403 controller and exit + require 'Controllers/Errors/403.php'; + exit; + } + } + + /** + * Checks if the user has the given permissions required for the given Module/Action combination. + * + * The query needs a little bit of explaining.
+ * The first two WHERE clauses just set up foreign key references - we're searching by name, not ID.
+ * The next two WHERE clauses return exact or wildcard matches for this Module/Action combination.
+ * The final two AND NOT phrases make sure it ignores wildcards that allow any access. + * + * @param string $module The Module to check permissions for + * @param string $action The Action to check permissions for + * @param bool $require If true, will die if the user does not have permission. If false, will just return false + * + * @return bool True on required or authorised, false on unauthorised + */ + public static function requirePermissionAuto($module, $action, $require = true) + { + self::setUpAuth(); + $db = Database::getInstance(); + + $result = $db->fetchColumn( + 'SELECT typeid FROM myury.act_permission + LEFT OUTER JOIN myury.modules ON act_permission.moduleid=modules.moduleid + LEFT OUTER JOIN myury.actions ON act_permission.actionid=actions.actionid + WHERE (myury.modules.name=$1 OR myury.act_permission.moduleid IS NULL) + AND (myury.actions.name=$2 OR myury.act_permission.actionid IS NULL) + AND NOT (myury.act_permission.actionid IS NULL AND myury.act_permission.typeid IS NULL) + AND NOT (myury.act_permission.moduleid IS NULL AND myury.act_permission.typeid IS NULL)', + [$module, $action] + ); + + //Don't allow empty result sets - throw an Exception as this is very very bad. + if (empty($result) && $require) { + throw new MyRadioException('There are no permissions defined for the '.$module.'/'.$action.' action!'); + } + + $authorised = false; + foreach ($result as $permission) { + //It only needs to match one + if ($permission === AUTH_NOLOGIN + || (self::hasPermission($permission) + && isset($_SESSION['auth_use_locked']) + && $_SESSION['auth_use_locked'] === false) + ) { + $authorised = true; + break; + } + } + + if (!$authorised && $require) { + //Requires login + if (!isset($_SESSION['memberid']) || (isset($_SESSION['auth_use_locked']) + && $_SESSION['auth_use_locked'] !== false)) { + $is_ajax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) + && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') + || empty($_SERVER['REMOTE_ADDR']); + if ($is_ajax) { + throw new MyRadioException('Login required', 401); + } else { + URLUtils::redirect('MyRadio', 'login', ['next' => $_SERVER['REQUEST_URI']]); + } + } else { + //Authenticated, but not authorized + require 'Controllers/Errors/403.php'; + } + exit; + } + + //Return true on required success, or whether authorised otherwise + return $require || $authorised; + } + + /** + * Returns a list of all currently defined permissions on MyRadio Service/Module/Action combinations. + * + * This has multiple UNIONS with similar queries so it gracefully deals with NULL values - the joins lose them. + * + * @todo Is there a nicer way of doing this? + * @todo Won't do null fields. Requires outer joins. + * + * @return array A 2D Array, where each second dimensions is as follows:
+ * action: The name of the Action page
+ * module: The name of the Module the action is in
+ * service: The name of the Service the module is in
+ * permission: The name of the permission applied to that Service/Module/Action combination
+ * actpermissionid: The unique ID of this Service/Module/Action combination + */ + public static function getAllActionPermissions() + { + return Database::getInstance()->fetchAll( + 'SELECT actpermissionid, + myury.services.name AS service, + myury.modules.name AS module, + myury.actions.name AS action, + public.l_action.descr AS permission + FROM myury.act_permission, myury.services, myury.modules, myury.actions, public.l_action + WHERE myury.act_permission.actionid=myury.actions.actionid + AND myury.act_permission.moduleid=myury.modules.moduleid + AND myury.act_permission.serviceid=myury.services.serviceid + AND myury.act_permission.typeid = public.l_action.typeid + + UNION + + SELECT actpermissionid, + myury.services.name AS service, + myury.modules.name AS module, + \'ALL ACTIONS\' AS action, + public.l_action.descr AS permission + FROM myury.act_permission, myury.services, myury.modules, public.l_action + WHERE myury.act_permission.moduleid=myury.modules.moduleid + AND myury.act_permission.serviceid=myury.services.serviceid + AND myury.act_permission.typeid = public.l_action.typeid + AND myury.act_permission.actionid IS NULL + + UNION + + SELECT actpermissionid, + myury.services.name AS service, + myury.modules.name AS module, + myury.actions.name AS action, + \'GLOBAL ACCESS\' AS permission + FROM myury.act_permission, myury.services, myury.modules, myury.actions + WHERE myury.act_permission.moduleid=myury.modules.moduleid + AND myury.act_permission.serviceid=myury.services.serviceid + AND myury.act_permission.actionid=myury.actions.actionid + AND myury.act_permission.typeid IS NULL + + ORDER BY service, module' + ); + } + + /** + * Returns an action permission with a MyRadio Service/Module/Action combination. + * + * This has multiple UNIONS with similar queries so it gracefully deals with NULL values - the joins lose them. + * + * @todo Is there a nicer way of doing this? + * @todo Won't do null fields. Requires outer joins. + * + * @return array A 2D Array, where each second dimensions is as follows:
+ * action: The name of the Action page
+ * module: The name of the Module the action is in
+ * service: The name of the Service the module is in
+ * permission: The name of the permission applied to that Service/Module/Action combination
+ * actpermissionid: The unique ID of this Service/Module/Action combination + */ + public static function getActionPermission($actPermissionID) + { + return Database::getInstance()->fetchOne( + 'SELECT actpermissionid, + myury.services.name AS service, + myury.modules.name AS module, + myury.actions.name AS action, + public.l_action.descr AS permission + FROM myury.act_permission, myury.services, myury.modules, myury.actions, public.l_action + WHERE myury.act_permission.actionid=myury.actions.actionid + AND myury.act_permission.moduleid=myury.modules.moduleid + AND myury.act_permission.serviceid=myury.services.serviceid + AND myury.act_permission.typeid = public.l_action.typeid + AND myury.act_permission.actpermissionid = $1 + + UNION + + SELECT actpermissionid, + myury.services.name AS service, + myury.modules.name AS module, + \'ALL ACTIONS\' AS action, + public.l_action.descr AS permission + FROM myury.act_permission, myury.services, myury.modules, public.l_action + WHERE myury.act_permission.moduleid=myury.modules.moduleid + AND myury.act_permission.serviceid=myury.services.serviceid + AND myury.act_permission.typeid = public.l_action.typeid + AND myury.act_permission.actionid IS NULL + AND myury.act_permission.actpermissionid = $1 + + UNION + + SELECT actpermissionid, + myury.services.name AS service, + myury.modules.name AS module, + myury.actions.name AS action, + \'GLOBAL ACCESS\' AS permission + FROM myury.act_permission, myury.services, myury.modules, myury.actions + WHERE myury.act_permission.moduleid=myury.modules.moduleid + AND myury.act_permission.serviceid=myury.services.serviceid + AND myury.act_permission.actionid=myury.actions.actionid + AND myury.act_permission.typeid IS NULL + AND myury.act_permission.actpermissionid = $1', + [$actPermissionID] + ); + } + + /** + * Returns a list of Permissions ready for direct use in a select MyRadioFormField. + * + * @return array A 2D Array matching the MyRadioFormField::TYPE_SELECT specification. + */ + public static function getAllPermissions() + { + return Database::getInstance()->fetchAll( + 'SELECT typeid AS value, descr AS text FROM public.l_action + ORDER BY descr ASC' + ); + } + + /** + * Returns a list of Officership roles assigned with a specific permission. + * + * @return array A 2D Array of [officers,trainingstatuses]. + */ + public static function getPermissionAssignedTo($typeid) + { + $db = Database::getInstance(); + $officers = $db->fetchAll( + 'SELECT officer.officerid AS officerid, officer.officer_name AS officer_name + FROM public.auth_officer + LEFT JOIN public.officer USING (officerid) + WHERE lookupid=$1', + [$typeid] + ); + $trainingStatuses = $db->fetchAll( + 'SELECT l_presenterstatus.presenterstatusid AS statusid, l_presenterstatus.descr AS statusname + FROM public.auth_trainingstatus + LEFT JOIN public.l_presenterstatus USING (presenterstatusid) + WHERE typeid=$1', + [$typeid] + ); + + return [$officers, $trainingStatuses]; + } + + /** + * udiff function for permission value equality. + * + * @param array $perm1 permission value & description + * @param array $perm2 permission value & description + * + * @return int comparison result + */ + private static function comparePermission($perm1, $perm2) + { + if ($perm1['value'] === $perm2['value']) { + return 0; + } elseif ($perm1['value'] < $perm2['value']) { + return -1; + } else { + return 1; + } + } + + /** + * Returns all permissions that are in $perms but not $diffPerms. + * + * @param $perms array of permissions + * @param $diffPerms array of permissions + * + * @return array all permissions not included in $perms + */ + public static function diffPermissions($perms, $diffPerms) + { + return array_udiff($perms, $diffPerms, 'self::comparePermission'); + } + + /** + * Add a new permission constant to the database. + * + * @param string $descr A useful friendly description of what this action means. + * @param string $constant /AUTH_[A-Z_]+/ + */ + public static function addPermission($descr, $constant) + { + $value = (int) Database::getInstance()->fetchColumn( + // This is for all intents and purposes an ON CONFLICT DO NOTHING, but if we did use that + // then the RETURNING wouldn't return the ID which we need, hence the no-op UPDATE. + 'INSERT INTO public.l_action (descr, phpconstant) + VALUES ($1, $2) ON CONFLICT (phpconstant) DO UPDATE SET descr = $1 RETURNING typeid', + [$descr, $constant] + )[0]; + define($constant, $value); + + return $value; + } + + /** + * Assigns a permission to a command. Note arguments are the integer IDs + * NOT the String names. + * + * @param int $module The module ID + * @param int $action The action ID + * @param int $permission The permission typeid + */ + public static function addActionPermission($module, $action, $permission) + { + $db = Database::getInstance(); + $db->query( + 'INSERT INTO myury.act_permission (serviceid, moduleid, actionid, typeid) + VALUES ($1, $2, $3, $4) + ON CONFLICT (serviceid, moduleid, actionid, typeid) DO NOTHING', + [Config::$service_id, $module, $action, $permission] + ); + } + + /** + * Deletes action permissions. + * + * @param int $actPermissionID The action permission ID. + */ + public static function removeActionPermission($actPermissionID) + { + $db = Database::getInstance(); + $db->query( + 'DELETE FROM myury.act_permission WHERE + actpermissionid = $1', + [$actPermissionID] + ); + } + + /** + * Tests whether the given username or password are valid against a provider + * (and the right provider if needed). + * + * This is a far more basic version of the full Controllers/login.php system, + * not verifying if the user needs to take an action first. + * It does, however, update the User's last login time.. + * You MUST use POST with this - otherwise the credentials will turn up in + * access logs. + * + * @param string $user + * @param string $pass + * + * @return MyRadio_User|false + * + * @api POST + */ + public static function testCredentials($user, $pass) + { + //Make a best guess at the user account + //This way we can skip authenticators if they have one set + $u = MyRadio_User::findByEmail($user); + if ($u instanceof MyRadio_User && $u->getAuthProvider() !== null) { + $authenticators = [$u->getAuthProvider()]; + } else { + $authenticators = Config::$authenticators; + } + + //Iterate over each authenticator + foreach ($authenticators as $authenticator) { + $a = new $authenticator(); + $result = $a->validateCredentials($user, $pass); + if ($result instanceof MyRadio_User) { + if (Config::$single_authenticator + && $result->getAuthProvider() !== null + && $result->getAuthProvider() !== $authenticator + ) { + //This is the wrong authenticator for the user + continue; + } else { + $result->updateLastLogin(); + + return $result; + } + } + } + + return false; + } + + /** + * Verify that the recaptcha response is valid. + * + * @param string $response g-recaptcha-response from the widget + * @param string $addr Remote IP address of response + * + * @return Bool/Array true if valid, array of errors if not + */ + public static function verifyRecaptcha($response, $addr = null) + { + $recaptcha = new ReCaptcha(Config::$recaptcha_private_key); + + $resp = $recaptcha->verify($response, $addr); + + if ($resp->isSuccess()) { + return true; + } else { + return $resp->getErrorCodes(); + } + } +} diff --git a/src/Classes/MyRadio/CoreUtils.php b/src/Classes/MyRadio/CoreUtils.php index 0a672f1ca..7dda998a1 100644 --- a/src/Classes/MyRadio/CoreUtils.php +++ b/src/Classes/MyRadio/CoreUtils.php @@ -1,73 +1,60 @@ - * @version 20140102 - * @package MyRadio_Core - * @todo Factor out permission code into a seperate class? */ -class CoreUtils { - - /** - * This stores whether the Permissions have been defined to prevent re-defining, causing errors and wasting time - * Once setUpAuth is run, this is set to true to prevent subsequent runs - * @var boolean - */ - private static $auth_cached = false; - private static $svc_version_cache = array(); - +class CoreUtils +{ /** - * Stores the result of CoreUtils::getAcademicYear - * + * Stores the result of CoreUtils::getAcademicYear. + * * This cut 8k queries off of loading one test page... - * + * * @var int */ private static $academicYear; /** - * Stores permission typeid => description mappings - * @var Array - */ - private static $typeid_descr = []; - - /** - * Stores actionid => uri mappings of custom web addresses (e.g. /myury/iTones/default gets mapped to /itones) - * @var Array - */ - private static $custom_uris = array(); - - /** - * Stores module name => id mappings to reduce query load - they are initialised once and stored - * @var Array + * Stores module name => id mappings to reduce query load - they are initialised once and stored. + * + * @var array */ - private static $module_ids = array(); + private static $module_ids = []; /** - * Stores action name => id mappings to reduce query load - they are initialised once and stored - * @var Array + * Stores action name => id mappings to reduce query load - they are initialised once and stored. + * + * @var array */ - private static $action_ids = array(); + private static $action_ids = []; /** - * Checks whether a given Module/Action combination is valid - * @param String $module The module to check - * @param String $action The action to check. Default 'default' - * @return boolean Whether or not the request is valid + * Checks whether a given Module/Action combination is valid. + * + * @param string $module The module to check + * @param string $action The action to check. Default 'default' + * + * @return bool Whether or not the request is valid * @assert ('Core', 'default') === true * @assert ('foo', 'barthatdoesnotandwillnoteverexisteverbecauseitwouldbesilly') === false * @assert ('../foo', 'bar') === false * @assert ('foo', '../bar') === false */ - public static function isValidController($module, $action = null) { + public static function isValidController($module, $action = null) + { if ($action === null) { $action = Config::$default_action; } @@ -77,79 +64,95 @@ public static function isValidController($module, $action = null) { } catch (MyRadioException $e) { return false; } - /** - * This is better than file_exists because it ensures that the response is valid for a version which has the file - * when live does not - */ - return is_string(stream_resolve_include_path('Controllers/' . $module . '/' . $action . '.php')); + + /* This is better than file_exists because it ensures that the response + * is valid for a version which has the file when live does not */ + return is_string(stream_resolve_include_path('Controllers/'.$module.'/'.$action.'.php')); } /** - * Provides a template engine object compliant with TemplateEngine interface - * @return MyRadioTwig + * Provides a template engine object compliant with TemplateEngine interface. + * + * @return MyRadioTwig + * * @todo Make this generalisable for drop-in template engine replacements * @assert () !== false * @assert () !== null */ - public static function getTemplateObject() { - require_once 'Twig/Autoloader.php'; - Twig_Autoloader::register(); - require_once 'Classes/MyRadioTwig.php'; + public static function getTemplateObject() + { return new MyRadioTwig(); } /** - * Checks whether a requested action is safe - * @param String $action A module action - * @return boolean Whether the module is safe to be used on a filesystem + * Checks whether a requested action is safe. + * + * @param string $action A module action + * + * @return bool Whether the module is safe to be used on a filesystem + * * @throws MyRadioException Thrown if directory traversal detected * @assert ('safe!') === true * @assert ('../notsafe!') throws MyRadioException */ - public static function actionSafe($action) { + public static function actionSafe($action) + { if (strpos($action, '/') !== false) { //Someone is trying to traverse directories throw new MyRadioException('Directory Traversal Thrwated'); } + return true; } /** - * Formats pretty much anything into a happy, human readable date/time + * Formats pretty much anything into a happy, human readable date/time. + * * @param string $timestring Some form of time - * @param bool $time Whether to include Hours,Mins. Default yes - * @return String A happy time + * @param bool $time Whether to include Hours,Mins. Default yes + * + * @return string A happy time * @assert (40000) == '01/01/1970' */ - public static function happyTime($timestring, $time = true, $date = true) { - return date(($date ? 'd/m/Y' : '') . ($time && $date ? ' ' : '') . ($time ? 'H:i' : ''), is_numeric($timestring) ? $timestring : strtotime($timestring)); + public static function happyTime($timestring, $time = true, $date = true) + { + return date( + ($date ? 'd/m/Y' : '').($time && $date ? ' ' : '').($time ? 'H:i' : ''), + is_numeric($timestring) ? $timestring : strtotime($timestring) + ); } /** - * Formats a number into h:m:s format. + * Formats a number into hh:mm:ss format. + * * @param int $int - * @return String + * + * @return string */ - public static function intToTime($int) { + public static function intToTime($int) + { $hours = floor($int / 3600); - if ($hours === 0) { - $hours = null; - } else { - $hours = $hours . ':'; - } - $mins = floor(($int - ($hours * 3600)) / 60); $secs = ($int - ($hours * 3600) - ($mins * 60)); - return "$hours$mins:$secs"; + + //force 2 digit values for h,m and s. + $hours = sprintf("%02d", $hours); + $mins = sprintf("%02d", $mins); + $secs = sprintf("%02d", $secs); + + return "$hours:$mins:$secs"; } /** - * Returns a postgresql-formatted timestamp + * Returns a postgresql-formatted timestamp. + * * @param int $time The time to get the timestamp for. Default right now. - * @return String a timestamp + * + * @return string a timestamp * @assert (30) == '1970-01-01 00:00:30' */ - public static function getTimestamp($time = null) { + public static function getTimestamp($time = null) + { if ($time === null) { $time = time(); } @@ -158,342 +161,196 @@ public static function getTimestamp($time = null) { } /** - * Gives you the starting year of the current academic year - * @return int year - * @assert () == 2013 + * Returns an RFC 2822-formatted timestamp (for JavaScript). + * + * @param int $time The time to get the timestamp for. Default right now. + * + * @return string a timestamp + * @assert (30) == 'Thu, 01 Jan 1970 00:00:30 +0000' */ - public static function getAcademicYear() { - if (empty(CoreUtils::$academicYear)) { - $term = Database::getInstance()->fetch_column('SELECT start FROM public.terms WHERE descr=\'Autumn\' - AND EXTRACT(year FROM start) = $1', array(date('Y'))); - if (strtotime($term[0]) <= strtotime('+' . Config::$account_expiry_before . ' days')) { - CoreUtils::$academicYear = date('Y'); - } else { - CoreUtils::$academicYear = date('Y') - 1; - } + public static function getRfc2822Timestamp($time = null) + { + if ($time === null) { + $time = time(); } - return CoreUtils::$academicYear; - } - /** - * Returns a postgresql formatted interval - * @param int $start The start time - * @param int $end The end time - * @return String a PgSQL valid interval value - * @assert (0, 0) == '0 seconds' - */ - public static function makeInterval($start, $end) { - return $end - $start . ' seconds'; + return gmdate('r', $time); } /** - * Redirects to another page. + * Returns an ISO 8601-formatted timestamp (for JavaScript). * - * @param string $module The module to which we should redirect. - * @param string $action The optional action inside the module to target. - * @param array $params Additional GET variables - * @return null Nothing. - */ - public static function redirect($module, $action = null, $params = array()) { - header('Location: ' . self::makeURL($module, $action, $params)); - } - - /** - * Builds a module/action URL - * @param string $module - * @param string $action - * @param array $params Additional GET variables - * @return String URL to Module/Action + * @param int $time The time to get the timestamp for. Default right now. + * + * @return string a timestamp + * @assert (30) == '1970-01-01T00:30:00Z' */ - public static function makeURL($module, $action = null, $params = array()) { - if (empty(self::$custom_uris)) { - $result = Database::getInstance()->fetch_all('SELECT actionid, custom_uri FROM myury.actions'); - - foreach ($result as $row) { - self::$custom_uris[$row['actionid']] = $row['custom_uri']; - } - } - //Check if there is a custom URL configured - $key = self::getActionId(self::getModuleId($module), empty($action) ? Config::$default_action : $action); - if (!empty(self::$custom_uris[$key])) { - return self::$custom_uris[$key]; + public static function getIso8601Timestamp($time = null) + { + if ($time === null) { + $time = time(); } - if (Config::$rewrite_url) { - $str = Config::$base_url . $module . '/' . (($action !== null) ? $action . '/' : ''); - if (!empty($params)) { - if (is_string($params)) { - if (substr($params, 0, 1) !== '?') { - $str .= '?'; - } - $str .= $params; - } else { - $str .= '?'; - foreach ($params as $k => $v) { - $str .= "$k=$v&"; - } - $str = substr($str, 0, -1); - } - } - } else { - $str = Config::$base_url . '?module=' . $module . (($action !== null) ? '&action=' . $action : ''); - - if (!empty($params)) { - if (is_string($params)) { - $str .= $params; - } else { - foreach ($params as $k => $v) { - $str .= "&$k=$v"; - } - } - } - } - return $str; + return gmdate('c', $time); } /** - * Sets up the Authentication Constants - * @return void - * @assert () == null + * Returns the ISO8601 Year and Week Number for the given time. + * + * @param int $time The time to get the info for, default now. + * + * @return array [year, week_number] */ - public static function setUpAuth() { - if (self::$auth_cached) { - return; + public static function getYearAndWeekNo($time = null) + { + if ($time === null) { + $time = time(); } - $db = Database::getInstance(); - $result = $db->fetch_all('SELECT typeid, phpconstant, descr FROM l_action'); - foreach ($result as $row) { - define($row['phpconstant'], $row['typeid']); - self::$typeid_descr[$row['typeid']] = $row['descr']; + $year_absolute = (int) gmdate('Y', $time); + $week_number = (int) gmdate('W', $time); + $month = (int) gmdate('n', $time); + + if ($month === 1 && $week_number > 50) { + //This is the final week of *last* year + $year_adjusted = $year_absolute - 1; + } else { + $year_adjusted = $year_absolute; } - self::$auth_cached = true; + return [$year_adjusted, $week_number]; } /** - * Returns the Actions and API Endpoints that utilise a given type. - * - * @param int $typeid - * @return [[action,...], [api method,...]] + * Gives you the starting year of the current academic year. + * + * @return int year + * @assert () == 2013 */ - public static function getAuthUsage($typeid) { - $db = Database::getInstance(); - $actions = $db->fetch_all( - 'SELECT modules.name AS module, actions.name AS action - FROM myury.act_permission - LEFT JOIN myury.modules USING (moduleid) - LEFT JOIN myury.actions USING (actionid) - WHERE typeid=$1', [$typeid]); - - $apis = $db->fetch_all( - 'SELECT api_name, method_name - FROM myury.api_method_auth - LEFT JOIN myury.api_class_map USING (class_name) - WHERE typeid=$1', [$typeid]); - - return [$actions, $apis]; - } + public static function getAcademicYear() + { + if (empty(self::$academicYear)) { + $term = Database::getInstance()->fetchColumn( + 'SELECT start FROM public.terms WHERE descr=\'Autumn\' + AND EXTRACT(year FROM start) = $1', + [date('Y')] + ); + + // Default to this year + $account_reset_time = strtotime('+'.Config::$account_expiry_before.' days'); + if (empty($term) || strtotime($term[0]) <= $account_reset_time) { + self::$academicYear = date('Y'); + } else { + self::$academicYear = date('Y') - 1; + } + } - /** - * Gets the description (friendly name) of the given permission. - * - * @param int $typeid - * @return String - */ - public static function getAuthDescription($typeid) { - self::setUpAuth(); - return self::$typeid_descr[$typeid]; + return self::$academicYear; } /** - * Checks using cached permissions whether the current member has the specified permission - * @param int $permission The ID of the permission, resolved by using an AUTH_ constant - * @return boolean Whether the member has the requested permission + * Returns a postgresql formatted interval. + * + * @param int $start The start time + * @param int $end The end time + * + * @return string a PgSQL valid interval value + * @assert (0, 0) == '0 seconds' */ - public static function hasPermission($permission) { - if (!isset($_SESSION['member_permissions'])) { - return false; - } - if ($permission === null) { - return true; - } - return in_array($permission, $_SESSION['member_permissions']); + public static function makeInterval($start, $end) + { + return $end - $start.' seconds'; } - /** - * Checks if the user has the given permission. Or, alternatiely, if we are currently running CLI, reutrns true. - * @param int $permission A permission constant to check - * @return void Will Fatal error if the user does not have the permission - */ - public static function requirePermission($permission) { - if (php_sapi_name() === 'cli') { - return true; //Non-interactive version has God Rights. - } - if (!self::hasPermission($permission)) { - //Load the 403 controller and exit - require 'Controllers/Errors/403.php'; - exit; + public static function intervalToSeconds($time) + { + $sec = 0; + foreach (array_reverse(explode(':', $time)) as $k => $v) { + $sec += pow(60, $k) * $v; } + return $sec; } /** - * Checks if the user has the given permissions required for the given Module/Action combination - * - * The query needs a little bit of explaining.
- * The first two WHERE clauses just set up foreign key references - we're searching by name, not ID.
- * The next two WHERE clauses return exact or wildcard matches for this Module/Action combination.
- * The final two AND NOT phrases make sure it ignores wildcards that allow any access. - * - * @param String $module The Module to check permissions for - * @param String $action The Action to check permissions for - * @param bool $require If true, will die if the user does not have permission. If false, will just return false - * @return bool True on required or authorised, false on unauthorised - */ - public static function requirePermissionAuto($module, $action, $require = true) { - self::setUpAuth(); - $db = Database::getInstance(); - - $result = $db->fetch_column('SELECT typeid FROM myury.act_permission - LEFT OUTER JOIN myury.modules ON act_permission.moduleid=modules.moduleid - LEFT OUTER JOIN myury.actions ON act_permission.actionid=actions.actionid - WHERE (myury.modules.name=$1 OR myury.act_permission.moduleid IS NULL) - AND (myury.actions.name=$2 OR myury.act_permission.actionid IS NULL) - AND NOT (myury.act_permission.actionid IS NULL AND myury.act_permission.typeid IS NULL) - AND NOT (myury.act_permission.moduleid IS NULL AND myury.act_permission.typeid IS NULL)', array($module, $action)); - - //Don't allow empty result sets - throw an Exception as this is very very bad. - if (empty($result)) { - throw new MyRadioException('There are no permissions defined for the ' . $module . '/' . $action . ' action!'); - } - - $authorised = false; - foreach ($result as $permission) { - //It only needs to match one - if ($permission === AUTH_NOLOGIN || (self::hasPermission($permission) && $_SESSION['auth_use_locked'] === false)) { - $authorised = true; - break; + * Runs the relevant encode commands on an uploaded music file. + * + * @param string $tmpfile The original unencoded filepath + * @param string $dbfile The destination filepath, sans extension + * @throws MyRadioException Thrown if encode or move commands appear to fail. + * @note Similar command is run for podcast uploads, which are done slightly differently + */ + public static function encodeTrack($tmpfile, $dbfile) + { + $commands = [ + 'mp3' => "nice -n 15 ffmpeg -i '{$tmpfile}' -ab 192k -f mp3 -map 0:a '{$dbfile}.mp3'", + 'ogg' => "nice -n 15 ffmpeg -i '{$tmpfile}' -acodec libvorbis -ab 192k -map 0:a '{$dbfile}.ogg'" + ]; + $escaped_commands = array_map('escapeshellcmd', $commands); + $failed_formats = []; + + foreach (['mp3', 'ogg'] as $format) { + if (file_exists("{$dbfile}.{$format}")) { + throw new MyRadioException("Cannot encode, track {$dbfile}.{$format} already exists", 500); } } - if (!$authorised && $require) { - //Requires login - if (!isset($_SESSION['memberid']) || $_SESSION['auth_use_locked'] !== false) { - require 'Controllers/MyRadio/login.php'; - } else { - //Authenticated, but not authorized - require 'Controllers/Errors/403.php'; + foreach ($escaped_commands as $format => $command) { + exec($command, $command_stdout, $command_exit_code); + if ($command_exit_code) { + $failed_formats[] = $format; } - exit; } - //Return true on required success, or whether authorised otherwise - return $require || $authorised; - } - - /** - * Returns a list of all currently defined permissions on MyRadio Service/Module/Action combinations. - * - * This has multiple UNIONS with similar queries so it gracefully deals with NULL values - the joins lose them. - * - * @todo Is there a nicer way of doing this? - * @todo Won't do null fields. Requires outer joins. - * - * @return Array A 2D Array, where each second dimensions is as follows:
- * action: The name of the Action page
- * module: The name of the Module the action is in
- * service: The name of the Service the module is in
- * permission: The name of the permission applied to that Service/Module/Action combination
- * actpermissionid: The unique ID of this Service/Module/Action combination - * - */ - public static function getAllActionPermissions() { - return Database::getInstance()->fetch_all( - 'SELECT actpermissionid, - myury.services.name AS service, - myury.modules.name AS module, - myury.actions.name AS action, - public.l_action.descr AS permission - FROM myury.act_permission, myury.services, myury.modules, myury.actions, public.l_action - WHERE myury.act_permission.actionid=myury.actions.actionid - AND myury.act_permission.moduleid=myury.modules.moduleid - AND myury.act_permission.serviceid=myury.services.serviceid - AND myury.act_permission.typeid = public.l_action.typeid - - UNION - - SELECT actpermissionid, - myury.services.name AS service, - myury.modules.name AS module, - \'ALL ACTIONS\' AS action, - public.l_action.descr AS permission - FROM myury.act_permission, myury.services, myury.modules, public.l_action - WHERE myury.act_permission.moduleid=myury.modules.moduleid - AND myury.act_permission.serviceid=myury.services.serviceid - AND myury.act_permission.typeid = public.l_action.typeid - AND myury.act_permission.actionid IS NULL - - UNION - - SELECT actpermissionid, - myury.services.name AS service, - myury.modules.name AS module, - myury.actions.name AS action, - \'GLOBAL ACCESS\' AS permission - FROM myury.act_permission, myury.services, myury.modules, myury.actions - WHERE myury.act_permission.moduleid=myury.modules.moduleid - AND myury.act_permission.serviceid=myury.services.serviceid - AND myury.act_permission.actionid=myury.actions.actionid - AND myury.act_permission.typeid IS NULL - - ORDER BY service, module'); - } - - /** - * Returns a list of Permissions ready for direct use in a select MyRadioFormField - * @return Array A 2D Array matching the MyRadioFormField::TYPE_SELECT specification. - */ - public static function getAllPermissions() { - return Database::getInstance()->fetch_all('SELECT typeid AS value, descr AS text FROM public.l_action - ORDER BY descr ASC'); - } - - /** - * Returns a list of all MyRadio managed Services in a 2D Array. - * @return Array A 2D Array with each second dimension as follows:
- * value: The ID of the Service - * text: The Text ID of the Service - * enabeld: Whether the Service is enabled - */ - public static function getServices() { - return Database::getInstance()->fetch_all('SELECT serviceid AS value, name AS text, enabled - FROM myury.services ORDER BY name ASC'); + if ($failed_formats) { + throw new MyRadioException('Conversion failed: ' . implode(',', $failed_formats), 500); + } elseif (!file_exists($dbfile.'.mp3') || !file_exists($dbfile.'.ogg')) { + throw new MyRadioException('Conversion failed', 500); + } + $orig_new_filename = $dbfile .".mp3.orig"; + // using copy() instead of rename() because renaming between different file partitions + // generates a warning relating to atomicity. + // Now added some more checking to hopefully find when tracks don't get uploaded correctly. + copy($tmpfile, $orig_new_filename); + if (!file_exists($orig_new_filename)) { + throw new MyRadioException('Could not copy file to library. File was not created.'); + } elseif ((filesize($tmpfile) !== filesize($orig_new_filename)) + || (md5_file($tmpfile) !== md5_file($orig_new_filename)) + ) { + throw new MyRadioException('File mismatch: "'.$tmpfile.'" copied to library as + "'.$orig_new_filename.'", files are not equal.'); + } else { + unlink($tmpfile); + } } /** * A simple debug method that only displays output for a specific user. - * @param int $userid The ID of the user to display for - * @param String $message The HTML to display for this user + * + * @param int $userid The ID of the user to display for + * @param string $message The HTML to display for this user * @assert (7449, 'Test') == null */ - public static function debug_for($userid, $message) { + public static function debugFor($userid, $message) + { if ($_SESSION['memberid'] == $userid) { - echo '

' . $message . '

'; + echo '

'.$message.'

'; } } /** - * Returns the ID of a Module, creating it if necessary + * Returns the ID of a Module, creating it if necessary. * * This method first caches all module IDs, if they aren't already available. It then checks * if the given module exists, and if not it creates one, generating an ID. - * - * @param String $module + * + * @param string $module + * * @return int */ - public static function getModuleId($module) { + public static function getModuleId($module) + { if (empty(self::$module_ids)) { - $result = Database::getInstance()->fetch_all('SELECT name, moduleid FROM myury.modules'); + $result = Database::getInstance()->fetchAll('SELECT name, moduleid FROM myury.modules'); foreach ($result as $row) { self::$module_ids[$row['name']] = $row['moduleid']; } @@ -501,140 +358,69 @@ public static function getModuleId($module) { if (empty(self::$module_ids[$module])) { //The module needs creating - $result = Database::getInstance()->fetch_column('INSERT INTO myury.modules (serviceid, name) - VALUES ($1, $2) RETURNING moduleid', array(Config::$service_id, $module)); - self::$module_ids[$module] = $result[0]; + $result = Database::getInstance()->fetchColumn( + 'INSERT INTO myury.modules (serviceid, name) + VALUES ($1, $2) RETURNING moduleid', + [Config::$service_id, $module] + ); + if ($result) { + self::$module_ids[$module] = $result[0]; + } else { + return; + } } + return self::$module_ids[$module]; } /** - * Returns the ID of a Service/Module/Action request, creating it if necessary - * @param int $module - * @param String $action + * Returns the ID of a Service/Module/Action request, creating it if necessary. + * + * @param int $module + * @param string $action + * * @return int */ - public static function getActionId($module, $action) { + public static function getActionId($module, $action) + { if (empty(self::$action_ids)) { - $result = Database::getInstance()->fetch_all('SELECT name, moduleid, actionid FROM myury.actions'); + $result = Database::getInstance()->fetchAll('SELECT name, moduleid, actionid FROM myury.actions'); foreach ($result as $row) { - self::$action_ids[$row['name'] . '-' . $row['moduleid']] = $row['actionid']; + self::$action_ids[$row['name'].'-'.$row['moduleid']] = $row['actionid']; } } - if (empty(self::$action_ids[$action . '-' . $module])) { + if (empty(self::$action_ids[$action.'-'.$module])) { //The action needs creating - $result = Database::getInstance()->fetch_column('INSERT INTO myury.actions (moduleid, name) - VALUES ($1, $2) RETURNING actionid', array($module, $action)); - self::$action_ids[$action . '-' . $module] = $result[0]; - } - return self::$action_ids[$action . '-' . $module]; - } - - /** - * Assigns a permission to a command - * @todo Document - * @param type $module - * @param type $action - * @param type $permission - */ - public static function addActionPermission($module, $action, $permission) { - $db = Database::getInstance(); - $db->query('INSERT INTO myury.act_permission (serviceid, moduleid, actionid, typeid) - VALUES ($1, $2, $3, $4)', array(Config::$service_id, $module, $action, $permission)); - } - - /** - * Returns the service version allocated to the given user. - * - * @param MyRadio_User $user If given this is the user to check. By default, - * it uses the currently logged-in user. - * - * If there is no user logged in, then the default version is returned. - */ - public static function getServiceVersionForUser(MyRadio_User $user = null) { - if ($user === null) { - if (!isset($_SESSION['memberid'])) { - return self::getDefaultServiceVersion(); - } - $user = MyRadio_User::getInstance(); - } - $serviceid = Config::$service_id; - $key = $serviceid . '-' . $user->getID(); - - if ($user->getID() === MyRadio_User::getInstance()->getID()) { - //It's the current user. If they have an override defined in their session, use that. - if (isset($_SESSION['myury_svc_version_' . $serviceid])) { - return array( - 'version' => $_SESSION['myury_svc_version_' . $serviceid], - 'path' => $_SESSION['myury_svc_version_' . $serviceid . '_path'], - 'proxy_static' => $_SESSION['myury_svc_version_' . $serviceid . '_proxy_static'] - ); - } - } - - if (!isset(self::$svc_version_cache[$key])) { - $db = Database::getInstance(); - - $result = $db->fetch_one('SELECT version, path, proxy_static - FROM myury.services_versions - WHERE serviceid IN (SELECT serviceid FROM myury.services_versions_member - WHERE memberid=$2 AND serviceversionid IN (SELECT serviceversionid FROM myury.services_versions - WHERE serviceid=$1) - )', array($serviceid, $user->getID())); - - if (empty($result)) { - self::$svc_version_cache[$key] = self::getDefaultServiceVersion(); + $result = Database::getInstance()->fetchColumn( + 'INSERT INTO myury.actions (moduleid, name) + VALUES ($1, $2) RETURNING actionid', + [$module, $action] + ); + if ($result) { + self::$action_ids[$action.'-'.$module] = $result[0]; } else { - $result['proxy_static'] = $result['proxy_static'] === 't'; - self::$svc_version_cache[$key] = $result; + return; } } - //If it's the current user, store the data in session. - if ($user->getID() === MyRadio_User::getInstance()->getID()) { - $_SESSION['myury_svc_version_' . $serviceid] = self::$svc_version_cache[$key]['version']; - $_SESSION['myury_svc_version_' . $serviceid . '_path'] = self::$svc_version_cache[$key]['path']; - $_SESSION['myury_svc_version_' . $serviceid . '_proxy_static'] = self::$svc_version_cache[$key]['proxy_static']; - } - - return self::$svc_version_cache[$key]; + return self::$action_ids[$action.'-'.$module]; } /** - * - */ - private static function getDefaultServiceVersion() { - $db = Database::getInstance(); - - $r = $db->fetch_one('SELECT version, path, proxy_static FROM myury.services_versions WHERE serviceid=$1 - AND is_default=true LIMIT 1', array(Config::$service_id)); - $r['proxy_static'] = $r['proxy_static'] === 't'; - - return $r; - } - - /** - * @todo Document this. - * @return boolean - */ - public static function getServiceVersions() { - $db = Database::getInstance(); - - return $db->fetch_all('SELECT version, path, proxy_static FROM myury.services_versions WHERE serviceid=$1', array(Config::$service_id)); - } - - /** - * Parses an object or array into client array datasource + * Parses an object or array into client array datasource. + * * @param mixed $data + * * @return array */ - public static function dataSourceParser($data, $full = true) { - if (is_object($data) && $data instanceof MyRadio_DataSource) { - return $data->toDataSource($full); + public static function dataSourceParser($data, $mixins = []) + { + if (is_object($data) && $data instanceof ServiceAPI) { + return $data->toDataSource($mixins); } elseif (is_array($data)) { foreach ($data as $k => $v) { - $data[$k] = self::dataSourceParser($v, $full); + $data[$k] = self::dataSourceParser($v, $mixins); } return $data; } else { @@ -642,25 +428,54 @@ public static function dataSourceParser($data, $full = true) { } } + /** + * Iteratively calls the toDataSource method on all of the objects in the given array, returning the results as + * a new array. + * @param array $array + * @param array $mixins Mixins + * @return array, unless it wasn't passed an array to begin with, in which case just return $array. + * @throws MyRadioException Throws an Exception if a provided object is not a DataSource + */ + public static function setToDataSource($array, $mixins = []) + { + if (!is_array($array)) { + return $array; + } + $result = []; + foreach ($array as $element) { + //It must implement the toDataSource method! + if (!method_exists($element, 'toDataSource')) { + throw new MyRadioException( + 'Attempted to convert '.get_class($element).' to a DataSource but it not a valid Data Object!', + 500 + ); + } + $result[] = $element->toDataSource($mixins); + } + return $result; + } + //from http://www.php.net/manual/en/function.xml-parse-into-struct.php#109032 - public static function xml2array($xml) { - $opened = array(); + public static function xml2array($xml) + { + $opened = []; $opened[1] = 0; $xml_parser = xml_parser_create(); xml_parse_into_struct($xml_parser, $xml, $xmlarray); $array = array_shift($xmlarray); - unset($array["level"]); - unset($array["type"]); + unset($array['level']); + unset($array['type']); $arrsize = sizeof($xmlarray); - for ($j = 0; $j < $arrsize; $j++) { + for ($j = 0; $j < $arrsize; ++$j) { $val = $xmlarray[$j]; - switch ($val["type"]) { - case "open": - $opened[$val["level"]] = 0; - case "complete": - $index = ""; - for ($i = 1; $i < ($val["level"]); $i++) { - $index .= "[" . $opened[$i] . "]"; + switch ($val['type']) { + case 'open': + $opened[$val['level']] = 0; + /* Fall through */ + case 'complete': + $index = ''; + for ($i = 1; $i < ($val['level']); ++$i) { + $index .= '['.$opened[$i].']'; } $path = explode('][', substr($index, 1, -1)); $value = &$array; @@ -668,42 +483,42 @@ public static function xml2array($xml) { $value = &$value[$segment]; } $value = $val; - unset($value["level"]); - unset($value["type"]); - if ($val["type"] == "complete") { - $opened[$val["level"] - 1] ++; + unset($value['level']); + unset($value['type']); + if ($val['type'] == 'complete') { + ++$opened[$val['level'] - 1]; } break; - case "close": - $opened[$val["level"] - 1] ++; - unset($opened[$val["level"]]); + case 'close': + $opened[$val['level'] - 1]++; + unset($opened[$val['level']]); break; } } + return $array; } - public static function requireTimeslot() { + public static function requireTimeslot() + { if (!isset($_SESSION['timeslotid'])) { - header('Location: ' . CoreUtils::makeURL('MyRadio', 'timeslot', ['next' => $_SERVER['REQUEST_URI']])); + URLUtils::redirect('MyRadio', 'timeslot', ['next' => $_SERVER['REQUEST_URI']]); exit; } } - public static function backWithMessage($message) { - header('Location: ' . $_SERVER['HTTP_REFERER'] . (strstr($_SERVER['HTTP_REFERER'], '?') !== false ? '&' : '?') . 'message=' . base64_encode($message)); - } - /** * Returns a randomly selected item from the list, in a biased manner - * Weighted should be an integer - how many times to put the item into the bag - * @param Array $data 2D of Format [['item' => mixed, 'weight' => n], ...] + * Weighted should be an integer - how many times to put the item into the bag. + * + * @param array $data 2D of Format [['item' => mixed, 'weight' => n], ...] */ - public static function biased_random($data) { - $bag = array(); + public static function biasedRandom($data) + { + $bag = []; foreach ($data as $ball) { - for (; $ball['weight'] > 0; $ball['weight'] --) { + for (; $ball['weight'] > 0; --$ball['weight']) { $bag[] = $ball['item']; } } @@ -712,7 +527,8 @@ public static function biased_random($data) { } //Reports some things - public static function shutdown() { + public static function shutdown() + { session_write_close(); //It doesn't seem to do this itself sometimes. try { $db = Database::getInstance(); @@ -724,122 +540,79 @@ public static function shutdown() { flush(); } - $errors = MyRadioError::getErrorCount(); - $exceptions = MyRadioException::getExceptionCount(); - $queries = $db->getCounter(); - $host = gethostbyname(gethostname()); - - $db->query('INSERT INTO myury.error_rate (server_ip, error_count, exception_count, queries) - VALUES ($1, $2, $3, $4)', array($host, $errors, $exceptions, $queries)); + //Discard any in-progress transactions + if ($db->getInTransaction()) { + $db->query('ROLLBACK'); + } } /** - * Ring YUSU's API and ask how it's doing - * + * Ring YUSU's API and ask how it's doing. + * * Currently, ListMembers is the only function available. Dan Bishop has plans for more at a later date. - * - * @return Array JSON Response, forced to assoc array - */ - public static function callYUSU($function = 'ListMembers') { - return json_decode(file_get_contents('https://www.yusu.org/api/api.php?apikey=' . Config::$yusu_api_key . '&function=' . $function), true); - /** - * @todo php5-curl not installed, pkg broken (20130716) - */ - $ch = curl_init(); - $timeout = 5; - curl_setopt($ch, CURLOPT_URL, 'https://www.yusu.org/api/api.php?apikey=' . Config::$yusu_api_key . '&function=' . $function); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); - curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/5.0 (Unix; en-GB) MyRadio/2013.07.16 (cURL)'); - $data = curl_exec($ch); - curl_close($ch); - return json_decode($data, true); - } - - public static function getErrorStats($since = null) { - if ($since === null) { - $since = time() - 86400; - } - $result = Database::getInstance()->fetch_all('SELECT - round(extract(\'epoch\' from timestamp) / 600) * 600 as timestamp, - SUM(error_count)/COUNT(error_count) AS errors, SUM(exception_count)/COUNT(exception_count) AS exceptions, - SUM(queries)/COUNT(queries) AS queries - FROM myury.error_rate WHERE timestamp>=$1 GROUP BY round(extract(\'epoch\' from timestamp) / 600) - ORDER BY timestamp ASC', array(self::getTimestamp($since))); - - $return = array(); - $return[] = array('Timestamp', 'Errors per request', 'Exceptions per request', 'Queries per request'); - foreach ($result as $row) { - $return[] = array(date('H:i', $row['timestamp']), (int) $row['errors'], (int) $row['exceptions'], (int) $row['queries']); - } - return $return; - } - - public static function getSafeHTML($dirty_html) { - require_once 'Classes/Vendor/htmlpurifier/HTMLPurifier.auto.php'; - $config = HTMLPurifier_Config::createDefault(); - $purifier = new HTMLPurifier($config); + * + * @return array JSON Response, forced to assoc array + */ + public static function callYUSU($function = 'ListMembers') + { + $options = [ + 'http' => [ + 'method' => 'GET', + 'header' => "User-Agent: MyRadio\r\n", + ], + ]; + $context = stream_context_create($options); + + return json_decode( + file_get_contents( + Config::$yusu_api_website + .'?apikey=' + .Config::$yusu_api_key + .'&function=' + .$function, + false, + $context + ), + true + ); + } + + public static function getSafeHTML($dirty_html) + { + $config = \HTMLPurifier_Config::createDefault(); + $purifier = new \HTMLPurifier($config); + return $purifier->purify($dirty_html); } /** - * Tests whether the given username or password are valid against a provider - * (and the right provider if needed). - * - * This is a far more basic version of the full Controllers/login.php system, - * not verifying if the user needs to take an action first. - * It does, however, update the User's last login time.. - * You MUST use POST with this - otherwise the credentials will turn up in - * access logs. - * - * @param String $user - * @param String $pass - * @return MyRadio_User|false - * @api POST + * Returns lookup values for Status for a select box. + * + * @return array */ - public static function testCredentials($user, $pass) { - //Make a best guess at the user account - //This way we can skip authenticators if they have one set - $u = MyRadio_User::findByEmail($user); - if ($u instanceof MyRadio_User && $u->getAuthProvider() !== null) { - $authenticators = [$u->getAuthProvider()]; - } else { - $authenticators = Config::$authenticators; - } - - //Iterate over each authenticator - foreach ($authenticators as $authenticator) { - $a = new $authenticator(); - $result = $a->validateCredentials($user, $pass); - if ($result instanceof MyRadio_User) { - if (Config::$single_authenticator && - $result->getAuthProvider() !== null && - $result->getAuthProvider() !== $authenticator) { - //This is the wrong authenticator for the user - continue; - } else { - $result->updateLastLogin(); - return $result; - } - } - } - return false; + public static function getStatusLookup() + { + return Database::getInstance()->fetchAll( + 'SELECT statusid AS value, descr AS text FROM public.l_status + ORDER BY descr ASC' + ); } /** * Returns information about the $_REQUEST array. - * + * * This *MUST* be used instead of print_r($_REQUEST) or var_dump($_REQUEST) * in debug output. - * - * @return String var_dump output + * + * @return string var_dump output */ - public static function getRequestInfo() { + public static function getRequestInfo() + { ob_start(); - if (isset($_REQUEST['redact'])) { - $info = array(); + if (isset($_REQUEST['redact']) || isset($_REQUEST['pass']) || isset($_REQUEST['password'])) { + $info = []; foreach ($_REQUEST as $k => $v) { - if (!in_array($k, $_REQUEST['redact'])) { + if (!in_array($k, $_REQUEST['redact']) && $k !== 'pass' && $k !== 'password') { $info[$k] = $v; } else { $info[$k] = '**REDACTED**'; @@ -849,44 +622,140 @@ public static function getRequestInfo() { } else { var_dump($_REQUEST); } + + return ob_get_clean(); + } + + /** + * Returns information about the $_SERVER array. + * + * @return string var_dump output + */ + public static function getServerInfo() + { + ob_start(); + var_dump($_SERVER); return ob_get_clean(); } /** * Generates a completely pseudorandom string, aimed for Salt purposes. + * * @param int $pwdLen The length of the string to generate - * @return String a random string of length $pwdLen + * + * @return string a random string of length $pwdLen */ - public static function randomString($pwdLen = 8) { + public static function randomString($pwdLen = 8) + { $result = ''; $pwdSource = '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; srand((double) microtime() * 1000000); while ($pwdLen) { $result .= substr($pwdSource, rand(0, strlen($pwdSource) - 1), 1); - $pwdLen--; + --$pwdLen; + } + + return $result; + } + + /** + * I'm becoming a Python person who just expects this to be a thing. + * + * Copypasta from http://stackoverflow.com/questions/834303/startswith-and-endswith-functions-in-php + */ + public static function startsWith($haystack, $needle) + { + // search backwards starting from haystack length characters from the end + return $needle === '' || strrpos($haystack, $needle, -strlen($haystack)) !== false; + } + + /** + * Explode tags from string to array. + * We want to handle the case when people delimit with commas, spaces, or commas and + * spaces, as well as handling extended spaces. + * + * @param string $tags A tags string, comma separated. + * + * @return array The exploded tags. + * + * @throws MyRadioException when tags are longer than 24 characters. + **/ + public static function explodeTags($tags) + { + $tags = preg_split('/[, ] */', $tags, null, PREG_SPLIT_NO_EMPTY); + $exploded_tags = []; + foreach ($tags as $tag) { + if (empty($tag)) { + continue; + } + if (strlen($tag) > 24) { + throw new MyRadioException( + "Sorry, individual tags longer than 24 characters aren't allowed. Please try again.", + 400 + ); + } + // Add the valid tag to the returned array. + $exploded_tags[] = trim($tag); } - return( $result ); + return $exploded_tags; } - private function __construct() { - + public static function checkUploadPostSize() + { + // Check that any files don't go over the PHP post_max_size + // Otherwise, sometimes PHP won't return an error, causing an empty $_POST. + // This would cause confusing errors relating to empty fields. + // https://stackoverflow.com/questions/2133652/how-to-gracefully-handle-files-that-exceed-phps-post-max-size + $post_size = trim(ini_get('post_max_size')); + if ($post_size != '') { + $last = strtolower(substr($post_size, -1)); + } else { + $last = ''; + } + $post_size = intval($post_size); // Convert to int (strips any suffix letters) + switch ($last) { + // The 'G' modifier is available since PHP 5.1.0 + case 'g': + $post_size *= 1024; + // fall through + case 'm': + $post_size *= 1024; + // fall through + case 'k': + $post_size *= 1024; + // fall through + } + if ($_SERVER['CONTENT_LENGTH'] > $post_size) { + throw new MyRadioException( + "The content uploaded in this form was too large for the server's configuration.", + 500 + ); + } + } + + private function __construct() + { } /** - * Generates a new password consisting of two words and a two-digit number + * Generates a new password consisting of two words and a two-digit number. + * * @todo Make this crypto secure random? - * @return String + * + * @return string */ - public static function newPassword() { - return self::$words[array_rand(self::$words)] . rand(10, 99) - . self::$words[array_rand(self::$words)]; + public static function newPassword() + { + return self::$words[array_rand(self::$words)].rand(10, 99) + .self::$words[array_rand(self::$words)]; } /** - * Words used by CoreUtils::newPassword - * @var String[] + * Words used by CoreUtils::newPassword. + * + * @var string[] */ - private static $words = array( + private static $words = [ 'Radio', 'Microphone', 'Studio', @@ -922,7 +791,85 @@ public static function newPassword() { 'Frequency', 'Modulation', 'Vinyl', - 'Broadcasting' - ); + 'Broadcasting', + ]; + /** + * This whole thing is a bodge that needs to die. + * + * Duplicate the name-match-based subtype identification logic from 2016-site, until it is configured to + * properly use the [show, season]Subtype field from MyRadio, and MyRadio has a proper GUI for setting them. + * + * It sucks, but that's the cost of progress. + * @param $show_name string + * @return string + */ + public static function getSubtypeForShow($show_name) + { + $blockMatches = [ + ["ury: early morning", "primetime"], + ["ury breakfast", "primetime"], + ["ury lunch", "primetime"], + ["ury brunch", "primetime"], + ["URY Brunch", "primetime"], + ["URY Afternoon Tea:", "primetime"], + ["URY:PM", "primetime"], + ["Alumni Takeover:", "primetime"], + + ["ury news", "news"], + ["ury sports", "news"], + ["ury football", "news"], + ["york sport report", "news"], + ["university radio talk", "news"], + ["candidate interview night", "news"], + ["election results night", "news"], + ["yusu election", "news"], + ["The Second Half With Josh Kerr", "news"], + ["URY SPORT", "news"], + ["URY News & Sport:", "news"], + ["URY N&S:", "news"], + + ["ury speech", "speech"], + ["yorworld", "speech"], + ["in the stalls", "speech"], + ["screen", "speech"], + ["stage", "speech"], + ["game breaking", "speech"], + ["radio drama", "speech"], + ["Book Corner", "speech"], + ["Saturated Facts", "speech"], + ["URWatch", "speech"], + ["Society Challenge", "speech"], + ["Speech Showcase", "speech"], + ["URY Speech:", "speech"], + + ["URY Music:", "music"], + + ["roses live 20", "event"], + ["roses 20", "event"], + ["freshers 20", "event"], + ["woodstock", "event"], + ["movember", "event"], + ["panto", "event"], + ["101:", "event"], + ["Vanbrugh Chair Debate", "event"], + ["URY Does RAG Courtyard Takeover", "event"], + ["URY Presents", "event"], + ["URYOnTour", "event"], + ["URY On Tour", "event"], + + ["YSTV", "collab"], + ["Nouse", "collab"], + ["York Politics Digest", "collab"], + ["Breakz", "collab"], + ]; + + $name = strtolower($show_name); + foreach ($blockMatches as $match) { + if (strpos($name, strtolower($match[0])) !== false /* bloody PHP */) { + return $match[1]; + } + } + return 'regular'; + } } diff --git a/src/Classes/MyRadio/GraphQLContext.php b/src/Classes/MyRadio/GraphQLContext.php new file mode 100644 index 000000000..a2c170f56 --- /dev/null +++ b/src/Classes/MyRadio/GraphQLContext.php @@ -0,0 +1,19 @@ +warnings[] = $warning; + } + + public function getWarnings() + { + return $this->warnings; + } +} diff --git a/src/Classes/MyRadio/GraphQLUtils.php b/src/Classes/MyRadio/GraphQLUtils.php new file mode 100644 index 000000000..f15ec924d --- /dev/null +++ b/src/Classes/MyRadio/GraphQLUtils.php @@ -0,0 +1,330 @@ + [ + 'c' => 'Current', + 'h' => 'Historic' + ], + 'OfficerType' => [ + 'h' => 'HeadOfTeam', + 'a' => 'AssistantHeadOfTeam', + 'm' => 'TeamMember', + 'o' => 'Other' + ] + ]; + + /** + * @param ResolveInfo $info + * @param string $name + * @return DirectiveNode|null + */ + public static function getDirectiveByName(ResolveInfo $info, string $name) + { + $fieldNode = $info->parentType->getField($info->fieldName)->astNode; + return self::getDirectiveByNameOnAstNode($fieldNode, $name); + } + + /** + * @param ObjectTypeDefinitionNode|FieldDefinitionNode $node + * @param string $name + * @return DirectiveNode|null + */ + private static function getDirectiveByNameOnAstNode($node, string $name) + { + /** @var NodeList $directives */ + $directives = $node->directives; + if ($directives) { + /** @var DirectiveNode[] $directives */ + foreach ($directives as $directive) { + if ($directive->name->value === $name) { + return $directive; + } + } + } + return null; + } + + /** + * @param DirectiveNode $directive + * @return ValueNode[] + */ + public static function getDirectiveArguments(DirectiveNode $directive) + { + $args = []; + foreach ($directive->arguments as $arg) { + $args[$arg->name->value] = $arg->value; + } + return $args; + } + + /** + * Er, take a wild guess? + * @param ResolveInfo $info + */ + public static function returnNullOrThrowForbiddenException(ResolveInfo $info) + { + if ($info->returnType instanceof NonNull) { + throw new MyRadioException('Caller cannot access this field', 403); + } else { + return null; + } + } + + /** + * Tests if the current caller is authorised to access the given GraphQL field + * @param ResolveInfo $info + * @param string|null $resolvedClass + * @param string|null $resolvedMethod + * @param mixed|null $resolvedObject + * @return bool + */ + public static function isAuthorisedToAccess( + ResolveInfo $info, + $resolvedClass, + $resolvedMethod, + $resolvedObject = null + ) { + $caller = MyRadio_Swagger2::getAPICaller(); + if ($caller === null) { + throw new MyRadioException('No valid authentication data provided', 401); + } + if ($caller->hasAuth(AUTH_APISUDO)) { + // I am become sudo, doer of API calls + return true; + } + // First, check if we have an @auth directive. If so, it overrides. + $authDirective = self::getDirectiveByName($info, 'auth'); + if ($authDirective !== null) { + return self::processAuthDirective( + self::getDirectiveArguments($authDirective), + $resolvedClass, + $resolvedMethod, + $resolvedObject + ); + } else { + // Check if there's an @auth on the parent object + $authDirective = self::getDirectiveByNameOnAstNode($info->parentType->astNode, "auth"); + if ($authDirective !== null) { + return self::processAuthDirective( + self::getDirectiveArguments($authDirective), + $resolvedClass, + $resolvedMethod, + $resolvedObject + ); + } else { + // Object-scalar rule: if we're dealing with an object, use API v2 rules + // If we're dealing with a scalar, assume it's okay, as it must have come from an object + if ($info->returnType instanceof WrappingType) { + $type = $info->returnType->getWrappedType(true); + } else { + $type = $info->returnType; + } + if ($type instanceof ScalarType || $type instanceof EnumType) { + return true; + } elseif ($resolvedClass === null && $resolvedMethod === null) { + return true; + } else { + return $caller->canCall($resolvedClass, $resolvedMethod); + } + } + } + } + + private static function processAuthDirective( + array $args, + $resolvedClass, + $resolvedMethod, + $resolvedObject = null + ) { + if (isset($args['constants'])) { + $constants = $args['constants']->values; + // No constants => public access + if (count($constants) === 0) { + return true; + } + foreach ($constants as $constant) { + if (AuthUtils::hasPermission(constant($constant))) { + return true; + } + } + } + /** @var EnumValueNode $hookVal */ + $hook = $args['hook']; + if (isset($hook)) { + $hookName = $hook->value; + switch ($hookName) { + case 'ViewShow': + /** @var MyRadio_Show|MyRadio_Season|MyRadio_Timeslot $show */ + $show = $resolvedObject; + if (AuthUtils::hasPermission(AUTH_VIEWMEMBERSHOWS)) { + return true; + } + return $show->isCurrentUserAnOwner(); + case 'ViewMember': + case 'ViewOfficer': + /** @var MyRadio_User $member */ + $member = $resolvedObject; + if (AuthUtils::hasPermission(AUTH_VIEWOTHERMEMBERS)) { + return true; + } + if ($hookName === 'viewOfficer' && $member->isOfficer()) { + return true; + } + return $member->getID() === (MyRadio_User::getCurrentOrSystemUser()->getID()); + default: + throw new MyRadioException("Unknown auth hook $hookName"); + } + } + return false; + } + + /** + * Given a resolved value, converts it into a scalar type if appropriate. + * + * For example, given a number or date string on a Date/Time/DateTime type field, + * this method will format it accordingly. + * @param ResolveInfo $info + * @param mixed $value + * @return mixed + */ + public static function processScalarIfNecessary(ResolveInfo $info, $value) + { + // Resolve enums if necessary + if ($info->returnType instanceof EnumType) { + if (isset(self::$ENUM_MAPPINGS[$info->returnType->name])) { + return self::$ENUM_MAPPINGS[$info->returnType->name][$value]; + } + } + // If the field is not a scalar, don't touch it. + if ($info->returnType instanceof ScalarType) { + $type = $info->returnType->name; + } elseif ($info->returnType instanceof WrappingType + && $info->returnType->getWrappedType(true) instanceof ScalarType + ) { + $type = $info->returnType->getWrappedType(true)->name; + } else { + return $value; + } + // If it has a @coerce directive, do as it says + $coerceDirective = self::getDirectiveByName($info, 'coerce'); + if ($coerceDirective !== null) { + foreach (self::getDirectiveArguments($coerceDirective)['hooks']->values as $hook) { + switch ($hook->value) { + case "FalseToNull": + if ($value === false) { + $value = null; + } + break; + default: + $val = $hook->value; + throw new MyRadioException("Unknown coerce hook $val"); + } + } + } + switch ($type) { + case "Int": + case "Float": + case "String": + case "Boolean": + case "ID": + case "HTMLString": + // Passed through directly + return $value; + case "Date": + case "Time": + case "DateTime": + // If the value is a number, assume it's a UNIX timestamp. If not, try and parse it. + if ($value === null) { + return $value; + } elseif (is_numeric($value)) { + $val_unix = (float) $value; + } else { + $val_unix = strtotime($value); + if ($val_unix === false) { + throw new MyRadioException("Failed to parse datetime $value"); + } + } + switch ($type) { + case "Date": + return date("Y-m-d", $val_unix); + case "Time": + return date("H:i:sP", $val_unix); + case "DateTime": + return date("Y-m-d\TH:i:sP", $val_unix); + } + break; + case "Duration": + // If it's a number, assume it's seconds. + if ($value === null) { + return $value; + } elseif (is_numeric($value)) { + $interval = new \DateInterval("PT${value}S"); + } else { + $data = date_parse($value); + $interval = new \DateInterval( + sprintf( + "P%02dY%02dM%02dDT%02dH%02dM%02dS", + $data['year'], + $data['month'], + $data['day'], + $data['hour'], + $data['minute'], + $data['second'] + ) + ); + } + return $interval->format("%H:%M:%S"); + default: + throw new MyRadioException("Unknown scalar type $type!"); + } + } + + public static function invokeNamed(\ReflectionMethod $meth, $object = null, $args = []) + { + $methArgs = $meth->getParameters(); + // If it has no arguments, we can just invoke it directly. + if (count($methArgs) === 0) { + return $meth->invoke($object); + } else { + // Overwrite them in the parameters array to ensure we call them in the correct order + foreach ($methArgs as &$param) { + $name = $param->getName(); + if (isset($args[$name])) { + $param = $args[$name]; + } else { + try { + $param = $param->getDefaultValue(); + } catch (\ReflectionException $e) { + $methName = $meth->getName(); + $obj = $meth->getDeclaringClass()->getName(); + throw new MyRadioException("Missing parameter $name in call to $obj::$methName"); + } + } + } + return $meth->invokeArgs($object, $methArgs); + } + } +} diff --git a/src/Classes/MyRadio/MyRadioAuthenticator.php b/src/Classes/MyRadio/MyRadioAuthenticator.php deleted file mode 100644 index 2073088ee..000000000 --- a/src/Classes/MyRadio/MyRadioAuthenticator.php +++ /dev/null @@ -1,49 +0,0 @@ - - */ -interface MyRadioAuthenticator { - /** - * @param String $user The username (a full email address, or the prefix - * if it matches Config::$eduroam_domain). - * @param String $password The provided password. - * @return MyRadio_User|false Map the credentials to a MyRadio User on success, or - * return false on failure. - */ - public function validateCredentials($user, $password); - - /** - * @param String $user The username (a full email address, or the prefix - * if it matches Config::$eduroam_domain). - * @return Array A list of IDs for the permission flags this user should be - * granted. - */ - public function getPermissions($user); - - /** - * @param String $user The username (a full email address, or the prefix - * if it matches Config::$eduroam_domain). - * @return boolean Whether the reset has happened or not. MyRadio will stop - * attempting resets once one Authenticator has return true. - */ - public function resetAccount($user); - - /** - * A friendly name to explain to users what the login method is - */ - public function getFriendlyName(); - - /** - * A friendly description to explain to users what selecting this authenticator does - */ - public function getDescription(); - - /** - * A friendly message to display on the "I've forgotten my password" page - */ - public function getResetFormMessage(); -} diff --git a/src/Classes/MyRadio/MyRadioDefaultAuthenticator.php b/src/Classes/MyRadio/MyRadioDefaultAuthenticator.php index 38fbc208d..f79dd31df 100644 --- a/src/Classes/MyRadio/MyRadioDefaultAuthenticator.php +++ b/src/Classes/MyRadio/MyRadioDefaultAuthenticator.php @@ -1,19 +1,35 @@ */ -class MyRadioDefaultAuthenticator extends Database implements MyRadioAuthenticator { - +class MyRadioDefaultAuthenticator extends \MyRadio\Database implements \MyRadio\Iface\MyRadioAuthenticator +{ /** - * Sets up the DB connection + * Sets up the DB connection. */ - public function __construct() { - $this->db = pg_connect('host=' . Config::$db_hostname . ' port=5432 dbname=' . Config::$db_name . ' - user=' . Config::$auth_db_user . ' password=' . Config::$auth_db_pass); + public function __construct() + { + if (empty(Config::$auth_db_user)) { + $this->db = pg_connect( + 'host='.Config::$db_hostname.' port=5432 dbname='.Config::$db_name + .' user='.Config::$db_user.' password='.Config::$db_pass + ); + } else { + $this->db = pg_connect( + 'host='.Config::$db_hostname.' port=5432 dbname='.Config::$db_name + .' user='.Config::$auth_db_user.' password='.Config::$auth_db_pass + ); + } if (!$this->db) { //Database isn't working. Throw an EVERYTHING IS BROKEN Exception throw new MyRadioException('Database Connection Failed!', MyRadioException::FATAL); @@ -21,23 +37,29 @@ public function __construct() { } /** - * Tears down the DB connection + * Tears down the DB connection. */ - public function __destruct() { - pg_close($this->db); + public function __destruct() + { + if (!empty(Config::$auth_db_user)) { + pg_close($this->db); + } } /** - * @param String $user The username (a full email address, or the prefix - * if it matches Config::$eduroam_domain). - * @param String $password The provided password. + * @param string $user The username (a full email address, or the prefix if it matches Config::$eduroam_domain). + * if it matches Config::$eduroam_domain). + * @param string $password The provided password. + * * @return MyRadio_User|false Map the credentials to a MyRadio User on success, or - * return false on failure. + * return false on failure. + * * @todo Require change password * @todo Account lock * @todo Make timing consistent */ - public function validateCredentials($user, $password) { + public function validateCredentials($user, $password) + { //If local passwords are disabled, don't even try. if (!Config::$enable_local_passwords) { return false; @@ -47,8 +69,11 @@ public function validateCredentials($user, $password) { if (!$user) { return false; } else { - $r = $this->fetch_column('SELECT password FROM ' - . 'public.member_pass WHERE memberid=$1', [$user->getID()]); + $r = $this->fetchColumn( + 'SELECT password FROM ' + .'public.member_pass WHERE memberid=$1', + [$user->getID()] + ); if (empty($r)) { return false; } else { @@ -58,10 +83,13 @@ public function validateCredentials($user, $password) { if (substr($r[0], 0, 3) === '$1$') { //Upgrade password $new_password = $this->encrypt($password); - $this->query('UPDATE member_pass SET password=$1 WHERE memberid=$2', - [$new_password, $user->getID()]); + $this->query( + 'UPDATE member_pass SET password=$1 WHERE memberid=$2', + [$new_password, $user->getID()] + ); } unset($password, $new_password, $r); //Just to be safe. + return $user; } else { return false; @@ -73,27 +101,32 @@ public function validateCredentials($user, $password) { /** * This authenticator does not add any extra permissions to the ones already * defined internally. - * - * @param String $user The username (a full email address, or the prefix - * if it matches Config::$eduroam_domain). - * @return Array A list of IDs for the permission flags this user should be - * granted. These are in addition to the ones computed by MyRadio - * internally. + * + * @param string $user The username (a full email address, or the prefix + * if it matches Config::$eduroam_domain). + * + * @return array A list of IDs for the permission flags this user should be + * granted. These are in addition to the ones computed by MyRadio + * internally. */ - public function getPermissions($user) { + public function getPermissions($user) + { return []; } /** * This authenticator will reset the password in the MyRadio database. - * - * @param String $user The username (a full email address, or the prefix - * if it matches Config::$eduroam_domain). - * @return boolean Whether the reset has happened or not. MyRadio will stop - * attempting resets once one Authenticator has return true. - * @todo implement password resets + * + * @param string $user The username (a full email address, or the prefix if it matches Config::$eduroam_domain). + * if it matches Config::$eduroam_domain). + * + * @return bool Whether the reset has happened or not. MyRadio will stop + * attempting resets once one Authenticator has return true. + * + * @todo implement password resets */ - public function resetAccount($user) { + public function resetAccount($user) + { $result = MyRadio_User::findByEmail($user); if (!$result) { return false; @@ -104,22 +137,25 @@ public function resetAccount($user) { //Create a reset token do { $token = CoreUtils::randomString(64); - } while ($db->num_rows( - $db->query('SELECT * FROM myury.password_reset_token ' - . 'WHERE token=$1', [$token])) > 0); + } while ($db->numRows($db->query('SELECT * FROM myury.password_reset_token WHERE token=$1', [$token])) > 0); //Add the reset token to the database (expires in 48h) $expires = CoreUtils::getTimestamp(time() + 86400 * 2); - $db->query('INSERT INTO myury.password_reset_token ' - . '(token, memberid, expires) VALUES ($1, $2, $3)', [$token, $result->getID(), $expires]); + $db->query( + 'INSERT INTO myury.password_reset_token (token, memberid, expires) VALUES ($1, $2, $3)', + [$token, $result->getID(), $expires] + ); //Email the user - MyRadioEmail::sendEmailToUser($result, 'Password reset', 'Hello,' - . '

A password reset has been requested for the ' . Config::$short_name - . ' account associated with this email address. If you did not request' - . ' this email, please ignore it.

' - . '

Click here to finish resetting your password.

' + MyRadioEmail::sendEmailToUser( + $result, + 'Password reset', + 'Hello,' + .'

A password reset has been requested for the '.Config::$short_name + .' account associated with this email address. If you did not request' + .' this email, please ignore it.

' + .'

'.URLUtils::makeURL('MyRadio', 'pwChange', ['token' => $token]).'

' + .'

Copy that link and paste it into your searchbar to finish resetting your password.

' ); return true; @@ -127,77 +163,92 @@ public function resetAccount($user) { /** * Encrypts a password using MyRadio's prefered technique. - * - * @param String $string The string to be encrypted - * @return String The encrypted string + * + * @param string $string The string to be encrypted + * + * @return string The encrypted string */ - private function encrypt($string) { - return crypt($string, '$6$rounds=4567$' . $this->randomString()); + private function encrypt($string) + { + return crypt($string, '$6$rounds=4567$'.$this->randomString()); } /** * Generates a cryptographically secure pseudorandom string, for Salt purposes. + * * @param int $pwdLen The length of the string to generate - * @return String a random string of length $pwdLen + * + * @return string a random string of length $pwdLen */ - private function randomString($pwdLen = 32) { + private function randomString($pwdLen = 32) + { return base64_encode(openssl_random_pseudo_bytes($pwdLen)); } - public function getFriendlyName() { - return Config::$short_name . '-only'; + public function getFriendlyName() + { + return Config::$short_name.'-only'; } - public function getDescription() { + public function getDescription() + { return 'By choosing this option, we will always use your unique ' - . $this->getFriendlyName() . ' username and password to log you in. ' - . 'This password is completely seperate to any other details ' - . 'you may have.'; + .$this->getFriendlyName() + .' username and password to log you in. ' + .'This password is completely seperate to any other details ' + .'you may have.'; } - public function removePassword($memberid) { + public function removePassword($memberid) + { $this->query('UPDATE member_pass SET password=NULL WHERE memberid=$1', [$memberid]); } /** * Sets a User's password. - * - * @param User $user - * @param String $password + * + * @param User $user + * @param string $password */ - public function setPassword(MyRadio_User $user, $password) { + public function setPassword(MyRadio_User $user, $password) + { $password = $this->encrypt($password); //Insert or Update - $result = $this->query('UPDATE member_pass SET password=$1 WHERE memberid=$2', - [$password, $user->getID()]); - - //Set require_password_change to false - $user->setRequirePasswordChange(false); - + $result = $this->query( + 'UPDATE member_pass SET password=$1 WHERE memberid=$2', + [$password, $user->getID()] + ); + if (pg_affected_rows($result) === 0) { - $this->query('INSERT INTO member_pass (memberid, password) VALUES ($1, $2)', - [$user->getID(), $password]); + $this->query( + 'INSERT INTO member_pass (memberid, password) VALUES ($1, $2)', + [$user->getID(), $password] + ); } + + //Set require_password_change to false + $user->setRequirePasswordChange(false); } - public function getResetFormMessage() { + public function getResetFormMessage() + { //If this is not the only authenticator, mention this will create a //MyRadio specific login. if (sizeof(Config::$authenticators) > 1) { $others = ''; foreach (Config::$authenticators as $auth) { if ($auth !== __CLASS__) { - $a = new $auth; - $others .= (empty($others) ? '' : ', ') . $a->getFriendlyName(); + $a = new $auth(); + $others .= (empty($others) ? '' : ', ').$a->getFriendlyName(); } } - return 'If you do not currently have a ' . Config::$short_name . + + return 'If you do not currently have a '.Config::$short_name. ' password, this will enable you to set one up which is' - . ' seperate to your ' . $others . ' password.'; + .' seperate to your '.$others.' password.'; } else { - return 'If you\'ve forgotten your ' . Config::$short_name . ' password, you' - . ' can fill in this form to have a reset email sent to you.'; + return 'If you\'ve forgotten your '.Config::$short_name.' password, you' + .' can fill in this form to have a reset email sent to you.'; } } - } diff --git a/src/Classes/MyRadio/MyRadioForm.php b/src/Classes/MyRadio/MyRadioForm.php index 573770506..a80d3602b 100644 --- a/src/Classes/MyRadio/MyRadioForm.php +++ b/src/Classes/MyRadio/MyRadioForm.php @@ -1,124 +1,148 @@ - * @version 20140102 - * @package MyRadio_Core */ -class MyRadioForm { - +class MyRadioForm +{ /** - * The name of the form - * @var String + * The name of the form. + * + * @var string */ private $name = 'autofrm'; /** * The module that it will submit to - * Best practice is this should be the current module - * @var String + * Best practice is this should be the current module. + * + * @var string */ private $module; /** - * The action that it will submit to - * @var String + * The action that it will submit to. + * + * @var string */ private $action; /** - * Whether to enable detailed output of what is happening (or isn't) + * Whether to enable detailed output of what is happening (or isn't). + * * @var bool */ private $debug = false; /** - * Additional classes to add to the base form element - * @var Array + * Additional classes to add to the base form element. + * + * @var array */ - private $classes = array(); + private $classes = []; /** - * Whether to enable Form Validation + * Whether to enable Form Validation. + * * @var bool */ private $validate = true; /** - * Whether to use GET instead of POST + * Whether to use GET instead of POST. + * * @var bool */ private $get = false; /** * The Twig template to use for the form. Must be form.twig or a child. - * @var String + * + * @var string */ private $template = 'form.twig'; /** - * The form fields in the form (an array of MyRadioFormField objects) - * @var Array + * The form fields in the form (an array of MyRadioFormField objects). + * + * @var array */ - private $fields = array(); + private $fields = []; /** - * The title of the page (the human readable name) - * @var String + * The title of the page (the human readable name). + * + * @var string */ private $title = null; /** - * Logging output - * @var Array + * The subtitle of the page (a smaller human readable description). + * + * @var string */ - private $debug_log = array(); - + private $subtitle = null; + /** - * Enable recaptcha requirement - * @var boolean + * Logging output. + * + * @var array + */ + private $debug_log = []; + + /** + * Enable recaptcha requirement. + * + * @var bool */ private $captcha = false; /** - * Fields that cannot be edited by params - * @var Array + * Fields that cannot be edited by params. + * + * @var array */ - private $restricted_fields = array('name', 'module', 'action', 'fields', 'restricted_fields', 'debug_log'); + private $restricted_fields = ['name', 'module', 'action', 'fields', 'restricted_fields', 'debug_log']; /** - * Creates a new MyRadioForm object with the given parameters - * @param string $name The name/id of the form + * Creates a new MyRadioForm object with the given parameters. + * + * @param string $name The name/id of the form * @param string $module The module the form submits to * @param string $action The action the form submits to - * @param array $params One or more of the following additional settings
- * debug - Verbose logging output - default false
- * classes - An array of additional classes to apply to the form - default empty
- * validate - Whether to validate the field input client-side - default true
- * get - Whether to use the GET submission method - default false
- * template - The Twig template to use for the form - default form.twig
- * title - Form Title
- * captcha - Whether to require a captcha for this form - default false - * + * @param array $params One or more of the following additional settings
+ * debug - Verbose logging output - default false
+ * classes - An array of additional classes to apply to the form - default empty
+ * validate - Whether to validate the field input client-side - default true
+ * get - Whether to use the GET submission method - default false
+ * template - The Twig template to use for the form - default form.twig
+ * title - Form Title
+ * subtitle - Form Subtitle
+ * captcha - Whether to require a captcha for this form - default false + * * @throws MyRadioException Thrown on failure of a sanity check */ - public function __construct($name, $module, $action, $params = array()) { + public function __construct($name, $module, $action, $params = []) + { //Sanity check - does the target exist? if (!CoreUtils::isValidController($module, $action)) { throw new MyRadioException('The Module/Action target of this MyRadioForm is invalid.'); @@ -131,86 +155,116 @@ public function __construct($name, $module, $action, $params = array()) { //Check all optional parameters foreach ($params as $k => $v) { //Sanity checks - is this a valid parameter and is it not blacklisted? - if (isset($this->$k) === false && @$this->$k !== null) - throw new MyRadioException('Tried to set MyRadioForm parameter ' . $k . ' but it does not exist.'); - if (in_array($k, $this->restricted_fields)) - throw new MyRadioException('Tried to set MyRadioForm parameter ' . $k . ' but it is not editable.'); + if (isset($this->$k) === false && @$this->$k !== null) { + throw new MyRadioException('Tried to set MyRadioForm parameter '.$k.' but it does not exist.'); + } + if (in_array($k, $this->restricted_fields)) { + throw new MyRadioException('Tried to set MyRadioForm parameter '.$k.' but it is not editable.'); + } $this->$k = $v; } } /** - * Changes the template to use when rendering - * - * @todo Check if template exists first - * @param String $template The path to the template, relative to Templates + * Changes the template to use when rendering. + * + * @todo Check if template exists first + * + * @param string $template The path to the template, relative to Templates */ - public function setTemplate($template) { + public function setTemplate($template) + { $this->template = $template; + return $this; } /** - * Update the title of the form - * @param String $title + * Update the title of the form. + * + * @param string $title */ - public function setTitle($title) { + public function setTitle($title) + { $this->title = $title; + + return $this; + } + + /** + * Update the subtitle of the form. + * + * @param string $subtitle + */ + public function setSubtitle($subtitle) + { + $this->subtitle = $subtitle; + return $this; } /** * Adds a new MyRadioFormField to this MyRadioForm. You should initialise a new MyRadioFormField and pass the object - * straight into the parameter of this method - * @param \MyRadioFormField $field The new MyRadioFormField to add to this MyRadioForm - * @return \MyRadioForm Returns this MyRadioForm for easy chaining + * straight into the parameter of this method. + * + * @param MyRadioFormField $field The new MyRadioFormField to add to this MyRadioForm + * + * @return MyRadioForm + * * @throws MyRadioException Thrown if there are duplicate fields with the same name */ - public function addField(MyRadioFormField $field) { + public function addField(MyRadioFormField $field) + { //Sanity check - is this name in use foreach ($this->fields as $f) { if ($f->getName() === $field->getName()) { - throw new MyRadioException('Tried to create a duplicate MyRadioFormField ' . $f->getName()); + throw new MyRadioException('Tried to create a duplicate MyRadioFormField '.$f->getName()); } } $this->fields[] = $field; + return $this; } /** - * Allows you to update a MyRadioFormField contained within this object with a new value to be used when rendering - * @param String $fieldname The unique name of the MyRadioFormField to edit - * @param mixed $value The new value of the MyRadioFormField. The variable type depends on the MyRadioFormField type - * @return void + * Allows you to update a MyRadioFormField contained within this object with a new value to be used when rendering. + * + * @param string $fieldname The unique name of the MyRadioFormField to edit + * @param mixed $value The new value of the MyRadioFormField. The type depends on the MyRadioFormField type + * * @throws MyRadioException When trying to update a MyRadioFormField that is not attached to this MyRadioForm */ - public function setFieldValue($fieldname, $value) { + public function setFieldValue($fieldname, $value) + { $name = explode('.', $fieldname)[0]; foreach ($this->fields as $k => $field) { if ($field->getName() === $name) { $this->fields[$k]->setValue($value, $fieldname); + return $this; } } - throw new MyRadioException('Cannot set value for field ' . $fieldname . ' as it does not exist.'); + throw new MyRadioException('Cannot set value for field '.$fieldname.' as it does not exist.'); + return $this; } /** * Sets this MyRadioForm as an editing form - it will take existing values and render them for editing and updating. - * + * * This methods sets all TYPE_FILE fields to not required - it is assumed that they are not needed for editing. - * - * @param mixed $identifier Usually a primary key, something unique that the receiving controller will use to know - * which instance of an entry is being updated - * @param Array $values A key=>value array of input names and their values. These will literally be sent to setFieldValue - * iteratively - * @param String action If set, will replace the default Form action. - * + * + * @param mixed $ident Usually a primary key, something unique that the receiving controller will use to know + * which instance of an entry is being updated + * @param array $values A key=>value array of input names and their values. These will literally be sent to + * setFieldValue iteratively + * @param string action If set, will replace the default Form action. + * * Note: This method should only be called once in the object's lifetime */ - public function editMode($identifier, $values, $action = null) { - $this->addField(new MyRadioFormField('myradiofrmedid', MyRadioFormField::TYPE_HIDDEN, array('value' => $identifier))); + public function editMode($ident, $values, $action = null) + { + $this->addField(new MyRadioFormField('myradiofrmedid', MyRadioFormField::TYPE_HIDDEN, ['value' => $ident])); foreach ($values as $k => $v) { $this->setFieldValue($k, $v); @@ -234,51 +288,77 @@ public function editMode($identifier, $values, $action = null) { } /** - * Renders a page using the template engine - * @param Array $frmcustom An optional array of custom fields to send to the Renderer. Useful when using a custom - * template which needs additional data. + * Sets the values of this form to the given data. Note that this does not set the other fields needed to make it + * an "edit" form - if you want that, you likely want {@link editMode}. + * @param $values array + * @return self + */ + public function setValues($values) + { + foreach ($values as $k => $v) { + if ($k === 'id') { + // You probably don't want this. + continue; + } + $this->setFieldValue($k, $v); + } + return $this; + } + + /** + * Renders a page using the template engine. + * + * @param array $frmcustom An optional array of custom fields to send to the Renderer. Useful when using a custom + * template which needs additional data. */ - public function render($frmcustom = array()) { - /** + public function render($frmcustom = []) + { + /* * Prevent XSRF attacks with this token - if this isn't present or is * different, then the request is invalid. */ if (!isset($_SESSION['myradio-xsrf-token'])) { $_SESSION['myradio-xsrf-token'] = bin2hex(openssl_random_pseudo_bytes(128)); } - $this->addField(new MyRadioFormField('__xsrf-token', MyRadioFormField::TYPE_HIDDEN, ['value' => $_SESSION['myradio-xsrf-token']])); - - /** + $this->addField(new MyRadioFormField( + '__xsrf-token', + MyRadioFormField::TYPE_HIDDEN, + ['value' => $_SESSION['myradio-xsrf-token']] + )); + + /* * If we need to do a captcha, load the requirements */ if ($this->captcha) { - require_once 'Classes/Vendor/recaptchalib.php'; - $captcha = recaptcha_get_html(Config::$recaptcha_public_key, null, true); + $captcha = '
' + .''; } else { $captcha = null; } - $fields = array(); - $redact = array(); + $fields = []; + $redact = []; foreach ($this->fields as $field) { $fields[] = $field->render(); - /** + /* * Password fields should be redacted from any * logging output. Printing request data should use * CoreUtils::getRequestInfo */ - if ($field->getType() === MyRadioFormField::TYPE_PASSWORD or - $field->getRedacted()) { - $redact[] = $this->getPrefix() . $field->getName(); + if ($field->getType() === MyRadioFormField::TYPE_PASSWORD + or $field->getRedacted() + ) { + $redact[] = $this->getPrefix().$field->getName(); } } $twig = CoreUtils::getTemplateObject()->setTemplate($this->template) ->addVariable('frm_name', $this->name) ->addVariable('frm_classes', $this->getClasses()) - ->addVariable('frm_action', CoreUtils::makeURL($this->module, $this->action)) + ->addVariable('frm_action', URLUtils::makeURL($this->module, $this->action)) ->addVariable('frm_method', $this->get ? 'get' : 'post') ->addVariable('title', isset($this->title) ? $this->title : $this->name) + ->addVariable('subtitle', isset($this->subtitle) ? $this->subtitle : '') ->addVariable('serviceName', isset($this->module) ? $this->module : $this->name) ->addVariable('frm_fields', $fields) ->addVariable('redact', $redact) @@ -288,10 +368,12 @@ public function render($frmcustom = array()) { } /** - * Returns a space-seperated String of classes applying to this MyRadioForm, ready to render - * @return String a space-seperated list of classes + * Returns a space-seperated String of classes applying to this MyRadioForm, ready to render. + * + * @return string a space-seperated list of classes */ - private function getClasses() { + private function getClasses() + { $classes = 'myradiofrm'; foreach ($this->classes as $class) { $classes .= " $class"; @@ -299,50 +381,56 @@ private function getClasses() { return $classes; } - + /** - * Get the field name prefix + * Get the field name prefix. */ - private function getPrefix() { - return $this->name . '-'; + private function getPrefix() + { + return $this->name.'-'; } /** - * Processes data submitted from this MyRadioForm, returning an Array of the values - * @return Array An array of form data that was submitted using this form definition - * or false if a captcha was requested and is incorrect. + * Processes data submitted from this MyRadioForm, returning an Array of the values. + * + * @return array An array of form data that was submitted using this form definition + * or false if a captcha was requested and is incorrect. */ - public function readValues() { + public function readValues() + { + CoreUtils::checkUploadPostSize(); + //If there was a captcha, verify it if ($this->captcha) { - require_once 'Classes/Vendor/recaptchalib.php'; - if (!recaptcha_check_answer(Config::$recaptcha_private_key, - $_SERVER['REMOTE_ADDR'], - $_REQUEST['recaptcha_challenge_field'], - $_REQUEST['recaptcha_response_field'] - )->is_valid) { + $valid = AuthUtils::verifyRecaptcha($_POST['g-recaptcha-response'], $_SERVER['REMOTE_ADDR']); + if ($valid !== true) { return false; } } - - $return = array(); + + $return = []; foreach ($this->fields as $field) { $value = $field->readValue($this->getPrefix()); - if ($field->getRequired() && empty($value)) { - throw new MyRadioException('Field ' . $field->getName() . ' is required - but has not been set.', 400); + if ($field->getRequired() && empty($value) && $value !== 0) { + throw new MyRadioException( + 'Field '.$field->getName().' is required but has not been set.', + 400 + ); } $return[$field->getName()] = $value; } //Edit Mode requests - if (isset($_REQUEST[$this->getPrefix() . 'myradiofrmedid'])) { - $return['id'] = (int) $_REQUEST[$this->getPrefix() . 'myradiofrmedid']; + if (isset($_REQUEST[$this->getPrefix().'myradiofrmedid'])) { + $tempID = $_REQUEST[$this->getPrefix().'myradiofrmedid']; + $return['id'] = is_numeric($tempID) ? (int) $tempID : $tempID; } //XSRF check - if ($_REQUEST[$this->getPrefix().'__xsrf-token'] !== $_SESSION['myradio-xsrf-token']) { - throw new MyRadioException('Invalid submission token. Possible XSRF attack.', 500); + if (!isset($_SESSION['myradio-xsrf-token']) + || $_REQUEST[$this->getPrefix().'__xsrf-token'] !== $_SESSION['myradio-xsrf-token'] + ) { + throw new MyRadioException('Session expired (Invalid token). Please refresh the page.', 401); } + return $return; } - } diff --git a/src/Classes/MyRadio/MyRadioFormField.php b/src/Classes/MyRadio/MyRadioFormField.php index 803d0c50a..0e6af68b2 100644 --- a/src/Classes/MyRadio/MyRadioFormField.php +++ b/src/Classes/MyRadio/MyRadioFormField.php @@ -1,79 +1,81 @@ + * A collection of these is automatically created when building a MyRadioForm. */ -class MyRadioFormField { +class MyRadioFormField +{ /** * The constant used to specify this MyRadioFormField should be a standard text field. - * + * * A text field can take the following custom options: - * + * * minlength: The minimum number of characters the user must enter for this to be valid input - * + * * maxlength: The maximum number of characters the user can enter for this to be valid input - * + * * placeholder: Placeholder text that is cleared when the input takes focus. */ - const TYPE_TEXT = 0x00; /** * The constant used to specify this MyRadioFormField should be a standard number field. - * + * * A number field can take the following custom options: - * + * * min: The lowest number the user must enter for this to be valid input - * + * * max: The highest number the user can enter for this to be valid input */ const TYPE_NUMBER = 0x01; /** - * The constant used to specify this MyRadioFormField must be a text field that validates as an email address - * + * The constant used to specify this MyRadioFormField must be a text field that validates as an email address. + * * The email field takes no custom options. */ const TYPE_EMAIL = 0x02; /** - * The constant used to specify this MyRadioFormField must be a valid date, and provides a datepicker widget for it - * + * The constant used to specify this MyRadioFormField must be a valid date, and provides a datepicker widget for it. + * * The date field currently takes no custom options. - * + * * @todo Support for mindate and maxdate */ const TYPE_DATE = 0x03; /** - * The constant used to specify this MyRadioFormField must be a valid date and time, providing a datetime widget for it - * + * The constant used to specify this MyRadioFormField must be a valid date and time, + * and provides a datetime widget for it. + * * The datetime field currently takes no custom options. * NOTE: Currently, the TIME aspect must be in 15 minute intervals - * + * * @todo Support for a custom time interval - * * @todo Support for mindate and maxdate - * * @todo Support for mintime and maxtime */ const TYPE_DATETIME = 0x04; /** - * The constant used to specify this MyRadioFormField must be a valid member, providing a Member autocomplete for it. + * The constant used to specify this MyRadioFormField must be a valid member, + * and provides a Member autocomplete for it. * This actually renders two fields - the visible one the user can enter a name into, and a hidden one that will * store the ID once it has been selected. - * + * * The member field takes the following custom options: - * + * * membername: Since value will set the hidden integer value, this can be used to set the text value of the visible * element when loading a pre-filled form. - * + * * @todo Support for only displaying this year's members in the search query */ const TYPE_MEMBER = 0x05; @@ -81,140 +83,143 @@ class MyRadioFormField { * The constant used to specify this MyRadioFormField must be a valid track, providing a Track autocomplete for it. * This actually renders two fields - the visible one the user can enter a track into, and a hidden one that will * store the ID once it has been selected. The value option takes a MyRadio_Track object. - * + * * The track field takes the following custom options: - * + * * trackname: Since value will set the hidden integer value, this can be used to set the text value of the visible * element when loading a pre-filled form. - * + * digitised: Require that search results are digitised tracks. Default false. + * * @todo Support for filtering to only digitised, clean tracks etc. */ const TYPE_TRACK = 0x06; /** - * The constant used to specify this MyRadioFormField must be a valid artist, providing an Artist autocomplete for it. + * The constant used to specify this MyRadioFormField must be a valid artist, + * providing an Artist autocomplete for it. * This actually renders two fields - the visible one the user can enter an artist into, and a hidden one that will * store the ID once it has been selected. - * + * * The artist field takes the following custom options: - * + * * artistname: Since value will set the hidden integer value, this can be used to set the text value of the visible * element when loading a pre-filled form. - * + * * @todo This currently doesn't work right as the Artists system needs some significant backend changes */ const TYPE_ARTIST = 0x07; /** * The constant used to specify this MyRadioFormField must be a standard HTML hidden field type. - * + * * The hidden field takes no custom options. */ const TYPE_HIDDEN = 0x08; /** * The constant used to specify this MyRadioFormField must be a standard HTML select field. - * - * The Custom Options property for this MyRadioFormField type is an Array of items in the select list, each defined as - * follows: - * + * + * The Custom Options property for this MyRadioFormField type is an Array of items in the select list, each defined + * as follows: + * * value: The value of the select option. - * + * * disabled: If true, this option cannot be selected (default false) - * + * * text: The human-readable value of the option that is displayed in the select dropdown. */ const TYPE_SELECT = 0x09; /** * The constant used to specify this MyRadioFormField must be a set of standard HTML radio fields. - * - * The Custom Options property for this MyRadioFormField type is an Array of items in the Radio list, each defined as - * follows: - * + * + * The Custom Options property for this MyRadioFormField type is an Array of items in the Radio list, each defined + * as follows: + * * value: The value of the radio option. - * + * * disabled: If true, this option cannot be selected (default false) - * + * * text: The human-readable value of this option that is displayed next to the radio button */ const TYPE_RADIO = 0x0A; /** * The constant used to specify this MyRadioFormField must be a check box. - * + * * This field type does *not* use the value field, due to the way defaults work. - * + * * The Custom Options this MyRadioFormField uses are: - * + * * checked: Whether or not this checkbox is checked by default. Default false. */ const TYPE_CHECK = 0x0B; /** * The constant used to specify this MyRadioFormField must be a select input with the days of the week as options. - * + * * It returns a number from 0-6, with 0 representing Monday. Value can be used to pre-set a day using these numbers. - * + * * This field type does not use any Custom Options. */ const TYPE_DAY = 0x0C; /** * The constant used to specify this MyRadioFormField should be a textarea with rich text input. - * + * * The following Custom Options are supported by this MyRadioFormField type: - * + * * minlength: The minimum number of characters the user must enter. This will include inserted HTML tags by the RTE. - * + * * maxlength: The maximum number of characters the user may enter. This will include inserted HTML tags by the RTE. - * + * * @todo Support custom # of rows and columns */ const TYPE_BLOCKTEXT = 0x0D; /** * The constant used to specify this MyRadioFormField should be a text field that only accepts a time input. * NOTE: This currently only accepts time entries at 15 minute intervals. + * * @todo Support for custom time intervals - * + * * This MyRadioFormField type does not support any Custom Options. */ const TYPE_TIME = 0x0E; /** - * The constant used to specify this MyRadioFormField should be a group of checkbox MyRadioFormFields grouped within a - * a single fieldset. This provides the advantage of Select All and Select None links and a generally more organised - * feel. - * - * The Custom Options field for this MyRadioFormField field type is an Array of MyRadioFormFields of the Checkbox type - * which are to be rendered within this MyRadioFormField. + * The constant used to specify this MyRadioFormField should be a group of checkbox MyRadioFormFields grouped within + * a single fieldset. This provides the advantage of Select All and Select None links and a generally more + * organised feel. + * + * The Custom Options field for this MyRadioFormField field type is an Array of MyRadioFormFields of the Checkbox + * type which are to be rendered within this MyRadioFormField. */ const TYPE_CHECKGRP = 0x0F; /** * The constant used to specify this MyRadioFormField creates a fieldset. * This should be closed with a TYPE_SECTION_CLOSE. - * + * * This MyRadioFormField type does not support any Custom Options + * * @todo Collapsible? */ const TYPE_SECTION = 0x10; /** - * The constant used to specify this MyRadioFormField should be a container for a set of repeating MyRadioFormFields. - * By default these render in a tabular layout. - * - * The Custom Options field for this MyRadioFormField field type is an Array of MyRadioFormFields of any singular type. - * This means that CHECKGRP, SECTION and other similar field types are not supported by this MyRadioFormField Type - * and may have... interesting... results. + * The constant used to specify this MyRadioFormField should be a container for a set of repeating + * MyRadioFormFields. By default these render in a tabular layout. + * + * The Custom Options field for this MyRadioFormField field type is an Array of MyRadioFormFields of any singular + * type. This means that CHECKGRP, SECTION and other similar field types are not supported by this MyRadioFormField + * Type and may have... interesting... results. */ const TYPE_TABULARSET = 0x11; /** * The constant used to specify this MyRadioFormField should be a file upload. - * + * * The file field takes the following custom options: - * + * * progress: If true, will display an upload progress bar. - * */ const TYPE_FILE = 0x12; /** * The constant used to specify this MyRadioFormField must be a valid album, providing an Album autocomplete for it. * This actually renders two fields - the visible one the user can enter an artist into, and a hidden one that will * store the ID once it has been selected. - * + * * The album field takes the following custom options: - * + * * albumname: Since value will set the hidden integer value, this can be used to set the text value of the visible * element when loading a pre-filled form. */ @@ -223,11 +228,11 @@ class MyRadioFormField { /** * The constant used to specify this MyRadioFormField should be a checkbox matrix formatted to * generate an appointment selector on a standard week. - * + * * The value of this form field should be a 2D array of 0 or more time ranges, of the format:
* {day: 1, start_time: 3600, end_time: 7200}
* Which is Monday, from 1am to 2am (times are seconds since midnight). - * + * * This field type takes no custom options. * This field type does not support repeating or tabular sets. * This field type does not support the "required" parameter. @@ -236,110 +241,146 @@ class MyRadioFormField { /** * The constant used to specify this MyRadioFormField should be a password field. - * This MyRadioFormField type does not support any Custom Options + * This MyRadioFormField type does not support any Custom Options. */ const TYPE_PASSWORD = 0x15; /** * The constant used to specify this MyRadioFormField closes a fieldset. - * + * * This MyRadioFormField type does not support any Custom Options */ const TYPE_SECTION_CLOSE = 0x16; /** - * The name/id of the Form Field - * @var string + * The name/id of the Form Field. + * + * @var string */ private $name; /** - * The type of the form field + * The type of the form field. + * * @var int */ private $type; /** - * Whether input in this field is required + * Whether input in this field is required. + * * @var bool */ private $required = true; /** - * The label of the field (null = use name) + * The label of the field (null = use name). + * * @var string */ private $label = null; /** - * Helpful text explaining the form field + * Helpful text explaining the form field. + * * @var string */ private $explanation = ''; /** - * Whether the form element should be visible + * Whether the form element should be visible. + * * @var bool */ private $display = true; /** - * Additional classes to add to the field + * Additional classes to add to the field. + * * @var array */ - private $classes = array(); + private $classes = []; /** - * For selects, radios and checkboxes only - the options to display + * For selects, radios and checkboxes only - the options to display. + * * @var 2D Array as defined - * {display: 'Value to Display', enabled: true} + * {display: 'Value to Display', enabled: true} */ - private $options = array(); + private $options = []; /** - * The value of the form field - * @var mixed + * The value of the form field. + * + * @var mixed */ private $value = null; /** * Whether the field is enabled/disabled by default - * Actually renders as readonly in most cases + * Actually renders as readonly in most cases. + * * @var bool */ private $enabled = true; - + /** * Whether the field is redacted by default. + * * @var bool */ private $redacted = false; /** - * Settings that cannot be altered by the $options parameter - * @var array + * Placeholder for the field + * + * @var mixed */ - private $restricted_attributes = array('restricted_attributes', 'name', 'type'); + private $placeholder = null; + + /** + * Settings that cannot be altered by the $options parameter. + * + * @var array + */ + private $restricted_attributes = ['restricted_attributes', 'name', 'type']; /** * Set up a new MyRadio Form Field with the new parameters, returning the new field. - * This method is only useful practically when the MyRadioFormField is inserted to a MyRadioForm - * @param String $name The name and id of the field, as used in the HTML properties - should be unique to the form - * '.' IS A RESERVED CHARACTER! - * @param int $type The MyRadioFormField Field Type to use. See the constants defined in this class for details - * @param Array $options A set of additional settings for the MyRadioFormField as follows (all optional):
- * required: Whether the field is required (default true)
- * label: The human-readable name of the field. (default reuses name)
- * explanation: Help text for the MyRadioFormField (default none)
- * display: Whether the MyRadioFormField should be visible when the page loads (default true)
- * classes: An array of additional classes to add to the input field (default empty)
- * options: An array of additional settings that are specific to the field type (default empty)
- * value: The default value of the field when it is rendered (default none)
- * enabled: Whether the field is enabled when the page is loaded (default true)
- * redacted: If true, this field is hidden in debug output (default false) + * This method is only useful practically when the MyRadioFormField is inserted to a MyRadioForm. + * + * @param string $name The name and id of the field, as used in the HTML properties - should be unique to the + * form '.' IS A RESERVED CHARACTER! '.' IS A RESERVED CHARACTER! + * @param int $type The MyRadioFormField Field Type to use. See the constants defined in this class for + * details + * @param array $options A set of additional settings for the MyRadioFormField as follows (all optional):
+ * required: Whether the field is required (default true)
label: The human-readable name + * of the field. (default reuses name)
+ * explanation: Help text for the MyRadioFormField (default none)
+ * display: Whether the MyRadioFormField should be visible when the page loads + * (default true)
+ * classes: An array of additional classes to add to the input field (default empty)
+ * options: An array of additional settings that are specific to the field type + * (default empty)
+ * value: The default value of the field when it is rendered (default none)
+ * enabled: Whether the field is enabled when the page is loaded (default true)
redacted: + * If true, this field is hidden in debug output (default false) + * required: Whether the field is required (default true)
+ * label: The human-readable name of the field. (default reuses name)
+ * explanation: Help text for the MyRadioFormField (default none)
+ * display: Whether the MyRadioFormField should be visible when the page loads + * (default true)
+ * classes: An array of additional classes to add to the input field (default empty)
+ * options: An array of additional settings that are specific to the field type + * (default empty)
+ * value: The default value of the field when it is rendered (default none)
+ * enabled: Whether the field is enabled when the page is loaded (default true)
+ * redacted: If true, this field is hidden in debug output (default false) + * * @throws MyRadioException If an attempt is made to set an $options value other than those listed above */ - public function __construct($name, $type, $options = array()) { + public function __construct($name, $type, $options = []) + { //Set essential parameters $this->name = $name; $this->type = $type; @@ -352,82 +393,96 @@ public function __construct($name, $type, $options = array()) { foreach ($options as $k => $v) { //Sanity checks - is this a valid parameter and is it not blacklisted? if (isset($this->$k) === false && @$this->$k !== null) { - throw new MyRadioException('Tried to set MyRadioFormField parameter ' . $k . ' but it does not exist.'); + throw new MyRadioException('Tried to set MyRadioFormField parameter '.$k.' but it does not exist.'); } if (in_array($k, $this->restricted_attributes)) { - throw new MyRadioException('Tried to set MyRadioFormField parameter ' . $k . ' but it is not editable.'); + throw new MyRadioException('Tried to set MyRadioFormField parameter '.$k.' but it is not editable.'); } $this->$k = $v; } } /** - * Returns the name property of this MyRadioFormField - * @return String The name of this MyRadioFormField + * Returns the name property of this MyRadioFormField. + * + * @return string The name of this MyRadioFormField */ - public function getName() { + public function getName() + { return $this->name; } /** - * Get if this needs a value + * Get if this needs a value. + * * @return bool */ - public function getRequired() { + public function getRequired() + { return $this->required; } /** - * Get the type of form field + * Get the type of form field. + * * @return int */ - public function getType() { + public function getType() + { return $this->type; } - + /** - * Get if this field is redacted + * Get if this field is redacted. + * * @return bool */ - public function getRedacted() { + public function getRedacted() + { return $this->redacted; } /** * Set whether this field needs a value. + * * @param bool $bool */ - public function setRequired($bool) { + public function setRequired($bool) + { $this->required = $bool; } /** * Merges the given options to the original ones. - * @param Array $options + * + * @param array $options */ - public function setOptions($options) { + public function setOptions($options) + { $this->options = array_merge($this->options, $options); } /** * Sets the value that will be set in this MyRadioFormField. - * + * * In the case of TABULARSETs, $value may be an array of multiple existing values. You must also provide an extended * field name, which is the name of this field, a period '.', and the name of the inner field. - * - * @param mixed $value The value that this MyRadioFormField will be set to. Type depends on $type parameter. - * @param String $subfield For TABULARSETs, this is fieldname.innerfieldname. + * + * @param mixed $value The value that this MyRadioFormField will be set to. Type depends on $type parameter. + * @param string $subfield For TABULARSETs, this is fieldname.innerfieldname. */ - public function setValue($value, $subField = null) { + public function setValue($value, $subField = null) + { if (strpos($subField, '.') !== false) { $subField = explode('.', $subField)[1]; } if ($this->type !== self::TYPE_TABULARSET) { $this->value = $value; + return; } else { foreach ($this->options as $field) { - if (!$field instanceof MyRadioFormField) { + if (!$field instanceof self) { continue; } if ($field->getName() === $subField) { @@ -439,17 +494,19 @@ public function setValue($value, $subField = null) { /** * Returns a space-separated string of classes that apply to this MyRadioFormField - * Includes ui-helper-hidden if the MyRadioFormField is set not to display + * Includes hidden if the MyRadioFormField is set not to display. + * * @return string A space-separated string of classes that apply to this MyRadioFormField */ - private function getClasses() { + private function getClasses() + { $classes = ''; foreach ($this->classes as $class) { $classes .= " $class"; } if (!$this->display) { - $classes .= ' ui-helper-hidden'; + $classes .= ' hidden'; } $classes .= ' myradiofrmfield'; @@ -459,12 +516,14 @@ private function getClasses() { /** * Prepares an Array of parameters ready to be sent to the Templater in order to render this MyRadioFormField in a - * MyRadioForm - * @return Array An array of parameters ready to be used in a Template render call + * MyRadioForm. + * + * @return array An array of parameters ready to be used in a Template render call */ - public function render() { + public function render() + { // If there are MyRadioFormFields in Options, convert these to their render values - $options = array(); + $options = []; foreach ($this->options as $k => $v) { if ($v instanceof self) { $options[$k] = $v->render(); @@ -473,13 +532,13 @@ public function render() { } } - if ($this->type === MyRadioFormField::TYPE_ARTIST) { + if ($this->type === self::TYPE_ARTIST) { $options['artistname'] = $this->value; } - if ($this->type === MyRadioFormField::TYPE_FILE) { + if ($this->type === self::TYPE_FILE) { $options['progress_id'] = uniqid(); $value = null; - } elseif (($this->type === MyRadioFormField::TYPE_TRACK) && !empty($this->value)) { + } elseif (($this->type === self::TYPE_TRACK) && !empty($this->value)) { if (is_array($this->value)) { //Deal with TABULARSETs foreach ($this->value as $k => $v) { if (empty($v)) { @@ -492,7 +551,20 @@ public function render() { $options['trackname'] = $this->value->getTitle(); $value = $this->value->getID(); } - } elseif (($this->type === MyRadioFormField::TYPE_MEMBER) && !empty($this->value)) { + } elseif (($this->type === self::TYPE_ALBUM) && !empty($this->value)) { + if (is_array($this->value)) { //Deal with TABULARSETs + foreach ($this->value as $k => $v) { + if (empty($v)) { + continue; + } + $options['albumname'][$k] = $v->getTitle(); + $value[$k] = $v->getID(); + } + } else { + $options['albumname'] = $this->value->getTitle(); + $value = $this->value->getID(); + } + } elseif (($this->type === self::TYPE_MEMBER) && !empty($this->value)) { if (is_array($this->value)) { //Deal with TABULARSETs foreach ($this->value as $k => $v) { if (empty($v)) { @@ -509,7 +581,7 @@ public function render() { $value = $this->value; } - return array( + return [ 'name' => $this->name, 'label' => ($this->label === null ? $this->name : $this->label), 'type' => $this->type, @@ -518,58 +590,93 @@ public function render() { 'class' => $this->getClasses(), 'options' => $options, 'value' => $value, - 'enabled' => $this->enabled - ); + 'enabled' => $this->enabled, + ]; } /** * To be used when getting values from a submitted form, this method returns the correctly type-cast value of the * MyRadioFormField depending on the $type parameter. - * + * * This is called by MyRadioForm::readValues() - * @param String $prefix The current prefix to the field name + * + * Repeated elements -> TYPE_TABULARSET data has a little quirk. Because PHP, the table data is returned as a + * parent array of each column (not row), with child arrays containing each row value for each column. + * This is probably opposite to what you're thinking. + * + * @param string $prefix The current prefix to the field name + * * @return mixed The submitted field value + * * @throws MyRadioException if the field type does not have a valid read handler - * @todo Verify all returns deal with repeated elements correctly + * + * @todo Verify all returns deal with repeated elements correctly */ - public function readValue($prefix) { - $name = $prefix . str_replace(' ', '_', $this->name); + public function readValue($prefix) + { + $name = $prefix.str_replace(' ', '_', $this->name); //The easiest ones can just be returned switch ($this->type) { case self::TYPE_TEXT: case self::TYPE_EMAIL: case self::TYPE_ARTIST: - case self::TYPE_HIDDEN: + //Deal with Arrays for repeated elements - see function comment. + if (is_array($_REQUEST[$name])) { + $stripped_values = []; + foreach ($_REQUEST[$name] as $field_key => $field_value) { + $stripped_values[$field_key] = strip_tags($field_value); + } + return $stripped_values; + } else { + return strip_tags($_REQUEST[$name]); + } + break; case self::TYPE_BLOCKTEXT: + $dom = new \DOMDocument(); + // We have to wrap the html so that DOMDocument has a root + $dom->loadHtml("
$_REQUEST[$name]
", LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + + $xpath = new \DOMXPath($dom); + while ($node = $xpath->query('//script')->item(0)) { + $node->parentNode->removeChild($node); + } + + // Strip the
...
nodes back off + return substr(trim($dom->saveHTML()), 5, -6); + break; + case self::TYPE_HIDDEN: case self::TYPE_PASSWORD: return $_REQUEST[$name]; break; case self::TYPE_MEMBER: - //Deal with Arrays for repeated elements + //Deal with Arrays for repeated elements - see function comment. if (is_array($_REQUEST[$name])) { - for ($i = 0; $i < sizeof($_REQUEST[$name]); $i++) { + for ($i = 0; $i < sizeof($_REQUEST[$name]); ++$i) { if (empty($_REQUEST[$name][$i])) { continue; } $_REQUEST[$name][$i] = MyRadio_User::getInstance($_REQUEST[$name][$i]); } + return $_REQUEST[$name]; } else { if (empty($_REQUEST[$name])) { - return null; + return; } + return MyRadio_User::getInstance($_REQUEST[$name]); } break; case self::TYPE_TRACK: - //Deal with Arrays for repeated elements + //Deal with Arrays for repeated elements - see function comment. if (is_array($_REQUEST[$name])) { - for ($i = 0; $i < sizeof($_REQUEST[$name]); $i++) { + for ($i = 0; $i < sizeof($_REQUEST[$name]); ++$i) { if (empty($_REQUEST[$name][$i])) { continue; } $_REQUEST[$name][$i] = MyRadio_Track::getInstance($_REQUEST[$name][$i]); } + return $_REQUEST[$name]; } else { return MyRadio_Track::getInstance($_REQUEST[$name]); @@ -579,13 +686,14 @@ public function readValue($prefix) { case self::TYPE_SELECT: case self::TYPE_RADIO: case self::TYPE_DAY: - //Deal with Arrays for repeated elements + //Deal with Arrays for repeated elements - see function comment. if (is_array($_REQUEST[$name])) { - for ($i = 0; $i < sizeof($_REQUEST[$name]); $i++) { + for ($i = 0; $i < sizeof($_REQUEST[$name]); ++$i) { if (is_numeric($_REQUEST[$name][$i])) { $_REQUEST[$name][$i] = (int) $_REQUEST[$name][$i]; } } + return $_REQUEST[$name]; } else { if (is_numeric($_REQUEST[$name])) { @@ -598,11 +706,12 @@ public function readValue($prefix) { case self::TYPE_DATE: case self::TYPE_DATETIME: case self::TYPE_TIME: - //Deal with repeated elements + //Deal with repeated elements - see function comment. if (is_array($_REQUEST[$name])) { - for ($i = 0; $i < sizeof($_REQUEST[$name]); $i++) { + for ($i = 0; $i < sizeof($_REQUEST[$name]); ++$i) { $_REQUEST[$name][$i] = $this->convertTime($_REQUEST[$name][$i]); } + return $_REQUEST[$name]; } else { return $this->convertTime($_REQUEST[$name]); @@ -612,52 +721,105 @@ public function readValue($prefix) { return (bool) (isset($_REQUEST[$name]) && ($_REQUEST[$name] === 'On' || $_REQUEST[$name] === 'on')); break; case self::TYPE_CHECKGRP: - $return = array(); + $return = []; foreach ($this->options as $option) { - $return[$option->getName()] = (int) $option->readValue($name . '-'); + $return[$option->getName()] = (int) $option->readValue($name.'-'); } + return $return; break; case self::TYPE_FILE: return $_FILES[$name]; break; case self::TYPE_TABULARSET: - $return = array(); + $return = []; + $clearNulls = []; foreach ($this->options as $option) { + if ($option->getType() === self::TYPE_DAY) { + $clearNulls[] = $option->getName(); + } $return[$option->getName()] = $option->readValue($prefix); } + + $fields = array_keys($return); + + //Explicitly set Days to null if the rest of the row is + //0 is treated as empty, so let's clear that up and advise using is_null + foreach ($clearNulls as $field) { + foreach ($return[$field] as $i => $v) { + if ($v > 0) { + continue; + } + $hasValue = false; + foreach ($fields as $other) { + if ($other !== $field && !is_null($return[$other][$i])) { + $hasValue = true; + break; + } + } + if (!$hasValue) { + $return[$field][$i] = null; + } + } + } + + //Clear rows that are entirely null + $removeKeys = []; + for ($i = 0; $i < sizeof($return[$fields[0]]); ++$i) { + $hasValue = false; + foreach ($fields as $field) { + if (!is_null($return[$field][$i])) { + $hasValue = true; + break; + } + } + if (!$hasValue) { + $removeKeys[] = $i; + } + } + if (!empty($removeKeys)) { + foreach ($fields as $field) { + foreach ($removeKeys as $key) { + unset($return[$field][$key]); + } + //Reset indexes + $return[$field] = array_values($return[$field]); + } + } + return $return; break; case self::TYPE_SECTION: case self::TYPE_SECTION_CLOSE: - return null; + return; break; case self::TYPE_ALBUM: - //Deal with Arrays for repeated elements + //Deal with Arrays for repeated elements - see function comment. if (is_array($_REQUEST[$name])) { - for ($i = 0; $i < sizeof($_REQUEST[$name]); $i++) { + for ($i = 0; $i < sizeof($_REQUEST[$name]); ++$i) { if (empty($_REQUEST[$name][$i])) { continue; } $_REQUEST[$name][$i] = MyRadio_Album::getInstance($_REQUEST[$name][$i]); } + return $_REQUEST[$name]; } else { return MyRadio_Album::getInstance($_REQUEST[$name]); } break; case self::TYPE_WEEKSELECT: - /** - * Now isn't this fun. The week select is comprised of 336 checkboxes that we need to amalgamate. - * Value returned are relative to days starting at midnight, not 7am. - * Selections spanning multiple days will return as two seperate selections ('days' here again being midnight) - */ + /* Now isn't this fun. The week select is comprised of 336 checkboxes that we need to amalgamate. Value + * returned is relative to days starting at midnight, not 7am. Selections spanning multiple days will + * return as two seperate selections ('days' here again being midnight) */ $times = []; $active_day = null; $active_time = null; - for ($i = 1; $i <= 7; $i++) { //Iterate over each day - for ($j = 0; $j < 86400; $j+=1800) { //Iterate over each 30 minute interval - if (strtolower($_REQUEST[$name . '-' . $i . '-' . $j]) === 'on') { + for ($i = 1; $i <= 7; ++$i) { //Iterate over each day + for ($j = 0; $j < 86400; $j += 1800) { //Iterate over each 30 minute interval + if (isset($_REQUEST[$name.'-'.$i.'-'.$j]) + && strtolower($_REQUEST[$name.'-'.$i.'-'.$j]) === 'on' + ) { //Is there already an active selection? If so, carry on. if ($active_day !== null) { //Yep, nothing to see here. @@ -685,26 +847,38 @@ public function readValue($prefix) { $active_time = null; } } + return $times; break; default: - throw new MyRadioException('Field type ' . $this->type . ' does not have a valid value interpreter definition.'); + throw new MyRadioException( + 'Field type ' . $this->type . ' does not have a valid value interpreter definition.' + ); } } - private function convertTime($timeString) { + private function convertTime($timeString) + { + if ($timeString === '') { + return; + } /* For why we need to do this, consult the notes at * http://php.net/manual/en/function.strtotime.php. - * YES, PHP IS RETARDED. - */ + * YES, PHP IS RETARDED. */ $timeString = str_replace('/', '-', $timeString); $time = (int) strtotime($timeString); //Times should be seconds since midnight *any* day if ($this->type === self::TYPE_TIME) { $time -= strtotime('Midnight'); + + // Handle timezones savings pushing this over the day boundary + if ($time >= 86400) { + $time -= 86400; + } elseif ($time < 0) { + $time += 86400; + } } return $time; } - } diff --git a/src/Classes/MyRadio/MyRadioLDAPAuthenticator.php b/src/Classes/MyRadio/MyRadioLDAPAuthenticator.php index 61b88a21b..c05b48994 100644 --- a/src/Classes/MyRadio/MyRadioLDAPAuthenticator.php +++ b/src/Classes/MyRadio/MyRadioLDAPAuthenticator.php @@ -1,35 +1,52 @@ */ -class MyRadioLDAPAuthenticator implements MyRadioAuthenticator { +class MyRadioLDAPAuthenticator implements \MyRadio\Iface\MyRadioAuthenticator +{ private $ldap_handle; - + /** - * Sets up the LDAP connection + * Sets up the LDAP connection. */ - public function __construct() { + public function __construct() + { $this->ldap_handle = ldap_connect(Config::$auth_ldap_server); } /** - * Tears down the LDAP connection + * Tears down the LDAP connection. */ - public function __destruct() { + public function __destruct() + { ldap_close($this->ldap_handle); } /** - * @param String $user The username (a full email address, or the prefix - * if it matches Config::$eduroam_domain). - * @param String $password The provided password. + * @param string $user The username (a full email address, or the prefix if it matches Config::$eduroam_domain). + * if it matches Config::$eduroam_domain). + * @param string $password The provided password. + * * @return MyRadio_User|false Map the credentials to a MyRadio User on success, or - * return false on failure. + * return false on failure. */ - public function validateCredentials($user, $password) { - if (@ldap_bind($this->ldap_handle, 'uid=' . $user . ',' . Config::$auth_ldap_root, $password)) { + public function validateCredentials($user, $password) + { + $myruser = MyRadio_User::findByEmail($user); + if ($myruser == null) { + return false; + } + $eduroam = $myruser->getEduroam(); + # Check that it looks like a legit user first. + if (!empty(Config::$auth_ldap_regex) && !preg_match(Config::$auth_ldap_regex, $eduroam)) { + return false; + } + if (@ldap_bind($this->ldap_handle, 'uid='.$eduroam.','.Config::$auth_ldap_root, $password)) { return MyRadio_User::findByEmail($user); } else { return false; @@ -38,44 +55,55 @@ public function validateCredentials($user, $password) { /** * This authenticator does not add any extra permissions to the ones already * defined internally. - * - * @param String $user The username (a full email address, or the prefix - * if it matches Config::$eduroam_domain). - * @return Array A list of IDs for the permission flags this user should be - * granted. These are in addition to the ones computed by MyRadio - * internally. + * + * @param string $user The username (a full email address, or the prefix + * if it matches Config::$eduroam_domain). + * + * @return array A list of IDs for the permission flags this user should be + * granted. These are in addition to the ones computed by MyRadio + * internally. */ - public function getPermissions($user) { + public function getPermissions($user) + { return []; } /** * This authenticator can not process password resets. - * - * @param String $user The username (a full email address, or the prefix - * if it matches Config::$eduroam_domain). - * @return boolean Whether the reset has happened or not. MyRadio will stop - * attempting resets once one Authenticator has return true. + * + * @param string $user The username (a full email address, or the prefix if it matches Config::$eduroam_domain). + * if it matches Config::$eduroam_domain). + * + * @return bool Whether the reset has happened or not. MyRadio will stop + * attempting resets once one Authenticator has return true. */ - public function resetAccount($user) { + public function resetAccount($user) + { return false; } - - public function getFriendlyName() { + + public function getFriendlyName() + { return Config::$auth_ldap_friendly_name; } - - public function getDescription() { - return 'By choosing this option, we will always use your '. - $this->getFriendlyName().' username and password to log you in.' - . ' Whenever you change your '.$this->getFriendlyName(). - ' password, your '.Config::$short_name. - ' password will also change.'; + + public function getDescription() + { + return 'By choosing this option, we will always use your ' + .$this->getFriendlyName().' username and password to log you in.' + .' Whenever you change your '.$this->getFriendlyName() + .' password, your '.Config::$short_name + .' password will also change.'; } - - public function getResetFormMessage() { - return '
Have you tried using your '. - $this->getFriendlyName().' username and password? If you can\'t remember your '. - $this->getFriendlyName().' login, please click here.
'; + + public function getResetFormMessage() + { + return '
Have you tried using your ' + .$this->getFriendlyName() + .' username and password? If you can\'t remember your ' + .$this->getFriendlyName() + .' login, please click here.
'; } } + diff --git a/src/Classes/MyRadio/MyRadioMenu.php b/src/Classes/MyRadio/MyRadioMenu.php index b18232297..49e3a779d 100644 --- a/src/Classes/MyRadio/MyRadioMenu.php +++ b/src/Classes/MyRadio/MyRadioMenu.php @@ -1,194 +1,181 @@ - * @version 20130930 - * @package MyRadio_Core - * @uses \CacheProvider - * @uses \Database - * @uses \CoreUtils + * Abstractor for the MyRadio Menu. + * + * @uses \CacheProvider + * @uses \Database + * @uses \AuthUtils + * @uses \URLUtils */ -class MyRadioMenu { - +class MyRadioMenu +{ /** - * Returns a customised MyRadio menu for the *currently logged in* user - * @param \MyRadio_User $user The currently logged in User's User object - * @return Array A complex Menu array array array array array + * Returns a customised MyRadio menu for the *currently logged in* user. + * + * @return array A complex Menu array array array array array */ - public function getMenuForUser(MyRadio_User $user) { + public function getMenuForUser() + { $full = $this->getFullMenu(); //Iterate over the Full Menu, creating a user menu - $menu = array(); + $menu = []; foreach ($full as $column) { - $newColumn = array('title' => $column['title'], 'sections' => array()); + $newColumn = ['title' => $column['title'], 'sections' => []]; foreach ($column['sections'] as $section) { - $items = array(); + $items = []; foreach ($section['items'] as $item) { if ($this->userHasPermission($item)) { $items[] = $item; } } //Add this section (if it has anything in it) - if (!empty($items)) - $newColumn['sections'][] = array('title' => $section['title'], 'items' => $items); + if (!empty($items)) { + $newColumn['sections'][] = ['title' => $section['title'], 'items' => $items]; + } } - if (!empty($newColumn['sections'])) + if (!empty($newColumn['sections'])) { $menu[] = $newColumn; + } } return $menu; } /** - * Returns the entire MyRadio Main Menu structure - * @todo Better Documentation + * Returns the entire MyRadio Main Menu structure. + * + * @return array An array that can be used by getMenuForUser() to build the menu */ - private function getFullMenu() { - $db = Database::getInstance(); - //First, columns - $columns = $db->fetch_all('SELECT columnid, title FROM myury.menu_columns - ORDER BY position ASC'); - //Now, sections - $sections = $db->fetch_all('SELECT sectionid, columnid, title FROM myury.menu_sections - ORDER BY position ASC'); - //And finally, items - $items = array_merge( - $db->fetch_all('SELECT itemid, sectionid, title, url, description FROM myury.menu_links ORDER BY title ASC'), $db->fetch_all('SELECT sectionid, template FROM myury.menu_twigitems') - ); - //Get permissions for each $item - foreach ($items as $key => $item) { - /** - * Secret: Some descriptions always reference the officer that *previously* - * held the position, not currently. - */ - if (isset($items[$key]['description'])) { - if (strstr($items[$key]['description'], '#MACRO_SM-1') !== false) { - $hist = MyRadio_Officer::getInstance(1)->getHistory(); - $n = 0; - while (sizeof($hist) - 1 > $n && $hist[$n]['User']->getName() === $hist[0]['User']->getName()) { - $n++; - } - $items[$key]['description'] = str_replace(['#MACRO_SM-1'], [$hist[$n]['User']->getName()], $items[$key]['description']); - } - if (strstr($items[$key]['description'], '#MACRO_PC-1') !== false) { - $hist = MyRadio_Officer::getInstance(106)->getHistory(); - $n = 0; - while (sizeof($hist) - 1 > $n && $hist[$n]['User']->getName() === $hist[0]['User']->getName()) { - $n++; - } - $items[$key]['description'] = str_replace(['#MACRO_PC-1'], [$hist[$n]['User']->getName()], $items[$key]['description']); - } - } + private function getFullMenu() + { + $data = json_decode(@file_get_contents('Menus/menu.json', FILE_USE_INCLUDE_PATH), true); - if (!isset($item['itemid'])) - continue; //Skip twigitems - $items[$key] = array_merge($items[$key], $this->breakDownURL($item['url'])); + if (is_null($data)) { + throw new MyRadioException('Menu file not found', 500); + } else { + $columns = $data['columns']; } - //That'll do for now. Time to make the $menu - $menu = array(); - foreach ($columns as $column) { - $newColumn = array('title' => $column['title'], 'sections' => array()); - - //Iterate over each section - foreach ($sections as $section) { - if ($section['columnid'] != $column['columnid']) - continue; - //This section is for this column - $newItems = array(); - //Iterate over each item - foreach ($items as $item) { - if ($item['sectionid'] != $section['sectionid']) - continue; - //Item is for this section - $newItems[] = $item; + foreach ($columns as $ckey => $column) { + foreach ($column['sections'] as $skey => $section) { + foreach ($section['items'] as $key => $item) { + if (empty($item['template'])) { + $columns[$ckey]['sections'][$skey]['items'][$key] = array_merge( + $section['items'][$key], + $this->breakDownURL($item['url']) + ); + } } - $newColumn['sections'][] = array('title' => $section['title'], 'items' => $newItems); } - - $menu[] = $newColumn; } - return $menu; + + return $columns; } /** * Gets all items for a module's submenu and puts them in an array. - * @param int $moduleid The id of the module to get items for - * @return Array An array that can be used by getSubMenuForUser() to build a submenu - * @todo Caching here breaks submenus + * + * @param string $module The name of the module to get items for + * + * @return array An array that can be used by getSubMenuForUser() to build a submenu */ - private function getFullSubMenu($moduleid) { - $db = Database::getInstance(); - - $items = $db->fetch_all('SELECT menumoduleid, title, url, description FROM myury.menu_module - WHERE moduleid=$1 ORDER BY title ASC', array($moduleid)); - //Get permissions for each $item - foreach ($items as $key => $item) { - $items[$key] = array_merge($items[$key], $this->breakDownURL($item['url'])); + private function getFullSubMenu($module) + { + if ($module == "MyRadio") { + // bypass for the MyRadio module, which doesn't have header menus + return []; + } + + $menu = json_decode(@file_get_contents('Menus/'.$module.'.json', FILE_USE_INCLUDE_PATH), true); + + if (is_null($menu)) { + $items = []; + } else { + $items = $menu['menu']; + + //Get permissions for each $item + foreach ($items as $key => $item) { + $items[$key] = array_merge($items[$key], $this->breakDownURL($item['url'])); + } } + return $items; } /** - * Takes a $url database column entry, and breaks it into its components - * @param String $url A database-fetched menu item URL - * @return Array with four keys - 'url', 'module', 'action'. All are the String names, not IDs. + * Takes a $url database column entry, and breaks it into its components. + * + * @param string $url A database-fetched menu item URL + * + * @return array with four keys - 'url', 'module', 'action'. All are the String names, not IDs. */ - private function breakDownURL($url) { - return array( + private function breakDownURL($url) + { + return [ 'url' => $this->parseURL($url), 'module' => $this->parseURL($url, 'module'), - 'action' => $this->parseURL($url, 'action') - ); + 'action' => $this->parseURL($url, 'action'), + ]; } /** - * Check if user has permission to see this menu item - * @param Array $item A MyRadioMenu Menu Item to check permissions for. Should have been passed through - * breadDownURL() previously. - * @return boolean Whether the user can see this item + * Check if user has permission to see this menu item. + * + * @param array $item A MyRadioMenu Menu Item to check permissions for. Should have been passed through + * breadDownURL() previously. + * + * @return bool Whether the user can see this item */ - private function userHasPermission($item) { + private function userHasPermission($item) + { return empty($item['action']) or - CoreUtils::requirePermissionAuto($item['module'], $item['action'], false); + AuthUtils::requirePermissionAuto($item['module'], $item['action'], false); } /** * @todo Document - * @param type $moduleid - * @param \MyRadio_User $user The currently logged in User's User object + * + * @param type $module + * * @return array */ - public function getSubMenuForUser($moduleid, MyRadio_User $user) { - $full = $this->getFullSubMenu($moduleid); + public function getSubMenuForUser($module) + { + $full = $this->getFullSubMenu($module); //Iterate over the Full Menu, creating a user menu - $menu = array(); + $menu = []; foreach ($full as $item) { if ($this->userHasPermission($item)) { $menu[] = $item; } } + return $menu; } /** * Detects module/action links and rewrites - * This is a method so it can easily be changed if Apache rewrites - * @param String $url The URL to parse + * This is a method so it can easily be changed if Apache rewrites. + * + * @param string $url The URL to parse + * * @todo Rewrite this to make sense */ - private function parseURL($url, $return = 'url') { + private function parseURL($url, $return = 'url') + { $exp = explode(',', $url); $module = str_replace('module=', '', $exp[0], $count); @@ -210,8 +197,9 @@ private function parseURL($url, $return = 'url') { } } else { //It's not a rewritable - if ($return !== 'url') - return null; + if ($return !== 'url') { + return; + } } if ($return === 'module') { return $module; @@ -223,9 +211,8 @@ private function parseURL($url, $return = 'url') { } } - $url = $count === 1 ? CoreUtils::makeURL($module, $action, $params) : $url; + $url = $count === 1 ? URLUtils::makeURL($module, $action, $params) : $url; + return $url; } - } - diff --git a/src/Classes/MyRadio/MyRadioNews.php b/src/Classes/MyRadio/MyRadioNews.php index 4714e386f..ee35e9918 100644 --- a/src/Classes/MyRadio/MyRadioNews.php +++ b/src/Classes/MyRadio/MyRadioNews.php @@ -1,94 +1,166 @@ - * @version 20130718 - * @package MyRadio_Core - * @uses \CacheProvider - * @uses \Database - * @uses \CoreUtils - * @todo Refactor to classes and ServiceAPI + * @todo Refactor to classes and ServiceAPI */ -class MyRadioNews { - - public function __construct() { - +class MyRadioNews +{ + public function __construct() + { } - + /** - * Returns all items in the given feed - * @param int $newsfeedid + * Returns all items in the given feed. + * + * @param int $newsfeedid The ID of the feed to get */ - public static function getFeed($newsfeedid, MyRadio_User $user = null, $revoked = false) { + public static function getFeed($newsfeedid, MyRadio_User $user = null, $revoked = false) + { $data = []; - foreach (Database::getInstance()->fetch_column( - 'SELECT newsentryid FROM public.news_feed' - . ' WHERE feedid=$1' .($revoked ? '' : ' AND revoked=false'), - [$newsfeedid]) as $row) { + foreach (Database::getInstance()->fetchColumn( + 'SELECT newsentryid FROM public.news_feed' + .' WHERE feedid=$1'.($revoked ? '' : ' AND revoked=false'), + [$newsfeedid] + ) as $row) { $data[] = self::getNewsItem($row, $user); } + return $data; } /** - * Returns the latest news item for the given feed, and if given a user, the timestamp of when they saw it - * @param id $newsfeedid The ID of the newsfeed to check - * @param MyRadio_User $user The User object to check if seen. Default null, won't return a seen column. - * @return Array + * Returns the latest news item for the given feed, and if given a user, the timestamp of when they saw it. + * + * @param id $newsfeedid The ID of the newsfeed to check + * @param MyRadio_User $user The User object to check if seen. Default null, won't return a seen column. + * + * @return array */ - public static function getLatestNewsItem($newsfeedid, MyRadio_User $user = null) { - return self::getNewsItem( - Database::getInstance()->fetch_column('SELECT newsentryid FROM public.news_feed - WHERE public.news_feed.feedid=$1 AND revoked=false - ORDER BY timestamp DESC LIMIT 1', [$newsfeedid])[0], $user - ); + public static function getLatestNewsItem($newsfeedid, MyRadio_User $user = null) + { + $newsentry = Database::getInstance()->fetchOne( + 'SELECT newsentryid FROM public.news_feed + WHERE public.news_feed.feedid=$1 AND revoked=false + ORDER BY timestamp DESC', + [$newsfeedid] + ); + + if (empty($newsentry)) { + return; + } + + return self::getNewsItem($newsentry['newsentryid'], $user); } - - public static function getNewsItem($newsentryid, MyRadio_User $user = null) { + + public static function getNewsItem($newsentryid, MyRadio_User $user = null) + { $db = Database::getInstance(); - $news = $db->fetch_one('SELECT newsentryid, fname || \' \' || sname AS author, timestamp AS posted, content - FROM public.news_feed, public.member - WHERE newsentryid=$1 - AND news_feed.memberid = member.memberid', array($newsentryid)); + $news = $db->fetchOne( + 'SELECT newsentryid, fname || \' \' || sname AS author, timestamp AS posted, content + FROM public.news_feed, public.member + WHERE newsentryid=$1 + AND news_feed.memberid = member.memberid', + [$newsentryid] + ); if (empty($news)) { - return null; + return; } - return array_merge($news, array('seen' => $db->fetch_one('SELECT seen FROM public.member_news_feed - WHERE newsentryid=$1 AND memberid=$2 LIMIT 1', array($news['newsentryid'], empty($user) ? 0 : $user->getID())), - 'posted' => CoreUtils::happyTime($news['posted']) - )); + return array_merge( + $news, + [ + 'seen' => $db->fetchOne( + 'SELECT seen FROM public.member_news_feed + WHERE newsentryid=$1 AND memberid=$2 LIMIT 1', + [ + $news['newsentryid'], + empty($user) ? 0 : $user->getID(), + ] + ), + 'posted' => CoreUtils::happyTime($news['posted']), + ] + ); } /** * @todo Document this - * @param type $newsentryid + * + * @param type $newsentryid * @param MyRadio_User $user */ - public static function markNewsAsRead($newsentryid, MyRadio_User $user) { + public static function markNewsAsRead($newsentryid, MyRadio_User $user) + { $db = Database::getInstance(); try { - $db->query('INSERT INTO public.member_news_feed (newsentryid, memberid) VALUES ($1, $2)', array($newsentryid, $user->getID())); + $db->query( + 'INSERT INTO public.member_news_feed (newsentryid, memberid) VALUES ($1, $2)', + [$newsentryid, $user->getID()] + ); } catch (MyRadioException $e) { - }; //Can sometimes get duplicate key errors } - - public static function addItem($feedid, $content) { - Database::getInstance()->query('INSERT INTO public.news_feed' - . ' (feedid, memberid, content) VALUES' - . ' ($1, $2, $3)', [$feedid, $_SESSION['memberid'], $content]); + + public static function addItem($feedid, $content, $memberid = 1) + { + // is there an active session? + if (MyRadio_User::getCurrentUser() !== null) { + $memberid = $_SESSION['memberid']; + } + + Database::getInstance()->query( + 'INSERT INTO public.news_feed' + .' (feedid, memberid, content) VALUES' + .' ($1, $2, $3)', + [$feedid, $memberid, $content] + ); } + public static function getForm() + { + return ( + new MyRadioForm( + 'myradio_news', + 'MyRadio', + 'addNews', + [ + 'title' => 'Add news item', + ] + ) + )->addField( + new MyRadioFormField( + 'body', + MyRadioFormField::TYPE_BLOCKTEXT, + [ + 'explanation' => '', + 'label' => 'Content', + ] + ) + )->addField( + new MyRadioFormField( + 'feedid', + MyRadioFormField::TYPE_HIDDEN + ) + ); + } } - diff --git a/src/Classes/MyRadio/MyRadioNullSession.php b/src/Classes/MyRadio/MyRadioNullSession.php new file mode 100644 index 000000000..1014cb9a7 --- /dev/null +++ b/src/Classes/MyRadio/MyRadioNullSession.php @@ -0,0 +1,48 @@ +db = null; + } + + /** + * Clear up old session entries in the database + * This should be called automatically by PHP every one in a while. + */ + public function gc($lifetime) + { + return true; + } + + /** + * Reads the session data from the database. If no data exists, creates an + * empty row. + */ + public function read($id) + { + return false; + } + + /** + * Writes changes to the session data to the database. + */ + public function write($id, $data) + { + return !empty($id); + } + + /** + * Deletes the session entry from the database. + */ + public function destroy($id) + { + return !empty($id); + } +} diff --git a/src/Classes/MyRadio/MyRadioSession.php b/src/Classes/MyRadio/MyRadioSession.php index 4c8e3d13d..0caac25f3 100644 --- a/src/Classes/MyRadio/MyRadioSession.php +++ b/src/Classes/MyRadio/MyRadioSession.php @@ -1,69 +1,94 @@ */ -class MyRadioSession { - +class MyRadioSession implements \SessionHandlerInterface +{ const TIMEOUT = 7200; //Session expires after 2hrs private $db; - public static function factory() { + public static function factory() + { if (isset($_SESSION)) { session_write_close(); } - return new self(); + + return new static(); } - public function __construct() { + public function __construct() + { $this->db = Database::getInstance(); } - public function open($id) { + public function open($save_path, $sesion_name): bool + { return true; } - public function close() { + public function close(): bool + { return true; } /** * Clear up old session entries in the database - * This should be called automatically by PHP every one in a while + * This should be called automatically by PHP every one in a while. */ - public function gc($lifetime) { - $this->db->query('DELETE FROM sso_session WHERE timestamp<$1', - array(CoreUtils::getTimestamp(time() - $lifetime))); + public function gc($lifetime): int|false + { + $this->db->query( + 'DELETE FROM sso_session WHERE timestamp<$1', + [CoreUtils::getTimestamp(time() - $lifetime)] + ); + return true; } /** * Reads the session data from the database. If no data exists, creates an - * empty row + * empty row. */ - public function read($id) { + public function read($id): string|false + { if (empty($id)) { return false; } - $result = $this->db->fetch_column('SELECT data FROM sso_session - WHERE id=$1 LIMIT 1', array($id)); + // Use transaction to fix duplicate race condition on session storm. + $this->db->query('BEGIN'); + $result = $this->db->fetchColumn( + 'SELECT data FROM sso_session + WHERE id=$1 LIMIT 1', + [$id] + ); if (empty($result)) { - $this->db->query('INSERT INTO sso_session (id, data, timestamp) - VALUES ($1, \'\', $2)', array($id, CoreUtils::getTimestamp())); - return ''; + $this->db->query( + 'INSERT INTO sso_session (id, data, timestamp) + VALUES ($1, \'\', NOW()) ON CONFLICT DO NOTHING', + [$id] + ); } + $this->db->query('COMMIT'); - return $result[0]; + if (empty($result)) { + return ''; + } else { + return $result[0]; + } } /** - * Writes changes to the session data to the database + * Writes changes to the session data to the database. */ - public function write($id, $data) { + public function write($id, $data): bool + { if (empty($id)) { return false; } @@ -71,20 +96,24 @@ public function write($id, $data) { return true; } $result = $this->db->query( - 'UPDATE sso_session SET data=$2, timestamp=NOW() - WHERE id=$1', array($id, $data)); - return ($result !== false); + 'UPDATE sso_session SET data=$2, timestamp=NOW() + WHERE id=$1', + [$id, $data] + ); + + return $result !== false; } /** - * Deletes the session entry from the database + * Deletes the session entry from the database. */ - public function destroy($id) { + public function destroy($id): bool + { if (empty($id)) { return false; } - $this->db->query('DELETE FROM sso_session WHERE id=$1', array($id)); + $this->db->query('DELETE FROM sso_session WHERE id=$1', [$id]); + return true; } - } diff --git a/src/Classes/MyRadio/MyRadio_Availability.php b/src/Classes/MyRadio/MyRadio_Availability.php new file mode 100644 index 000000000..cca41fbd9 --- /dev/null +++ b/src/Classes/MyRadio/MyRadio_Availability.php @@ -0,0 +1,418 @@ +id = (int) $id; + + if ($result === null) { + $result = self::$db->fetchOne( + 'SELECT * FROM '.$this->availability_table.' WHERE '.$this->id_field.'=$1', + [$id] + ); + } + if (empty($result)) { + throw new MyRadioException('Availability '.$id.' does not exist!'); + } + + $this->created_by = MyRadio_User::getInstance($result['memberid']); + $this->approved_by = empty($result['approvedid']) ? null : MyRadio_User::getInstance($result['approvedid']); + $this->effective_from = strtotime($result['effective_from']); + $this->effective_to = empty($result['effective_to']) ? null : strtotime($result['effective_to']); + + //Make times be in seconds since midnight + $this->timeslots = array_map( + function ($x) { + return [ + 'id' => $x['id'], 'day' => $x['day'], + 'start_time' => strtotime($x['start_time'], 0), + 'end_time' => strtotime($x['end_time'], 0), + ]; + }, + self::$db->fetchAll( + 'SELECT id, day, start_time, end_time, \'order\' FROM '.$this->timeslot_table.' + WHERE '.$this->id_field.'=$1', + [$this->id] + ) + ); + } + + /** + * Returns data about the Availability. + * + * @param array $mixins Mixins + * @mixin timeslots Provides data about the timeslots in this campaign + * + * @return array + */ + public function toDataSource($mixins = []) + { + $mixin_funcs = [ + 'timeslots' => function (&$data) { + $data['timeslots'] = $this->getTimeslots(); + }, + ]; + + $data = [ + 'id' => $this->getID(), + 'created_by' => $this->getCreatedBy()->getID(), + 'approved_by' => ($this->getApprovedBy() == null) ? null : $this->getApprovedBy()->getID(), + 'effective_from' => CoreUtils::happyTime($this->getEffectiveFrom()), + 'effective_to' => ($this->getEffectiveTo() === null) ? + 'Never' : CoreUtils::happyTime($this->getEffectiveTo()), + 'num_timeslots' => sizeof($this->getTimeslots()), + ]; + + $this->addMixins($data, $mixins, $mixin_funcs); + + return $data; + } + + /** + * Get the ID of the Availability. + * + * @return int + */ + public function getID() + { + return $this->id; + } + + /** + * Get the User that created this Availability. + * + * @return MyRadio_User + */ + public function getCreatedBy() + { + return $this->created_by; + } + + /** + * Get the User that approved this Availability. + * + * @return MyRadio_User + */ + public function getApprovedBy() + { + return $this->approved_by; + } + + /** + * Get the time (as epoch int) that this Availability starts. + * + * @return int + */ + public function getEffectiveFrom() + { + return $this->effective_from; + } + + /** + * Get the time (as epoch int) that this Availability ends. + * Returns null if the Availability does not end. + * + * @return int + */ + public function getEffectiveTo() + { + return $this->effective_to; + } + + /** + * Get an array of times during the Active period that the Availability is visible on the Website. + * + * @return array [[day: 1, start_time: 0, end_time: 86399], ...] + */ + public function getTimeslots() + { + return $this->timeslots; + } + + /** + * Returns a MyRadioForm filled in and ripe for being used to edit this Availability. + * + * @return MyRadioForm + */ + public function getEditForm() + { + return $this->getForm($this->banner->getID()) + ->editMode( + $this->getID(), + [ + 'timeslots' => $this->getTimeslots(), + 'effective_from' => CoreUtils::happyTime($this->getEffectiveFrom()), + 'effective_to' => $this->getEffectiveTo() === null ? null : + CoreUtils::happyTime($this->getEffectiveTo()), + ] + ); + } + + /** + * Return if this Availability is currently active. That is, it has started and has not expired. + * It returns true even when there isn't currently a Timeslot for the Availaibility running. + * + * @return bool + */ + public function isActive() + { + return $this->effective_from <= time() && ($this->effective_to == null or $this->effective_to > time()); + } + + /** + * Removes all timeslots associated with a Availability. + * + * Used when editing, as they are then immediately added again. + */ + public function clearTimeslots() + { + $this->timeslots = []; + self::$db->query('DELETE FROM '.$this->timeslot_table.' WHERE '.$this->id_field.'=$1', [$this->getID()]); + $this->updateCacheObject(); + } + + /** + * Sets the start time of the Availability. + * + * @param int $time + * + * @return MyRadio_BannerCampaign + */ + public function setEffectiveFrom($time) + { + $this->effective_from = $time; + self::$db->query( + 'UPDATE '.$this->availability_table.' SET effective_from=$1 WHERE '.$this->id_field.'=$2', + [CoreUtils::getTimestamp($time), $this->getID()] + ); + $this->updateCacheObject(); + + return $this; + } + + /** + * Sets the end time of the Campaign. + * + * @param int $time + * + * @return MyRadio_BannerCampaign + */ + public function setEffectiveTo($time) + { + if ($time === null) { + $this->effective_to = $time; + self::$db->query( + 'UPDATE '.$this->availability_table.' SET effective_to=NULL WHERE '.$this->id_field.'=$1', + [$this->getID()] + ); + } else { + self::$db->query( + 'UPDATE '.$this->availability_table.' SET effective_to=$1 WHERE '.$this->id_field.'=$2', + [CoreUtils::getTimestamp($time), $this->getID()] + ); + } + + $this->updateCacheObject(); + + return $this; + } + + /** + * Adds an Active Timeslot to the Campaign. + * + * @param int $day Day the timeslot is on. 1 = Monday, 7 = Sunday. Timeslots cannot span days. + * @param int $start Seconds since midnight that the Timeslot starts. + * @param int $end Seconds since midnight that the Timeslot ends. + * + * @todo Input validation. + */ + public function addTimeslot($day, $start, $end) + { + $start = gmdate('H:i:s', $start).'+00'; + $end = gmdate('H:i:s', $end).'+00'; + + $id = self::$db->fetchColumn( + 'INSERT INTO '.$this->timeslot_table.' + ('.$this->id_field.', memberid, approvedid, day, start_time, end_time) + VALUES ($1, $2, $2, $3, $4, $5) RETURNING id', + [ + $this->getID(), + MyRadio_User::getInstance()->getID(), + $day, + $start, + $end, + ] + )[0]; + + $this->timeslots[] = [ + 'id' => $id, + 'day' => $day, + 'start_time' => strtotime($start, 0), + 'end_time' => strtotime($end, 0), + ]; + + $this->updateCacheObject(); + } + + /** + * Get all Banner Campaigns. + * + * @return MyRadio_BannerCampaign[] + */ + public static function getAllAvailabilities() + { + return self::resultSetToObjArray( + self::$db->fetchColumn( + 'SELECT '.$this->id_field.' FROM '.$this->availability_table + ) + ); + } + + /** + * Returns the form needed to create or edit Availabilities. + * + * @return MyRadioForm + */ + protected static function getForm($module, $action) + { + return ( + new MyRadioForm( + 'availabilityfrm', + $module, + $action + ) + ) + ->addField( + new MyRadioFormField( + 'effective_from', + MyRadioFormField::TYPE_DATETIME, + [ + 'required' => true, + 'value' => CoreUtils::happyTime(time()), + 'label' => 'Start Time', + 'explanation' => 'The time from which this Availability becomes active.', + ] + ) + ) + ->addField( + new MyRadioFormField( + 'effective_to', + MyRadioFormField::TYPE_DATETIME, + [ + 'required' => false, + 'label' => 'End Time', + 'explanation' => 'The time at which this Availability becomes inactive. Leaving this blank means' + .' the Availability will continue indefinitely.', + ] + ) + ) + ->addField( + new MyRadioFormField( + 'timeslots', + MyRadioFormField::TYPE_WEEKSELECT, + [ + 'label' => 'Timeslots', + 'explanation' => 'All times filled in on this schedule (i.e. are purple) are times during the' + .' week that this Availability is considered active, and therefore appears on the website.' + .' Click a square to toggle it. Click and drag to select lots at once!', + ] + ) + ); + } +} diff --git a/src/Classes/MyRadio/MyRadio_Daemon.php b/src/Classes/MyRadio/MyRadio_Daemon.php index 390764a3d..918b812cf 100644 --- a/src/Classes/MyRadio/MyRadio_Daemon.php +++ b/src/Classes/MyRadio/MyRadio_Daemon.php @@ -1,18 +1,33 @@ - * @package MyRadio_Core - */ - -/** - * A method object that constructs a MyRadioForm given an array representation - * of its specification. - * - * This is used by the FormLoaders to create a form after it has been parsed. - * - * @version 20140113 - * @author Matt Windsor - * @package MyRadio_Core - */ -class MyRadio_FormConstructor { - /** - * The prefix for strings that signify special processing directives. - * @const string - */ - const SPECIAL_PREFIX = '!'; - - /** - * The name of the MyRadio module to which the form will submit. - * @var string - */ - private $module; - - /** - * The name of the MyRadio action to which the form will submit. - * @var string - */ - private $action; - - /** - * A list of current bindings of variables in the form. - * @var array - */ - private $bindings; - - /** - * The internal form representation, ready to render to a form. - * @var array - */ - private $form_array; - - /** - * The form being constructed. - * @var MyRadioForm - */ - private $form; - - /** - * Constructs a new MyRadio_FormConstructor. - * - * @param array $form_array The array representation of the form. - * @param array $bindings A map of bindings of variables to substitute - * into the form wherever a !bind directive is - * found. - * @param string $module The module to which the form will submit. - * @param string $action The action to which the form will submit. - */ - public function __construct( - array $form_array, - array $bindings, - /* string */ $module, - /* string */ $action - ) { - $this->form_array = $form_array; - $this->bindings = $bindings; - $this->module = $module; - $this->action = $action; - $this->form = null; - } - - /** - * Constructs a MyRadioForm from its array representation. - * - * @return MyRadioForm The finished form. - */ - public function toForm() { - $this->makeBareForm(); - $this->addInitialFields(); - return $this->form; - } - - /** - * Constructs a bare form with no fields. - * - * @return null Nothing. - */ - private function makeBareForm() { - $this->form = new MyRadioForm( - $this->form_array['name'], - $this->module, - $this->action, - $this->form_array['options'] - ); - } - - /** - * Adds the fields in the form specification to the form. - * - * @return null Nothing. - */ - private function addInitialFields() { - foreach($this->form_array['fields'] as $name => $field) { - $this->addFieldToForm($name, $field, $this->bindings); - } - } - - /** - * Compiles a field description into a field and adds it to the given form. - * - * @param string $name The name of the field. - * @param array $field The field description array to compile. - * @param array $bindings The set of variable bindings to give to - * the field constructor. - * - * @return Nothing. - */ - public function addFieldToForm( - /* string */ $name, - array $field, - array $bindings - ) { - return $this->getFieldConstructorClass($name) - ->newInstanceArgs([$name, $field, $this, $bindings]) - ->make(); - } - - /** - * Deduces the appropriate form field constructor to use for a field. - * - * @param string $name The name of the field. - * - * @return ReflectionClass A reflection class for the field constructor. - */ - private function getFieldConstructorClass(/* string */ $name) { - if ($this->isSpecialFieldName($name)) { - $class = 'MyRadio_SpecialFormFieldConstructor'; - } else { - $class = 'MyRadio_NormalFormFieldConstructor'; - } - return new ReflectionClass($class); - } - - /** - * Constructs and adds a fully built field to the form. - * - * @param string $name The name of the field. - * @param int $type The type enumerator of the field. - * @param array $options The options to pass to the field constructor. - * - * @return null Nothing. - */ - public function constructAndAddField($name, $type, array $options) { - $this->form->addField( - new MyRadioFormField($name, $type, $options) - ); - } - - /** - * Determines whether a field name denotes a special field. - * - * @param string $name The field name. - * - * @return boolean True if the field is special; false otherwise. - */ - public function isSpecialFieldName(/* string */ $name) { - return is_string($name) && (strpos($name, self::SPECIAL_PREFIX) === 0); - } -} - diff --git a/src/Classes/MyRadio/MyRadio_FormFieldConstructor.php b/src/Classes/MyRadio/MyRadio_FormFieldConstructor.php deleted file mode 100644 index cbffb32bb..000000000 --- a/src/Classes/MyRadio/MyRadio_FormFieldConstructor.php +++ /dev/null @@ -1,72 +0,0 @@ - - * @package MyRadio_Core - */ - - -/** - * A method object that constructs a field given a name and array - * representation of its specification. - * - * @version 20140113 - * @author Matt Windsor - * @package MyRadio_Core - */ -class MyRadio_FormFieldConstructor { - /** - * The name of the field being constructed. - * @var string - */ - protected $name; - - /** - * The field specification. - * @var array - */ - protected $field; - - /** - * The binding array. - * @var array - */ - protected $bindings; - - /** - * The form constructor. - * @var MyRadio_FormConstructor - */ - protected $fc; - - /** - * A ReflectionClass used to introspect the form field class. - * @var ReflectionClass - */ - protected $rc; - - /** - * Constructs a new MyRadio_FormFieldConstructor. - * - * @param string $name The field name. - * @param array $field The field specification. - * @param MyRadio_FormConstructor $fc The form constructor. - */ - public function __construct( - /* string */ $name, - array $field, - MyRadio_FormConstructor $fc, - array $bindings - ) { - $this->name = $name; - $this->field = $field; - $this->fc = $fc; - $this->bindings = $bindings; - $this->rc = new ReflectionClass('MyRadioFormField'); - } -} - -?> diff --git a/src/Classes/MyRadio/MyRadio_JsonFormLoader.php b/src/Classes/MyRadio/MyRadio_JsonFormLoader.php deleted file mode 100644 index 3ef6b073c..000000000 --- a/src/Classes/MyRadio/MyRadio_JsonFormLoader.php +++ /dev/null @@ -1,131 +0,0 @@ - - * @package MyRadio_Core - */ - -/** - * A loader for forms written declaratively in JSON format. - * - * The format is thus: - * { - * 'name': 'form_name', - * 'options': { ... }, - * 'fields': { - * 'field_name': { - * 'type': 'constant name without TYPE_, case insensitive', - * 'label': 'etc etc', - * 'options': { ... } - * } - * } - * } - * - * @version 20130428 - * @author Matt Windsor - * @package MyRadio_Core - */ -class MyRadio_JsonFormLoader { - /** - * The name of the current MyRadio module. - * @var string - */ - private $module; - - /** - * The internal form representation, ready to render to a form. - * @var array - */ - private $form_array; - - /** - * Constructs a new MyRadio_JsonFormLoader. - * - * @param string $module The name of the calling MyRadio module. - * - * @return MyRadio_JsonFormLoader - */ - public function __construct($module) { - $this->module = $module; - $this->form_array = null; - } - - /** - * Loads a form from its filename, from the module's forms directory. - * - * @param string $name The (file)name of the form, without the '.json'. - * @return MyRadio_JsonFormLoader this. - */ - public function fromName($name) { - return $this->fromPath( - 'Models/' . $this->module . '/' . $name . '.json' - ); - } - - /** - * Loads a form from its file path. - * - * @param string $path The path to load from. - * @return MyRadio_JsonFormLoader this. - */ - public function fromPath($path) { - return $this->fromString( - file_get_contents($path, true) - ); - } - - /** - * Loads a form from a JSON string. - * - * @param string $str The string to load from. - * @return MyRadio_JsonFormLoader this. - */ - public function fromString($str) { - $this->form_array = json_decode($str, true); - if ($this->form_array === null) { - throw new MyRadioException( - 'Failed to load form from JSON: Code ' . - json_last_error() - ); - } - return $this; - } - - /** - * Compiles a previously loaded form to a form object. - * - * @param string $action The name of the action to trigger on submission. - * @param array $binds The mapping of names used in !bind directives to - * variables. - * - * @return MyRadioForm The processed form. - */ - public function toForm($action, array $binds=[]) { - $fc = new MyRadio_FormConstructor( - $this->form_array, - $binds, - $this->module, - $action - ); - return $fc->toForm(); - } - - /** - * Loads and renders a form from its MyRadio module and name. - * - * This is a convenience wrapper for 'fromPath'. - * - * @param string $module The name of the calling MyRadio module. - * @param string $name The (file)name of the form, without the '.json'. - * @param string $action The name of the action to trigger on submission. - * @return MyRadioForm The processed form. - */ - public static function loadFromModule($module, $name, $action, $binds=[]) { - return ( - new MyRadio_JsonFormLoader($module) - )->fromName($name)->toForm($action, $binds); - } -} - -?> diff --git a/src/Classes/MyRadio/MyRadio_NormalFormFieldConstructor.php b/src/Classes/MyRadio/MyRadio_NormalFormFieldConstructor.php deleted file mode 100644 index 4a24fc4a3..000000000 --- a/src/Classes/MyRadio/MyRadio_NormalFormFieldConstructor.php +++ /dev/null @@ -1,110 +0,0 @@ - - * @package MyRadio_Core - */ - -/** - * A method object that constructs a MyRadioFormField given a name and array - * representation of its specification. - * - * @version 20140113 - * @author Matt Windsor - * @package MyRadio_Core - */ -class MyRadio_NormalFormFieldConstructor extends MyRadio_FormFieldConstructor { - /** - * Builds and attaches the field. - * - * @return null Nothing. - */ - public function make() { - $type = $this->getTypeConstant($this->field['type']); - - // The constructor will complain if these are passed into the parameters - // array. - unset($this->field['name']); - unset($this->field['type']); - - $this->doBinding(); - $this->doMacros(); - - $this->fc->constructAndAddField($this->name, $type, $this->field); - } - - /** - * Performs binding of !bind(foo) strings in a field description to their - * entries in the binding array. - * - * @return null Nothing. - */ - private function doBinding() { - foreach($this->field as $key => &$value) { - if ($this->fc->isSpecialFieldName($value)) { - $value = $this->handlePotentialBinding($value); - } - } - } - - /** - * Performs macro substitution on any string value in the field array. - * - * @return null Nothing. - */ - private function doMacros() { - $index = '#ERROR'; - if (array_key_exists('repeater', $this->bindings)) { - $index = $this->bindings['repeater']; - } - - $macros = [ - '%!SHORTNAME%' => Config::$short_name, - '%!INDEX%' => $index, - '%!%' => '%!' - ]; - - foreach($this->field as $key => &$value) { - foreach($macros as $lhs => $rhs) { - $value = str_replace($lhs, $rhs, $value); - } - } - } - - /** - * Handles a potential instance of !bind(foo). - * - * @param string $input The incoming value. - * - * @return object The value after expanding any bindings. - */ - private function handlePotentialBinding($input) { - $matches = []; - if (preg_match('/^!bind\( *(\w+) *\)$/', $input, $matches)) { - if (!array_key_exists($matches[1], $this->bindings)) { - throw new MyRadioException( - 'Tried to !bind to unbound form variable: ' . $matches[1] . '.' - ); - } - $output = $this->bindings[$matches[1]]; - } - - return $output; - } - - /** - * Infers a type constant (TYPE_XYZ) from a case insensitive name. - * - * @param string $name The name of the type constant. - * - * @return int The type constant. - */ - private function getTypeConstant($name) { - return $this->rc->getconstant('TYPE_' . strtoupper($name)); - } -} - -?> diff --git a/src/Classes/MyRadio/MyRadio_SpecialFormFieldConstructor.php b/src/Classes/MyRadio/MyRadio_SpecialFormFieldConstructor.php deleted file mode 100644 index b8ba34e45..000000000 --- a/src/Classes/MyRadio/MyRadio_SpecialFormFieldConstructor.php +++ /dev/null @@ -1,162 +0,0 @@ - - * @package MyRadio_Core - */ - -/** - * A method object that constructs a special form field given a name and array - * representation of its specification. - * - * @version 20140113 - * @author Matt Windsor - * @package MyRadio_Core - */ -class MyRadio_SpecialFormFieldConstructor extends MyRadio_FormFieldConstructor { - /** - * Builds and attaches the field. - * - * @return null Nothing. - */ - public function make() { - $done = $this->dispatchSpecialFieldHandler(); - if (!$done) { - throw new MyRadioException( - 'Illegal special field name: ' . $this->name . '.' - ); - } - } - - /** - * Attempts to handle the special field by dispatching on its name. - * - * @return boolean True if the field was handled; false otherwise. - */ - private function dispatchSpecialFieldHandler() { - $matches = []; - // TODO: Replace regexes with something a bit less awful. - $operators = [ - '/^!repeat\( *([0-9]+) *, *([0-9]+) *\)$/' => 'addRepeatedFieldsToForm', - '/^!section\((.*)\)$/' => 'addSectionToForm' - ]; - $done = false; - - foreach ($operators as $regex => $callback) { - if (preg_match($regex, $this->name, $matches)) { - call_user_func([$this, $callback], $matches); - $done = true; - } - } - - return $done; - } - - /** - * Adds a repeated field block to a form. - * - * @param array $options The array of options to the !repeat - * directive. This should contain three - * numeric indices: the repeat directive, the - * repetition ID start, and end. - * - * @return Nothing. - */ - private function addRepeatedFieldsToForm(array $options) { - $start = intval($options[1]); - $end = intval($options[2]); - - if ($start >= $end) { - throw new MyRadioException( - 'Start and end wrong way around on !repeat: start=' . - $start . - ', end=' . - $end . - '.' - ); - } - for ($i = $start; $i <= $end; $i++) { - $this->addRepeatedFieldsInstanceToForm($i); - } - } - - /** - * Adds an instance of a repeated field block to a form. - * - * The fields inside the block have the variable 'repeater' bound to the - * current iteration of the repeater. - * - * @param int $iteration The current iteration of the repeater. - * - * @return null Nothing. - */ - private function addRepeatedFieldsInstanceToForm($iteration) { - foreach ($this->field as $name => $infield) { - $new_bindings = array_merge( - ['repeater' => strval($iteration)], - $this->bindings - ); - $new_name = $name . $iteration; - $this->fc->addFieldToForm($new_name, $infield, $new_bindings); - } - } - - /** - * Adds a section to a form. - * - * @param array $options The array of options to the !section directive. - * This should contain one numeric index: the - * section name. - * - * @return null Nothing. - */ - private function addSectionToForm(array $options) { - $this->addSectionHeader($options[1]); - $this->addSectionBody(); - } - - /** - * Adds a section header to a form. - * - * @param string $name The human-readable name of the section. - * - * @return null Nothing. - */ - private function addSectionHeader(/* string */ $name) { - $this->fc->addFieldToForm( - $this->sectionHeaderName($name), - [ - 'type' => 'section', - 'label' => $name, - 'options' => [] - ], - $this->bindings - ); - } - - /** - * Adds a section body to a form. - * - * @return null Nothing. - */ - private function addSectionBody() { - foreach($this->field as $name => $infield) { - $this->fc->addFieldToForm($name, $infield, $this->bindings); - } - } - - /** - * Generates a valid name for a section header form field. - * - * @return string $name A name that should be unique to the section, but - * contains no invalid characters. - */ - private function sectionHeaderName($name) { - return base64_encode($name); - } -} - -?> diff --git a/src/Classes/MyRadio/URLUtils.php b/src/Classes/MyRadio/URLUtils.php new file mode 100644 index 000000000..b66e135df --- /dev/null +++ b/src/Classes/MyRadio/URLUtils.php @@ -0,0 +1,154 @@ + uri mappings of custom web addresses (e.g. /myradio/iTones/default gets mapped to /itones). + * + * @var array + */ + private static $custom_uris = []; + + /** + * Redirects back to previous page. + */ + public static function back() + { + header('Location: '.$_SERVER['HTTP_REFERER']); + } + + public static function backWithMessage($message) + { + header('Location: '.$_SERVER['HTTP_REFERER'] + .(strstr($_SERVER['HTTP_REFERER'], '?') !== false ? '&' : '?').'message='.base64_encode($message)); + } + + /** + * Responds with nocontent. + */ + public static function nocontent() + { + header('HTTP/1.1 204 No Content'); + exit; + } + + /** + * Responds with JSON data. + */ + public static function dataToJSON($data) + { + header('Content-Type: application/json'); + header('HTTP/1.1 200 OK'); + + //Decode to datasource if needed + $data = CoreUtils::dataSourceParser($data); + + $canDisplayErr = Config::$display_errors || AuthUtils::hasPermission(AUTH_SHOWERRORS); + if (!empty(MyRadioError::$php_errorlist) && $canDisplayErr) { + $data['myradio_errors'] = MyRadioError::$php_errorlist; + } + + echo json_encode($data, JSON_NUMERIC_CHECK | JSON_UNESCAPED_SLASHES); + exit; + } + + /** + * Redirects to another page. + * + * @param string $module The module to which we should redirect. + * @param string $action The optional action inside the module to target. + * @param array $params Additional GET variables + */ + public static function redirect($module, $action = null, $params = []) + { + header('Location: '.self::makeURL($module, $action, $params)); + } + + public static function redirectWithMessage($module, $action, $message, $params = []) + { + $params['message'] = base64_encode($message); + self::redirect($module, $action, $params); + } + + /** + * Redirects to another page, specified already by the caller. + * + * @param string $URI The relative URI to redirect to. + */ + public static function redirectURI($URI) + { + header('Location: '.$URI); + } + + /** + * Builds a module/action URL. + * + * @param string $module + * @param string $action + * @param array $params Additional GET variables + * + * @return string URL to Module/Action + */ + public static function makeURL($module, $action = null, $params = []) + { + if (empty(self::$custom_uris) && class_exists('Database')) { + $result = Database::getInstance()->fetchAll('SELECT actionid, custom_uri FROM myury.actions'); + + foreach ($result as $row) { + self::$custom_uris[$row['actionid']] = $row['custom_uri']; + } + } + //Check if there is a custom URL configured + $key = CoreUtils::getActionId( + CoreUtils::getModuleId($module), + empty($action) ? Config::$default_action : $action + ); + if (!empty(self::$custom_uris[$key])) { + return self::$custom_uris[$key]; + } + + if (Config::$rewrite_url) { + $str = Config::$base_url.$module.'/'.(($action !== null) ? $action.'/' : ''); + if (!empty($params)) { + if (is_string($params)) { + if (substr($params, 0, 1) !== '?') { + $str .= '?'; + } + $str .= $params; + } else { + $str .= '?'; + foreach ($params as $k => $v) { + $str .= "$k=$v&"; + } + $str = substr($str, 0, -1); + } + } + } else { + $str = Config::$base_url.'?module='.$module.(($action !== null) ? '&action='.$action : ''); + + if (!empty($params)) { + if (is_string($params)) { + $str .= "&$params"; + } else { + foreach ($params as $k => $v) { + $str .= "&$k=$v"; + } + } + } + } + + return $str; + } +} diff --git a/src/Classes/MyRadioEmail.php b/src/Classes/MyRadioEmail.php index 69536250d..298c4aa46 100644 --- a/src/Classes/MyRadioEmail.php +++ b/src/Classes/MyRadioEmail.php @@ -1,390 +1,483 @@ - * @author Lloyd Wallis - * @version 20130809 - * @package MyRadio_Mail - * @todo Footers contain hard-coded URLs. This used to be necessary (when the links went to mint), but isn't now. */ -class MyRadioEmail extends ServiceAPI { - - // Defaults - /** - * @todo Hardcoded URLs - */ - private static $headers = 'Content-type: text/plain; charset=utf-8'; - private static $sender = 'From: MyRadio '; - private static $footer = 'This email was sent automatically from MyRadio. You can opt out of emails by visiting https://ury.org.uk/myury/Profile/edit/.'; - private static $html_footer = 'This email was sent automatically from MyRadio. You can opt out of emails on your profile page.'; - // Standard - /** - * @var string carriage return + newline - */ - private static $rtnl = "\r\n"; - private static $multipart_boundary = 'muryp2c6cf41f304e3'; - private $email_id; - private $r_lists = array(); - private $r_users = array(); - private $subject; - private $body; - private $body_transformed; - private $multipart = false; - private $from; - private $timestamp; - - protected function __construct($eid) { - self::$db = Database::getInstance(); - - $info = self::$db->fetch_one('SELECT * FROM mail.email WHERE email_id=$1', array($eid)); - - if (empty($info)) { - throw new MyRadioException('Email ' . $eid . ' does not exist!'); - } - - $this->subject = $info['subject']; - $this->body = $info['body']; - $this->from = (empty($info['sender']) ? null : MyRadio_User::getInstance($info['sender'])); - $this->timestamp = strtotime($info['timestamp']); - $this->email_id = $eid; +class MyRadioEmail extends ServiceAPI +{ + /** + * @var string carriage return + newline + */ + private static $rtnl = "\r\n"; + private static $multipart_boundary = 'muryp2c6cf41f304e3'; + private $email_id; + private $r_lists = []; + private $r_users = []; + private $subject; + private $body; + private $body_transformed; + private $multipart = false; + private $from; + private $timestamp; + + protected function __construct($eid) + { + self::$db = Database::getInstance(); + + $info = self::$db->fetchOne('SELECT * FROM mail.email WHERE email_id=$1', [$eid]); + + if (empty($info)) { + throw new MyRadioException('Email '.$eid.' does not exist!'); + } - $this->r_users = self::$db->fetch_column('SELECT memberid FROM mail.email_recipient_member WHERE email_id=$1', array($eid)); + $this->subject = $info['subject']; + $this->body = $info['body']; + $this->from = (empty($info['sender']) ? null : MyRadio_User::getInstance($info['sender'])); + $this->timestamp = strtotime($info['timestamp']); + $this->email_id = $eid; + + $this->r_users = self::$db->fetchColumn( + 'SELECT memberid FROM mail.email_recipient_member WHERE email_id=$1', + [$eid] + ); + $this->r_lists = self::$db->fetchColumn( + 'SELECT listid FROM mail.email_recipient_list WHERE email_id=$1', + [$eid] + ); + + /* + * Check if the body needs to be split into multipart. + * This creates a string with both Text and HTML parts. + */ + $split = strip_tags($this->body); + if ($this->body !== $split) { + //There's HTML in there + $split = \Html2Text\Html2Text::convert($this->body, true); // ignore errors + $this->multipart = true; + $body_transformed = 'This is a MIME encoded message.' + .self::$rtnl.self::$rtnl + .'--'.self::$multipart_boundary + .self::$rtnl + .'Content-Type: text/plain;charset=utf-8' + .self::$rtnl + .'Content-Transfer-Encoding: quoted-printable' + .self::$rtnl.self::$rtnl + .quoted_printable_encode(self::addFooter($split)) + .self::$rtnl.self::$rtnl + .'--'.self::$multipart_boundary + .self::$rtnl + .'Content-Type: text/html;charset=utf-8' + .self::$rtnl + .'Content-Transfer-Encoding: quoted-printable' + .self::$rtnl.self::$rtnl + .quoted_printable_encode(self::addHTMLFooter($this->body)) + .self::$rtnl.self::$rtnl + .'--'.self::$multipart_boundary.'--'; + } else { + $body_transformed = quoted_printable_encode(self::addFooter($this->body)); + } - $this->r_lists = self::$db->fetch_column('SELECT listid FROM mail.email_recipient_list WHERE email_id=$1', array($eid)); + // PHP's mail function doesn't deal with the fact there's a line limit for + // emails for you, other than to mention it's a thing in the docs. + $this->body_transformed = wordwrap($body_transformed, 80, "\r\n", true); + } /** - * Check if the body needs to be split into multipart. - * This creates a string with both Text and HTML parts. + * Create a new email. + * + * @param MyRadio_User $from The User who sent the email. If null, uses no-reply + * @param array $to A 2D array of 'lists' = [l1, l2], 'members' = [m1, m2] + * @param string $subject email subject + * @param string $body email body + * @param int $timestamp Send time. If null, use now. + * @param bool $already_sent If true, all Recipients will be set to having had the email sent. + * @note Use one of the SendToUser* wrapper functions instead of this one. */ - $split = strip_tags($this->body); - if ($this->body !== $split) { - //There's HTML in there - $this->multipart = true; - $this->body_transformed = 'This is a MIME encoded message.' - . self::$rtnl . self::$rtnl . '--' . self::$multipart_boundary . self::$rtnl - . 'Content-Type: text/plain;charset=utf-8' . self::$rtnl . self::$rtnl - . self::addFooter($split) . self::$rtnl . self::$rtnl . '--' . self::$multipart_boundary . self::$rtnl - . 'Content-Type: text/html;charset=utf-8' . self::$rtnl . self::$rtnl - . self::addHTMLFooter($this->body) . self::$rtnl . self::$rtnl . '--' . self::$multipart_boundary . '--'; - } else { - $this->body_transformed = self::addFooter($this->body); - } - } - - /** - * Create a new email - * @param MyRadio_User $from The User who sent the email. If null, uses no-reply - * @param array $to A 2D array of 'lists' = [l1, l2], 'members' = [m1, m2] - * @param String $subject email subject - * @param String $body email body - * @param int $timestamp Send time. If null, use now. - * @param bool $already_sent If true, all Recipients will be set to having had the email sent. - */ - public static function create($to, $subject, $body, $from = null, $timestamp = null, $already_sent = false) { - //Remove duplicate recipients - $to['lists'] = empty($to['lists']) ? [] : array_unique($to['lists']); - $to['members'] = empty($to['members']) ? [] : array_unique($to['members']); - - if (!is_bool($already_sent)) { - $already_sent = false; - } - self::$db = Database::getInstance(); - - if (strlen($body) > 1024000) { - //Woah - that's a big email. Where's this coming from? - //If its more than a couple MB expect this script/service to shortly die due to RAM usage. - $caller = array_shift(debug_backtrace()); - trigger_error('Received long email body: '.strlen($body).' bytes. Source: ' - .$from.'/'.$caller['file'].':'.$caller['line'], E_USER_NOTICE); - } - - $params = array($subject, trim($body)); - if ($timestamp !== null) { - $params[] = CoreUtils::getTimestamp($timestamp); + private static function create($to, $subject, $body, $from = null, $timestamp = null, $already_sent = false) + { + //Remove duplicate recipients + $to['lists'] = empty($to['lists']) ? [] : array_unique($to['lists']); + $to['members'] = empty($to['members']) ? [] : array_unique($to['members']); + + if (!is_bool($already_sent)) { + $already_sent = false; + } + self::$db = Database::getInstance(); + + if (strlen($body) > 1024000) { + //Woah - that's a big email. Where's this coming from? + //If its more than a couple MB expect this script/service to shortly die due to RAM usage. + $caller = array_shift(debug_backtrace()); + trigger_error( + 'Received long email body: '.strlen($body).' bytes. Source: ' + .$from.'/'.$caller['file'].':'.$caller['line'], + E_USER_NOTICE + ); + } + + $params = [$subject, trim($body)]; + if ($timestamp !== null) { + $params[] = CoreUtils::getTimestamp($timestamp); + } + if ($from instanceof ServiceAPI) { + $params[] = $from->getID(); + } + + $eid = self::$db->fetchColumn( + 'INSERT INTO mail.email (subject, body' + .($timestamp !== null ? ', timestamp' : '').($from instanceof ServiceAPI ? ', sender' : '').') + VALUES ($1, $2'.(($timestamp !== null or $from instanceof ServiceAPI) ? ', $3' : '') + .(($timestamp !== null && $from !== null) ? ', $4' : '').') RETURNING email_id', + $params + ); + + $eid = $eid[0]; + + if (empty($eid)) { + throw new MyRadioException('Failed to create email. See previous error.'); + } + + if (!empty($to['lists'])) { + foreach ($to['lists'] as $list) { + if (is_object($list)) { + $list = $list->getID(); + } + self::$db->query( + 'INSERT INTO mail.email_recipient_list (email_id, listid, sent) VALUES ($1, $2, $3)', + [$eid, $list, $already_sent] + ); + } + } + if (!empty($to['members'])) { + foreach ($to['members'] as $member) { + if (is_object($member)) { + $member = $member->getID(); + } + self::$db->query( + 'INSERT INTO mail.email_recipient_member (email_id, memberid, sent) VALUES ($1, $2, $3)', + [$eid, $member, $already_sent] + ); + } + } + + return new self($eid); } - if ($from !== null) { - $params[] = $from->getID(); + + private function getHeader() + { + $headers = ['MIME-Version: 1.0']; + + if ($this->from !== null) { + $headers[] = 'From: '.$this->from->getName().' <'.$this->from->getPublicEmail().'>'; + $headers[] = 'Return-Path: '.$this->from->getPublicEmail(); + } else { + $headers[] = 'From: '.Config::$long_name.' '; + $headers[] = 'Return-Path: no-reply@'.Config::$email_domain; + } + + /* + * !! Multipart headers must be *last* or things Go Badly + */ + if ($this->multipart) { + $headers[] = 'Content-Type: multipart/alternative;boundary='.self::$multipart_boundary; + } else { + $headers[] = 'Content-Type: text/plain; charset=utf-8'; + $headers[] = 'Content-Transfer-Encoding: quoted-printable'; + } + + return implode(self::$rtnl, $headers); } - $eid = self::$db->fetch_column('INSERT INTO mail.email (subject, body' - . ($timestamp !== null ? ', timestamp' : '') . ($from !== null ? ', sender' : '') . ') - VALUES ($1, $2' . (($timestamp !== null or $from !== null) ? ', $3' : '') - . (($timestamp !== null && $from !== null) ? ', $4' : '') . ') RETURNING email_id' - , $params); + private static function addFooter($message) + { + $footer = 'This email was sent automatically from MyRadio. ' + .'You can opt out of emails by visiting '.URLUtils::makeURL('Profile', 'edit').'.'; + return $message.self::$rtnl.self::$rtnl.$footer; + } - $eid = $eid[0]; - - if (empty($eid)) { - throw new MyRadioException('Failed to create email. See previous error.'); + private static function addHTMLFooter($message) + { + $html_footer = 'This email was sent automatically from MyRadio. ' + .'You can opt out of emails on your profile page.'; + return $message.'

'.$html_footer; } - if (!empty($to['lists'])) { - foreach ($to['lists'] as $list) { - if (is_object($list)) { - $list = $list->getID(); + /** + * Actually send the email. + * This should only ever be called by MyRadio_EmailQueueDaemon. + */ + public function send() + { + //Don't send if it's scheduled in the future. + if ($this->timestamp > time()) { + return; } - self::$db->query('INSERT INTO mail.email_recipient_list (email_id, listid, sent) VALUES ($1, $2, $3)', - array($eid, $list, $already_sent)); - } - } - if (!empty($to['members'])) { - foreach ($to['members'] as $member) { - if (is_object($member)) { - $member = $member->getID(); + $this->body_transformed = utf8_encode($this->body_transformed); + foreach ($this->getUserRecipients() as $user) { + if (!$this->getSentToUser($user)) { + //Don't send if the user has opted out + if ($user->getReceiveEmail()) { + $u_subject = trim(str_ireplace('#NAME', $user->getFName(), $this->subject)); + if (substr($u_subject, 0, 1) !== '[') { + $u_subject = '['.Config::$short_name.'] '.$u_subject; + } + $u_message = str_ireplace('#NAME', $user->getFName(), $this->body_transformed); + if (!mail( + $user->getName() . ' <' . $user->getEmail() . '>', + $u_subject, + $u_message, + $this->getHeader() + )) { + continue; + } + } + $this->setSentToUser($user); + } + } + + foreach ($this->getListRecipients() as $list) { + if (!$this->getSentToList($list)) { + foreach ($list->getMembers() as $user) { + //Don't send if the user has opted out + if ($user->getReceiveEmail()) { + $u_subject = str_ireplace('#NAME', $user->getFName(), $this->subject); + $u_message = str_ireplace('#NAME', $user->getFName(), $this->body_transformed); + if (!mail( + $list->getName().' <'.$user->getEmail().'>', + '['.Config::$short_name.'] '.$u_subject, + $u_message, + $this->getHeader() + )) { + continue; + } + } + } + $this->setSentToList($list); + } } - self::$db->query('INSERT INTO mail.email_recipient_member (email_id, memberid, sent) VALUES ($1, $2, $3)', - array($eid, $member, $already_sent)); - } + + return; } - return new self($eid); - } + public function getSentToUser(MyRadio_User $user) + { + $r = self::$db->fetchColumn( + 'SELECT sent FROM mail.email_recipient_member WHERE email_id=$1 AND memberid=$2 LIMIT 1', + [$this->email_id, $user->getID()] + ); + return $r[0] === 't'; + } - private function getHeader() { - $headers = array('MIME-Version: 1.0'); + public function setSentToUser(MyRadio_User $user) + { + self::$db->query( + 'UPDATE mail.email_recipient_member SET sent=\'t\' WHERE email_id=$1 AND memberid=$2', + [$this->email_id, $user->getID()] + ); + $this->updateCacheObject(); + } - if ($this->from !== null) { - $headers[] = 'From: ' . $this->from->getName() . ' <' . $this->from->getEmail() . '>'; - $headers[] = 'Return-Path: ' . $this->from->getEmail(); - } else { - $headers[] = 'From: '.Config::$long_name.' '; - $headers[] = 'Return-Path: no-reply@'.Config::$email_domain; + public function getSentToList(MyRadio_List $list) + { + $r = self::$db->fetchColumn( + 'SELECT sent FROM mail.email_recipient_list WHERE email_id=$1 AND listid=$2 LIMIT 1', + [$this->email_id, $list->getID()] + ); + return $r[0] === 't'; } - /** - * !! Multipart headers must be *last* or things Go Badly - */ - if ($this->multipart) { - $headers[] = 'Content-Type: multipart/alternative;boundary=' . self::$multipart_boundary; - } else { - $headers[] = 'Content-Type: text/plain; charset=utf-8'; + public function setSentToList(MyRadio_List $list) + { + self::$db->query( + 'UPDATE mail.email_recipient_list SET sent=\'t\' WHERE email_id=$1 AND listid=$2', + [$this->email_id, $list->getID()] + ); + $this->updateCacheObject(); } - return implode(self::$rtnl, $headers); - } + /** + * Sends an email to the specified User. + * + * @param MyRadio_User $to + * @param string $subject email subject + * @param string $message email message + * + * @todo Check if "Receive Emails" is enabled for the User + */ + public static function sendEmailToUser(MyRadio_User $to, $subject, $message, MyRadio_User $from = null) + { + self::create(['members' => [$to]], $subject, $message, $from); - private static function addFooter($message) { - return $message . self::$rtnl . self::$rtnl . self::$footer; - } + return true; + } - private static function addHTMLFooter($message) { - return $message . '
' . self::$html_footer; - } + /** + * Sends an email to the specified MyRadio_List. + * + * @param MyRadio_List $to + * @param string $subject email subject + * @param string $message email message + * + * @todo Check if "Receive Emails" is enabled for the User + */ + public static function sendEmailToList(MyRadio_List $to, $subject, $message, MyRadio_User $from = null) + { + if ($from !== null && !$to->hasSendPermission($from)) { + return false; + } + self::create(['lists' => [$to]], $subject, $message, $from); - /** - * Actually send the email - */ - public function send() { - //Don't send if it's scheduled in the future. - if ($this->timestamp > time()) { - return; + return true; } - $this->body_transformed = utf8_encode($this->body_transformed); - foreach ($this->getUserRecipients() as $user) { - if (!$this->getSentToUser($user)) { - //Don't send if the user has opted out - if ($user->getReceiveEmail()) { - $u_subject = str_ireplace('#NAME', $user->getFName(), $this->subject); - $u_message = str_ireplace('#NAME', $user->getFName(), $this->body_transformed); - if (!mail($user->getName() . ' <' . $user->getEmail() . '>', '['.Config::$short_name.'] ' . $u_subject, $u_message, $this->getHeader())) { - continue; - } + + /** + * Sends an email to all the specified Users, with certain customisation abilities: + * #NAME is replaced with the User's first name. + * + * @param array $to An array of User objects + * @param string $subject email subject + * @param sting $message email message + */ + public static function sendEmailToUserSet($to, $subject, $message, MyRadio_User $from = null) + { + foreach ($to as $user) { + if (!($user instanceof MyRadio_User)) { + throw new MyRadioException($user.' is not an instance or derivative of the user class!'); + } } - $this->setSentToUser($user); - } + self::create(['members' => $to], $subject, $message, $from); } - foreach ($this->getListRecipients() as $list) { - if (!$this->getSentToList($list)) { - foreach ($list->getMembers() as $user) { - //Don't send if the user has opted out - if ($user->getReceiveEmail()) { - $u_subject = str_ireplace('#NAME', $user->getFName(), $this->subject); - $u_message = str_ireplace('#NAME', $user->getFName(), $this->body_transformed); - if (!mail($list->getName() . ' <' . $user->getEmail() . '>', '['.Config::$short_name.'] ' . $u_subject, $u_message, $this->getHeader())) { - continue; + /** + * Returns if the User received this email. + * + * Will return true if the email was sent to a mailing list they were + * not a member of at the time. + * + * @param MyRadio_User $user + * + * @return bool + */ + public function isRecipient(MyRadio_User $user) + { + foreach ($this->r_users as $ruser) { + if ($ruser === $user->getID()) { + return true; } - } } - $this->setSentToList($list); - } + foreach ($this->getListRecipients() as $list) { + if ($list->isMember($user->getID())) { + return true; + } + } + + return false; } - return; - } - - public function getSentToUser(MyRadio_User $user) { - $r = self::$db->fetch_column('SELECT sent FROM mail.email_recipient_member WHERE email_id=$1 AND memberid=$2 LIMIT 1', array($this->email_id, $user->getID())); - - return $r[0] === 't'; - } - - public function setSentToUser(MyRadio_User $user) { - self::$db->query('UPDATE mail.email_recipient_member SET sent=\'t\' WHERE email_id=$1 AND memberid=$2', array($this->email_id, $user->getID())); - $this->updateCacheObject(); - } - - public function getSentToList(MyRadio_List $list) { - $r = self::$db->fetch_column('SELECT sent FROM mail.email_recipient_list WHERE email_id=$1 AND listid=$2 LIMIT 1', array($this->email_id, $list->getID())); - - return $r[0] === 't'; - } - - public function setSentToList(MyRadio_List $list) { - self::$db->query('UPDATE mail.email_recipient_list SET sent=\'t\' WHERE email_id=$1 AND listid=$2', array($this->email_id, $list->getID())); - $this->updateCacheObject(); - } - - /** - * Sends an email to the specified User - * @param MyRadio_User $to - * @param string $subject email subject - * @param sting $message email message - * @todo Check if "Receive Emails" is enabled for the User - */ - public static function sendEmailToUser(MyRadio_User $to, $subject, $message, $from = null) { - self::create(array('members' => array($to)), $subject, $message, $from); - return true; - } - - /** - * Sends an email to the specified MyRadio_List - * @param MyRadio_List $to - * @param string $subject email subject - * @param sting $message email message - * @todo Check if "Receive Emails" is enabled for the User - */ - public static function sendEmailToList(MyRadio_List $to, $subject, $message, $from = null) { - if ($from !== null && !$to->hasSendPermission($from)) { - return false; + public function getSubject() + { + return $this->subject; } - self::create(array('lists' => array($to)), $subject, $message, $from); - return true; - } - - /** - * Sends an email to all the specified Users, with certain customisation abilities: - * #NAME is replaced with the User's first name - * - * @param Array $to An array of User objects - * @param string $subject email subject - * @param sting $message email message - */ - public static function sendEmailToUserSet($to, $subject, $message, $from = null) { - - foreach ($to as $user) { - if (!($user instanceof MyRadio_User)) { - throw new MyRadioException($user . ' is not an instance or derivative of the user class!'); - } - - self::create(array('members' => $to), $subject, $message, $from); + + public function getListRecipients() + { + return MyRadio_List::resultSetToObjArray($this->r_lists); } - } - - /** - * Returns if the User received this email. - * - * Will return true if the email was sent to a mailing list they were - * not a member of at the time. - * - * @param MyRadio_User $user - * @return boolean - */ - public function isRecipient(MyRadio_User $user) { - foreach ($this->r_users as $ruser) { - if ($ruser === $user->getID()) { - return true; - } + + public function getUserRecipients() + { + return MyRadio_User::resultSetToObjArray($this->r_users); } - foreach ($this->getListRecipients() as $list) { - if ($list->isMember($user)) { - return true; - } + + public function getViewableBody() + { + if ($this->body) { + $data = CoreUtils::getSafeHTML($this->body); + } else { + /* + * @todo Filtering here. + */ + $body = $this->body_transformed; + $data = CoreUtils::getSafeHTML($body); + } + + if (strpos($data, '<') === false) { + return nl2br($data); + } else { + return $data; + } } - return false; - } - - public function getSubject() { - return $this->subject; - } - - public function getListRecipients() { - return MyRadio_List::resultSetToObjArray($this->r_lists); - } - - public function getUserRecipients() { - return MyRadio_User::resultSetToObjArray($this->r_users); - } - - public function getViewableBody() { - if ($this->body) { - $data = CoreUtils::getSafeHTML($this->body); - } else { - /** - * @todo Filtering here. - */ - $body = $this->body_transformed; - $data = CoreUtils::getSafeHTML($body); + + public function getID() + { + return $this->email_id; } - - if (strpos($data, '<') === false) { - return nl2br($data); - } else { - return $data; + + /** + * @mixin body Also returns the body of the email. + */ + public function toDataSource($mixins = []) + { + $mixin_funcs = [ + 'body' => function (&$data) { + $data['body'] = $this->getViewableBody(); + }, + ]; + + $data = [ + 'email_id' => $this->getID(), + 'from' => empty($this->from) ? null : $this->from->getName(), + 'timestamp' => $this->timestamp, + 'subject' => $this->subject, + 'view' => [ + 'display' => 'icon', + 'value' => 'envelope', + 'title' => 'Read this email', + 'url' => URLUtils::makeURL('Mail', 'view', ['emailid' => $this->getID()]), + ], + ]; + + $this->addMixins($data, $mixins, $mixin_funcs); + return $data; } - } - - public function getID() { - return $this->email_id; - } - - public function toDataSource($full = false) { - $data = [ - 'email_id' => $this->getID(), - 'from' => empty($this->from) ? null : $this->from->getName(), - 'timestamp' => CoreUtils::happyTime($this->timestamp), - 'subject' => $this->subject, - 'view' => [ - 'display' => 'icon', - 'value' => 'mail-open', - 'title' => 'Read this email', - 'url' => CoreUtils::makeURL('Mail', 'view', ['emailid' => $this->getID()]) - ] - ]; - - if ($full) { - $data['body'] = $this->getViewableBody(); + + /** + * BELOW HERE IS FOR IF STUFF BREAKS REALLY EARLY BEFORE ^ WILL WORK *. + */ + + /** + * @param string $subject email subject + * @param sting $message email message + */ + public static function sendEmailToComputing($subject, $message) + { + mail( + 'MyRadio Service <'.Config::$error_report_email.'@'.Config::$email_domain.'>', + $subject, + self::addFooter($message), + self::getDefaultHeader() + ); + return true; } - - return $data; - } - - /** BELOW HERE IS FOR IF STUFF BREAKS REALLY EARLY BEFORE ^ WILL WORK * */ - - /** - * - * @param string $subject email subject - * @param sting $message email message - */ - public static function sendEmailToComputing($subject, $message) { - mail("MyRadio Service ", $subject, self::addFooter($message), self::getDefaultHeader()); - return TRUE; - } - - /** - * - * @return string default headers for sending email - Plain text and sent from no-reply - */ - private static function getDefaultHeader() { - return self::$headers . self::$rtnl . self::$sender; - } + /** + * @return string default headers for sending email - Plain text and sent from no-reply + */ + private static function getDefaultHeader() + { + $headers = 'Content-type: text/plain; charset=utf-8'; + $sender = 'From: MyRadio '; + return $headers.self::$rtnl.$sender; + } } - diff --git a/src/Classes/MyRadioError.php b/src/Classes/MyRadioError.php index 9b909360d..cfab2f9ff 100644 --- a/src/Classes/MyRadioError.php +++ b/src/Classes/MyRadioError.php @@ -1,31 +1,23 @@ - * @version 20130711 - * @package MyRadio_Core */ -class MyRadioError { - - /** - * @var int $count Stores the number of errors thrown - */ - private static $count = 0; - +class MyRadioError +{ /** - * @var array $error_type An array that matches error codes from $errno to - * a short string which names the error type (such as - * "User-generated error", or "User-generated warning") + * @var array An array that matches error codes from $errno to + * a short string which names the error type (such as + * "User-generated error", or "User-generated warning") */ - private static $error_type = array( + private static $error_type = [ E_ERROR => 'Fatal error', E_WARNING => 'Warning', E_PARSE => 'Parse error', @@ -38,55 +30,60 @@ class MyRadioError { E_USER_WARNING => 'User-generated warning', E_USER_NOTICE => 'User-generated notice', E_STRICT => 'Runtime notice', - E_RECOVERABLE_ERROR => 'Recoverable error' - ); + E_RECOVERABLE_ERROR => 'Recoverable error', + ]; - private static function getErrorName($errno) { - return $error_name = (isset(self::$error_type[$errno]) ? - self::$error_type[$errno] : 'Unknown error code'); + private static function getErrorName($errno) + { + return isset(self::$error_type[$errno]) ? self::$error_type[$errno] : 'Unknown error code'; } /** - * @var array $php_errorlist An array holding all php errors as arrays of - * [$error_name,$errstr,$errfile,$errline] + * @var array An array holding all php errors as arrays of + * [$error_name,$errstr,$errfile,$errline] */ - public static $php_errorlist = array(); + public static $php_errorlist = []; /** - * Places all php errors into the array $php_errorlist - * @param string $errno A numeric value which corresponds to the type of - * error (Notice, Fatal Error, User-generated warning, etc). - * @param string $errstr A string that contains the error message text, - * ideally including details that identify the cause of the error. + * Places all php errors into the array $php_errorlist. + * + * @param string $errno A numeric value which corresponds to the type of + * error (Notice, Fatal Error, User-generated warning, etc). + * @param string $errstr A string that contains the error message text, + * ideally including details that identify the cause of the error. * @param string $errfile The full local path of the file which has - * triggered this error (such as /var/www/public_html/badscript.php). + * triggered this error (such as /var/www/public_html/badscript.php). * @param string $errline The line number where the error was generated - * (within the file identified by $errfile). + * (within the file identified by $errfile). */ - public static function errorsToArray($errno, $errstr, $errfile, $errline) { - if ($errno === E_STRICT or error_reporting() === 0) + public static function errorsToArray($errno, $errstr, $errfile, $errline) + { + if ($errno === E_STRICT or (error_reporting() & $errno) === 0) { return; + } $error_name = self::getErrorName($errno); - $php_error = array( + $php_error = [ 'name' => $error_name, 'string' => $errstr, 'file' => htmlspecialchars($errfile, ENT_NOQUOTES, 'UTF-8'), - 'line' => $errline); + 'line' => $errline, ]; array_push(self::$php_errorlist, $php_error); } /** - * Logs all php errors into the php log file - * @param string $errno A numeric value which corresponds to the type of - * error (Notice, Fatal Error, User-generated warning, etc). - * @param string $errstr A string that contains the error message text, - * ideally including details that identify the cause of the error. + * Logs all php errors into the php log file. + * + * @param string $errno A numeric value which corresponds to the type of + * error (Notice, Fatal Error, User-generated warning, etc). + * @param string $errstr A string that contains the error message text, + * ideally including details that identify the cause of the error. * @param string $errfile The full local path of the file which has - * triggered this error (such as /var/www/public_html/badscript.php). + * triggered this error (such as /var/www/public_html/badscript.php). * @param string $errline The line number where the error was generated - * (within the file identified by $errfile). + * (within the file identified by $errfile). */ - public static function errorsToLog($errno, $errstr, $errfile, $errline) { + public static function errorsToLog($errno, $errstr, $errfile, $errline) + { /* * Stage one: log the error using PHP's error logger. */ @@ -94,9 +91,9 @@ public static function errorsToLog($errno, $errstr, $errfile, $errline) { // Structure the error message in the same way as PHP logs // fatal errors, because they'll be saved in the same file. - $error_message = $error_name . ': ' . - $errstr . ' in ' . - $errfile . ' on line ' . $errline; + $error_message = $error_name.': ' + .$errstr.' in ' + .$errfile.' on line '.$errline; error_log($error_message); // log to PHP_ERROR_LOG file } @@ -105,17 +102,19 @@ public static function errorsToLog($errno, $errstr, $errfile, $errline) { */ /** - * Sends the errors to the defined email every 24 hours - * @param string $errno A numeric value which corresponds to the type of error - * (Notice, Fatal Error, User-generated warning, etc). - * @param string $errstr A string that contains the error message text, - * ideally including details that identify the cause of the error. + * Sends the errors to the defined email every 24 hours. + * + * @param string $errno A numeric value which corresponds to the type of error + * (Notice, Fatal Error, User-generated warning, etc). + * @param string $errstr A string that contains the error message text, + * ideally including details that identify the cause of the error. * @param string $errfile The full local path of the file which has triggered - * this error (such as /var/www/public_html/badscript.php). + * this error (such as /var/www/public_html/badscript.php). * @param string $errline The line number where the error was generated - * (within the file identified by $errfile). + * (within the file identified by $errfile). */ - public static function errorsToEmail($errno, $errstr, $errfile, $errline) { + public static function errorsToEmail($errno, $errstr, $errfile, $errline) + { //If errors have been supressed, don't throw them. if (error_reporting() === 0) { return; @@ -124,10 +123,8 @@ public static function errorsToEmail($errno, $errstr, $errfile, $errline) { if (strstr($errstr, 'should be compatible with') !== false) { return; } - self::$count++; //Increment the error counter $errstr = utf8_encode($errstr); - $error_name = self::getErrorName($errno); // Log errors to file for permenance self::errorsToLog($errno, $errstr, $errfile, $errline); /* @@ -137,31 +134,38 @@ public static function errorsToEmail($errno, $errstr, $errfile, $errline) { $lockfile = fopen(Config::$log_file_lock, 'a+'); if (!$lockfile) { - error_log('FAIL: fopen failed in ' . __FUNCTION__ . ' in ' . __FILE__ . ''); - error_log(__FUNCTION__ . ' failed! Check server logs!'); + error_log('FAIL: fopen failed in '.__FUNCTION__.' in '.__FILE__.''); + error_log(__FUNCTION__.' failed! Check server logs!'); throw new MyRadioException('Failed to open log file.', 500); } $locked = flock($lockfile, LOCK_EX); if (!$locked) { - error_log('FAIL: flock failed in ' . __FUNCTION__ . ' in ' . __FILE__ . ''); - error_log(__FUNCTION__ . ' failed! Check server logs!'); + error_log('FAIL: flock failed in '.__FUNCTION__.' in '.__FILE__.''); + error_log(__FUNCTION__.' failed! Check server logs!'); throw new MyRadioException('Failed to open log lock file'); } rewind($lockfile); // Run through the lockfile and grab the date/errfile pairs. - unset($lockfile_data); + $lockfile_data = []; while (!feof($lockfile)) { $buffer = fgets($lockfile); - if ($buffer == '') + if ($buffer == '') { continue; // EOF line is empty - $match = preg_match('#^([0-9]{4}-[0-9]{2}-[0-9]{2}' . - 'T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\+|-)[0-9]{4})' . - '\s+(.+)$#', $buffer, $matches); + } + $match = preg_match( + '#^([0-9]{4}-[0-9]{2}-[0-9]{2}' + .'T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\+|-)[0-9]{4})' + .'\s+(.+)$#', + $buffer, + $matches + ); if (!$match) { - error_log('FAIL: preg_match could not match ' . - 'expected pattern in error log file, in ' . - __FILE__ . ''); + error_log( + 'FAIL: preg_match could not match ' + .'expected pattern in error log file, in ' + .__FILE__.'' + ); continue; } $lockfile_data[$matches[2]] = $matches[1]; @@ -176,15 +180,16 @@ public static function errorsToEmail($errno, $errstr, $errfile, $errline) { if (isset($lockfile_data[$errfile])) { $alert_date = date_create($lockfile_data[$errfile]); if (!$alert_date) { - error_log('FAIL: date_create could not create a date object' . - 'from the last alert date in ' . __FUNCTION__ . ' in ' . - __FILE__ . '.'); + error_log( + 'FAIL: date_create could not create a date object' + .'from the last alert date in '.__FUNCTION__ + .' in '.__FILE__.'.' + ); throw new MyRadioException('Failed to create date object.'); } $alert_timestamp = date_format($alert_date, 'U'); $current_timestamp = date('U'); - $diff_seconds = $current_timestamp - - $alert_timestamp; + $diff_seconds = $current_timestamp - $alert_timestamp; // Change this to TESTING_ONLY to check that it works // but remember to change it back to One Day (or some // other value you deem suitable) when testing is @@ -199,7 +204,6 @@ public static function errorsToEmail($errno, $errstr, $errfile, $errline) { } } - /* * Stage three: send email and update lockfile, if necessary. */ @@ -215,16 +219,17 @@ public static function errorsToEmail($errno, $errstr, $errfile, $errline) { $lockfile_data[$errfile] = gmdate(DATE_ISO8601); ftruncate($lockfile, 0); // we want to start from blank foreach ($lockfile_data as $page => $date) { - fwrite($lockfile, $date . ' ' . $page . "\n"); + fwrite($lockfile, $date.' '.$page."\n"); } } // Now that lockfile has been updated (if it was necessary) // it's time to release the lock, and close the file. - if (flock($lockfile, LOCK_UN) == false || - fclose($lockfile) == false) { - error_log('FAIL: flock or fclose failed in ' . __FUNCTION__ . ' in ' . __FILE__); - error_log(__FUNCTION__ . ' failed! Check server logs!'); + if (flock($lockfile, LOCK_UN) == false + || fclose($lockfile) == false + ) { + error_log('FAIL: flock or fclose failed in '.__FUNCTION__.' in '.__FILE__); + error_log(__FUNCTION__.' failed! Check server logs!'); throw new MyRadioException('Failed to release lock on log file.', 500); } @@ -234,36 +239,25 @@ public static function errorsToEmail($errno, $errstr, $errfile, $errline) { if (Config::$error_report_email) { $rtnl = "

\r\n

"; // carriage return + newline ob_start(); - debug_print_backtrace(); + //debug_print_backtrace(); Sometimes these have passwords. $trace = str_replace("\n", $rtnl, ob_get_clean()); - $message = $errstr . $rtnl . $rtnl . $trace; + $message = $errstr.$rtnl.$rtnl.$trace; if (class_exists('MyRadioEmail') && class_exists('Config')) { $sent = MyRadioEmail::sendEmailToList( - MyRadio_List::getByName(Config::$error_report_email), - 'MyRadio error alert', $message); + MyRadio_List::getByName(Config::$error_report_email), + 'MyRadio error alert', + $message + ); if (!$sent) { error_log('FAIL: mail failed to send error alert email.'); // Good chance that if the mail command failed, // then error_log will also fail to send mail, // but we have to try. - error_log(__FUNCTION__ . ' failed! Check server logs!'); + error_log(__FUNCTION__.' failed! Check server logs!'); throw new MyRadioException('Failed to send email error alert.', 500); } } } } } - - /** - * Returns the number of errors encountered during execution. - * @return int - */ - public static function getErrorCount() { - return self::$count; - } - - public static function resetErrorCount() { - self::$count = 0; - } - -} \ No newline at end of file +} diff --git a/src/Classes/MyRadioException.php b/src/Classes/MyRadioException.php index e3b4e294c..bcc1bf8e9 100644 --- a/src/Classes/MyRadioException.php +++ b/src/Classes/MyRadioException.php @@ -1,129 +1,215 @@ - * @version 20130711 - * @package MyRadio_Core + * and logging. */ -class MyRadioException extends RuntimeException { - - const FATAL = -1; - - private static $count = 0; - - /** - * Extends the default session by enabling useful output - * @param String $message A nice message explaining what is going on - * @param int $code A number representing the problem. -1 Indicates fatal. - * @param \Exception $previous - */ - public function __construct($message, $code = 500, Exception $previous = null) { - self::$count++; - if (self::$count > Config::$exception_limit) { - trigger_error('Exception limit exceeded. Futher exceptions will not be reported.'); - return; - } - - $trace = $this->getTrace(); - $traceStr = $this->getTraceAsString(); - if ($previous) { - $trace = array_merge($trace, $previous->getTrace()); - $traceStr .= "\n\n".$this->getTraceAsString(); - } - - parent::__construct((string) $message, (int) $code, $previous); +class MyRadioException extends \RuntimeException implements ClientAware +{ + const FATAL = -1; - if (defined('SILENT_EXCEPTIONS') && SILENT_EXCEPTIONS) { - return; + private static $count = 0; + private $error; + private $trace; + private $traceStr; + + public function getCodeName() + { + switch ($this->code) { + case 400: + return 'Bad Request'; + case 401: + return 'Authentication Required'; + case 403: + return 'Unauthorized'; + case 404: + return 'File Not Found'; + case 405: + return 'Method Not Allowed'; + case 418: + return 'I\'m a Teapot'; + case 500: + return 'Internal Server Error'; + } } - //Set up the Exception - $error = "

MyRadio has encountered a problem processing this request.

- - - - -
Message{$this->getMessage()}
Location{$this->getFile()}:{$this->getLine()}
Trace" . nl2br($traceStr) . "
"; - - if (class_exists('Config')) { - if (Config::$email_exceptions && class_exists('MyRadioEmail') && $code !== 400) { - MyRadioEmail::sendEmailToComputing('[MyRadio] Exception Thrown', - $error . "\r\n" . $message . "\r\n" . - (isset($_SESSION) ? print_r($_SESSION, true) : '') . "\r\n" . CoreUtils::getRequestInfo()); - } - //Configuration is available, use this to decide what to do - if (Config::$display_errors or (class_exists('CoreUtils') && - CoreUtils::hasPermission(AUTH_SHOWERRORS))) { - if ((isset($_SERVER['HTTP_X_REQUESTED_WITH']) && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') or empty($_SERVER['REMOTE_ADDR'])) { - //This is an Ajax/CLI request. Return JSON - header('HTTP/1.1 ' . $code . ' Internal Server Error'); - header('Content-Type: text/json'); - echo json_encode(array( - 'status' => 'MyRadioException', - 'error' => $message, - 'code' => $code, - 'trace' => $trace - )); - } else { - //Output to the browser - header('HTTP/1.1 ' . $code . ' Internal Server Error'); - - if (class_exists('CoreUtils') && !headers_sent()) { - //We can use a pretty full-page output - $twig = CoreUtils::getTemplateObject(); - $twig->setTemplate('error.twig') - ->addVariable('serviceName', 'Error') - ->addVariable('title', 'Internal Server Error') - ->addVariable('body', $error) - ->addVariable('uri', $_SERVER['REQUEST_URI']) - ->render(); - } else { - echo $error; - } + /** + * Extends the default session by enabling useful output. + * + * @param string $message A nice message explaining what is going on + * @param int $code A number representing the problem. -1 Indicates fatal. + * @param \Exception $previous + */ + public function __construct($message, $code = 500, \Exception $previous = null) + { + parent::__construct((string) $message, (int) $code, $previous); + + ++self::$count; + if (self::$count > Config::$exception_limit) { + trigger_error("Exception limit exceeded. Further exceptions will not be reported."); + return; + } + + $this->trace = $this->getTrace(); + $this->traceStr = $this->getTraceAsString(); + if ($previous) { + $this->trace = array_merge($this->trace, $previous->getTrace()); + $this->traceStr .= "\n\n".$this->getTraceAsString(); } - } else { - $error = '
' . $this->getMessage() . '' - . '

A fatal error has occured that has prevented MyRadio from performing the action you requested. ' - . 'The computing team have been notified.

'; - //Output limited info to the browser - header('HTTP/1.1 ' . $code . ' Internal Server Error'); - - if (class_exists('CoreUtils') && !headers_sent()) { - //We can use a pretty full-page output - $twig = CoreUtils::getTemplateObject(); - $twig->setTemplate('error.twig') - ->addVariable('serviceName', 'Error') - ->addVariable('title', 'Internal Server Error') - ->addVariable('body', $error) - ->addVariable('uri', $_SERVER['REQUEST_URI']) - ->render(); + + //Set up the Exception + if ($code === 403) { + $this->error = "

I'm sorry, but you don't have permission to access this page.

+

{$this->getMessage()}

"; } else { - echo $error; + $this->error = "

MyRadio has encountered a problem processing this request.

+ + + + +
Message: {$this->getMessage()}
Location: {$this->getFile()}:{$this->getLine()}
Trace: " . nl2br($this->traceStr) . '
'; + } + } + + /** + * Called when the exception is not caught. + */ + public function uncaught() + { + $silent = (defined('SILENT_EXCEPTIONS') && SILENT_EXCEPTIONS); + + if (class_exists('\MyRadio\Config')) { + $is_ajax = (isset($_SERVER['HTTP_X_REQUESTED_WITH']) + && strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) === 'xmlhttprequest') + || empty($_SERVER['REMOTE_ADDR']) + || (defined('JSON_DEBUG') && JSON_DEBUG); + + if (Config::$email_exceptions + && class_exists('\MyRadio\MyRadioEmail') + && $this->code !== 400 + && $this->code !== 401 + && $this->code !== 403 + ) { + MyRadioEmail::sendEmailToComputing( + '[MyRadio] Exception Thrown', + 'Code: '.$this->code."\r\n\r\n" + .'Message: '.$this->message."\r\n\r\n" + ."Trace: \r\n".$this->traceStr."\r\n\r\n" + ."Request: \r\n".CoreUtils::getRequestInfo()."\r\n\r\n" + ."RequestURI: \r\n".$_SERVER['REQUEST_URI']."\r\n\r\n" + ."Session: \r\n" + .(isset($_SESSION) ? print_r($_SESSION, true) : '') + ); + } + + if (Config::$log_file) { + // TODO make this create the dir - maybe use error_log? + file_put_contents( + Config::$log_file, + CoreUtils::getTimestamp().'['.$this->code.'] '.$this->message."\n".$this->traceStr."\n\n", + FILE_APPEND + ); + } + + //Configuration is available, use this to decide what to do + if (!$silent + && Config::$display_errors + || (class_exists('\MyRadio\MyRadio\AuthUtils') + && defined('AUTH_SHOWERRORS') + && AuthUtils::hasPermission(AUTH_SHOWERRORS)) + ) { + if ($is_ajax) { + //This is an Ajax/CLI request. Return JSON + header('HTTP/1.1 '.$this->code.' '.$this->getCodeName()); + header('Content-Type: application/json'); + echo json_encode( + [ + 'status' => 'MyRadioException', + 'error' => $this->message, + 'code' => $this->code, + 'trace' => $this->trace, + ] + ); + } else { + //Output to the browser + header('HTTP/1.1 '.$this->code.' '.$this->getCodeName()); + + if (class_exists('\MyRadio\MyRadio\CoreUtils') && !headers_sent()) { + //We can use a pretty full-page output + $twig = CoreUtils::getTemplateObject(); + $twig->setTemplate('error.twig') + ->addVariable('serviceName', 'Error') + ->addVariable('title', $this->getCodeName()) + ->addVariable('body', $this->error) + ->addVariable('uri', $_SERVER['REQUEST_URI']) + ->render(); + } else { + echo $this->error; + } + } + } elseif (!$silent) { + if ($is_ajax) { + //This is an Ajax/CLI request. Return JSON + header('HTTP/1.1 '.$this->code.' '.$this->getCodeName()); + header('Content-Type: application/json'); + echo json_encode( + [ + 'status' => 'MyRadioError', + 'error' => $this->message, + 'code' => $this->code, + ] + ); + //Stick the details in the session in case the user wants to report it + $_SESSION['last_ajax_error'] = [$this->error, $this->code, $this->trace]; + } else { + $error = '
' + .'

Sorry, we encountered an error and are unable to continue. Please try again later.

' + .'

'.$this->message.'

' + .'

Computing Team have been notified.

' + .'
'; + //Output limited info to the browser + header('HTTP/1.1 '.$this->code.' '.$this->getCodeName()); + + if (class_exists('\MyRadio\MyRadio\CoreUtils') && !headers_sent()) { + //We can use a pretty full-page output + $twig = CoreUtils::getTemplateObject(); + $twig->setTemplate('error.twig') + ->addVariable('title', $this->getCodeName()) + ->addVariable('body', $this->error) + ->addVariable('uri', $_SERVER['REQUEST_URI']) + ->render(); + } else { + echo $error; + } + } + } + } elseif (!$silent) { + echo 'MyRadio is unavailable at the moment. ' + .'Please try again later. If the problem persists, contact support.'; } - } - } else { - echo 'A fatal error has occured that has prevented MyRadio from performing the action you requested. Please contact computing@ury.org.uk.'; } - } - /** - * Get the number of MyRadioExceptions that have been fired. - * @return int - */ - public static function getExceptionCount() { - return self::$count; - } + public static function resetExceptionCount() + { + self::$count = 0; + } - public static function resetExceptionCount() { - self::$count = 0; - } + public function isClientSafe(): bool + { + return $this->code < 500; + } -} \ No newline at end of file + public function getCategory() + { + return 'oh_dear'; + } +} diff --git a/src/Classes/MyRadioGeoIP.php b/src/Classes/MyRadioGeoIP.php new file mode 100644 index 000000000..10697d31a --- /dev/null +++ b/src/Classes/MyRadioGeoIP.php @@ -0,0 +1,35 @@ + - * @version 20131012 * @depends Config - * @package MyRadio_Core */ -class MyRadioTwig implements TemplateEngine { - - private $contextVariables = array(); +class MyRadioTwig implements \MyRadio\Iface\TemplateEngine +{ + private $contextVariables = []; private $template; private $twig; /** - * Cannot be private - parent does not allow it + * Cannot be private - parent does not allow it. + * * @todo Better Documentation */ - public function __construct() { - $twig_loader = new Twig_Loader_Filesystem(__DIR__ . '/../Templates/'); - $this->contextVariables['notices'] = ''; - $this->twig = new Twig_Environment($twig_loader, array('auto_reload' => true)); + public function __construct() + { + $twig_loader = new FilesystemLoader(__DIR__.'/../Templates/'); + $this->contextVariables['notices'] = []; + $this->twig = new Environment($twig_loader, ['auto_reload' => true]); if (Config::$template_debug) { $this->twig->addExtension(new Twig_Extension_Debug()); $this->twig->enableDebug(); } + $this->twig + /** + * Returns true if $date2 is on the calendar day after $date1. Both must be + * UNIX timestamps. + */ + ->addFunction(new TwigFunction('is_next_day', function ($date1, $date2) { + return $date2 >= ($date1 + 86400) || ($date1 % 86400) > ($date2 % 86400); + })); + $this->addVariable('memberid', isset($_SESSION['memberid']) ? $_SESSION['memberid'] : 0) - ->addVariable('impersonator', !empty($_SESSION['myradio-impersonating']) ? - ('Impersonated by ' . $_SESSION['myradio-impersonating']['name']).'' : '') - ->addVariable('timeslotname', isset($_SESSION['timeslotname']) ? $_SESSION['timeslotname'] : null) - ->addVariable('timeslotid', isset($_SESSION['timeslotid']) ? $_SESSION['timeslotid'] : null) - ->addVariable('shiburl', Config::$shib_url) - ->addVariable('baseurl', Config::$base_url) - ->addVariable('rewriteurl', Config::$rewrite_url) - ->addVariable('serviceName', 'MyRadio') - ->setTemplate('stripe.twig') - ->addVariable('uri', $_SERVER['REQUEST_URI']) - ->addVariable('module', empty($GLOBALS['module']) ? Config::$default_module : $GLOBALS['module']) - ->addVariable('action', empty($GLOBALS['action']) ? Config::$default_action : $GLOBALS['action']) - ->addVariable('config', Config::getPublicConfig()); - - - //We override the defaults later so we don't depend on the database. - //If the session is set, we can assume database access is available - //as it is read from the database - if (isset($_SESSION)) { - $this->addVariable('name', isset($_SESSION['name']) ? $_SESSION['name'] : 'Login') - ->addVariable('baseurl', CoreUtils::getServiceVersionForUser()['proxy_static'] ? - CoreUtils::makeURL('MyRadio', 'StaticProxy', array('0' => null)) : Config::$base_url); - - if (!empty($GLOBALS['module']) && isset($_SESSION['memberid'])) { - $this->addVariable('submenu', (new MyRadioMenu())->getSubMenuForUser(CoreUtils::getModuleID($GLOBALS['module']), MyRadio_User::getInstance())) - ->addVariable('title', $GLOBALS['module']); - } + ->addVariable( + 'impersonatorurl', + !empty($_SESSION['myradio-impersonating']) + ? (URLUtils::makeURL('MyRadio', 'impersonate', ['next' => $_SERVER['REQUEST_URI']])) + : '' + ) + ->addVariable( + 'impersonator', + !empty($_SESSION['myradio-impersonating']) + ? $_SESSION['myradio-impersonating']['name'] + : '' + ) + ->addVariable('timeslotname', isset($_SESSION['timeslotname']) ? $_SESSION['timeslotname'] : null) + ->addVariable('timeslotid', isset($_SESSION['timeslotid']) ? $_SESSION['timeslotid'] : null) + ->addVariable('baseurl', Config::$base_url) + ->addVariable('websiteurl', Config::$website_url) + ->addVariable('shortname', Config::$short_name) + ->addVariable('rewriteurl', Config::$rewrite_url) + ->addVariable('serviceName', 'MyRadio') + ->setTemplate('stripe.twig') + ->addVariable('uri', $_SERVER['REQUEST_URI']) + ->addVariable('module', empty($GLOBALS['module']) ? Config::$default_module : $GLOBALS['module']) + ->addVariable('action', empty($GLOBALS['action']) ? Config::$default_action : $GLOBALS['action']) + ->addVariable('config', Config::getPublicConfig()) + ->addVariable('name', isset($_SESSION['name']) ? $_SESSION['name'] : '') + ->addVariable('nonav', isset($_GET['nonav'])); + + if (!empty($GLOBALS['module']) && isset($_SESSION['memberid'])) { + $this->addVariable('submenu', (new MyRadioMenu())->getSubMenuForUser($GLOBALS['module'])) + ->addVariable('title', $GLOBALS['module']); } - - if (!empty($_SESSION['joyride'])) { - $this->addVariable('joyride', $_SESSION['joyride']); - } //Make requests override session-set joyrides if (!empty($_REQUEST['joyride'])) { $this->addVariable('joyride', $_REQUEST['joyride']); } - if (CoreUtils::hasPermission(AUTH_SELECTSERVICEVERSION)) { - $this->addVariable('version_header', '
  • ' . - (empty(CoreUtils::getServiceVersionForUser()['version']) ? - 'Select Version' : CoreUtils::getServiceVersionForUser()['version']) . '
  • '); - } else { - $this->addVariable('version_header', ''); - } - if (isset($_REQUEST['message'])) { - $this->addInfo(base64_decode($_REQUEST['message'])); + $this->addInfo(strip_tags(base64_decode($_REQUEST['message']), '')); } } /** - * Registers a new variable to be passed to the template - * @param String $name The name of the variable - * @param mixed $value The value of the variable - literally any valid type + * Registers a new variable to be passed to the template. + * + * @param string $name The name of the variable + * @param mixed $value The value of the variable - literally any valid type + * * @return \MyRadioTwig This for chaining */ - public function addVariable($name, $value) { - /** + public function addVariable($name, $value) + { + /* * This is a hack for datatables, as there's no easy way for Twig to know booleans. * It's slow. * @todo Is there a better way of casting true/false to Yes/No? @@ -102,16 +115,20 @@ public function addVariable($name, $value) { throw new MyRadioException('Notices cannot be directly set via the Template Engine'); } $this->contextVariables[$name] = $value; + return $this; } /** * Recursively iterates over an array of any depth, replacing all booleans with "Yes" or "No". * Used for the datatable hack. - * @param Array $value - * @return Array + * + * @param array $value + * + * @return array */ - private function boolParser($value) { + private function boolParser($value) + { if (!is_array($value)) { return $value; } @@ -122,65 +139,83 @@ private function boolParser($value) { $value[$k] = $this->boolParser($v); } } + return $value; } - public function addInfo($message, $icon = 'info') { - $this->contextVariables['notices'][] = array('icon' => $icon, 'message' => $message, 'state' => 'highlight'); + public function addInfo($message, $icon = 'info-sign') + { + $this->contextVariables['notices'][] = ['icon' => $icon, 'message' => $message, 'state' => 'info']; + return $this; } - public function addError($message, $icon = 'alert') { - $this->contextVariables['notices'][] = array('icon' => $icon, 'message' => $message, 'state' => 'error'); + public function addError($message, $icon = 'warning-sign') + { + $this->contextVariables['notices'][] = ['icon' => $icon, 'message' => $message, 'state' => 'danger']; + return $this; } /** - * Sets the template file to use - * @param String $template The template filename + * Sets the template file to use. + * + * @param string $template The template filename + * * @throws MyRadioException If template does not exist + * * @return MyRadioTwig This for chaining */ - public function setTemplate($template) { - if (!file_exists(__DIR__ . '/../Templates/' . $template)) { + public function setTemplate($template) + { + if (!file_exists(__DIR__.'/../Templates/'.$template)) { throw new MyRadioException("Template $template does not exist"); } //Validate template try { - $this->twig->parse($this->twig->tokenize(file_get_contents(__DIR__ . '/../Templates/' . $template), $template)); + $this->twig->parse($this->twig->tokenize( + new \Twig\Source(file_get_contents(__DIR__.'/../Templates/'.$template), $template) + )); // the $template is valid } catch (Twig_Error_Syntax $e) { - throw new MyRadioException('Twig Parse Error' . $e->getMessage(), $e->getCode(), $e); + throw new MyRadioException('Twig Parse Error'.$e->getMessage(), $e->getCode(), $e); } $this->template = $this->twig->loadTemplate($template); + return $this; } /** - * Renders the template + * Renders the template. */ - public function render() { - if (CoreUtils::hasPermission(AUTH_SHOWERRORS) || Config::$display_errors) { + public function render() + { + if ((defined('AUTH_SHOWERRORS') && AuthUtils::hasPermission(AUTH_SHOWERRORS)) + || Config::$display_errors + ) { $this->addVariable('phperrors', MyRadioError::$php_errorlist); if (isset($_SESSION)) { //Is the DB working? $this->addVariable('query_count', Database::getInstance()->getCounter()); - } + } } $output = $this->template->render($this->contextVariables); if (empty($output)) { //That's not right. - throw new MyRadioException('Failed to render page ' - . '(template ' . $this->template->getTemplateName() . ')', 500); + throw new MyRadioException( + 'Failed to render page ' + .'(template '.$this->template->getTemplateName().')', + 500 + ); } echo $output; } - public static function getInstance() { + public static function getInstance() + { return new self(); } - } diff --git a/src/Classes/NIPSWeb/NIPSWeb_AutoPlaylist.php b/src/Classes/NIPSWeb/NIPSWeb_AutoPlaylist.php index 18ce36d66..2f6cf5a7e 100644 --- a/src/Classes/NIPSWeb/NIPSWeb_AutoPlaylist.php +++ b/src/Classes/NIPSWeb/NIPSWeb_AutoPlaylist.php @@ -2,111 +2,131 @@ /** * This file provides the NIPSWeb_AutoPlaylist class for MyRadio - Contains Jingles etc. - * @package MyRadio_NIPSWeb */ +namespace MyRadio\NIPSWeb; + +use MyRadio\MyRadioException; +use MyRadio\ServiceAPI\MyRadio_Track; +use MyRadio\ServiceAPI\MyRadio_User; /** - * The NIPSWeb_AutoPlaylist class helps provide control and access to Auto playlists - * - * @version 20130508 - * @author Andy Durant - * @package MyRadio_NIPSWeb - * @uses \Database + * The NIPSWeb_AutoPlaylist class helps provide control and access to Auto playlists. + * + * @uses \Database */ -class NIPSWeb_AutoPlaylist extends ServiceAPI { +class NIPSWeb_AutoPlaylist extends \MyRadio\ServiceAPI\ServiceAPI +{ + /** + * The Singleton store for AutoPlaylist objects. + * + * @var NIPSWeb_AutoPlaylist + */ + private static $playlists = []; + private $auto_playlist_id; + protected $name; + protected $tracks; + protected $query; - /** - * The Singleton store for AutoPlaylist objects - * @var NIPSWeb_AutoPlaylist - */ - private static $playlists = array(); - private $auto_playlist_id; - protected $name; - protected $tracks; - protected $query; + /** + * Initiates the AutoPlaylist variables. + * + * @param int $playlistid The ID of the auto playlist to initialise + */ + protected function __construct($playlistid) + { + $this->auto_playlist_id = $playlistid; + $result = self::$db->fetchOne( + 'SELECT * FROM bapsplanner.auto_playlists WHERE auto_playlist_id=$1 LIMIT 1', + [$playlistid] + ); + if (empty($result)) { + throw new MyRadioException('The specified NIPSWeb Auto Playlist does not seem to exist', 404); + } - /** - * Initiates the AutoPlaylist variables - * @param int $playlistid The ID of the auto playlist to initialise - */ - protected function __construct($playlistid) { - $this->auto_playlist_id = $playlistid; - $result = self::$db->fetch_one('SELECT * FROM bapsplanner.auto_playlists WHERE auto_playlist_id=$1 LIMIT 1', array($playlistid)); - if (empty($result)) { - throw new MyRadioException('The specified NIPSWeb Auto Playlist does not seem to exist'); - return; + $this->name = $result['name']; + $this->query = $result['query']; } - $this->name = $result['name']; - $this->query = $result['query']; - } + /** + * Return the MyRadio_Tracks that belong to this playlist. + * + * Lazily evaluated - Tracks will not be loaded until the method is called + * + * @return Array[MyRadio_Track] + */ + public function getTracks() + { + if (empty($this->tracks)) { + $tracks = self::$db->fetchAll($this->query); + $this->tracks = []; - /** - * Return the MyRadio_Tracks that belong to this playlist - * - * Lazily evaluated - Tracks will not be loaded until the method is called - * - * @return Array[MyRadio_Track] - */ - public function getTracks() { - if (empty($this->tracks)) { - $tracks = self::$db->fetch_all($this->query); - $this->tracks = array(); + foreach ($tracks as $id) { + $this->tracks[] = MyRadio_Track::getInstance($id['trackid']); + } + } - foreach ($tracks as $id) { - $this->tracks[] = MyRadio_Track::getInstance($id['trackid']); - } + return $this->tracks; } - return $this->tracks; - } - - /** - * Get the Title of the AutoPlaylist - * @return String - */ - public function getTitle() { - return $this->name; - } - /** - * Get the unique manageditemid of the AutoPlaylist - * @return int - */ - public function getID() { - return $this->auto_playlist_id; - } + /** + * Get the Title of the AutoPlaylist. + * + * @return string + */ + public function getTitle() + { + return $this->name; + } - public static function getAllAutoPlaylists($editable_only = false) { - if ($editable_only && !MyRadio_User::getInstance()->hasAuth(AUTH_EDITCENTRALRES)) - return array(); - $result = self::$db->fetch_column('SELECT auto_playlist_id FROM bapsplanner.auto_playlists ORDER BY name'); - $response = array(); - foreach ($result as $id) { - $response[] = self::getInstance($id); + /** + * Get the unique manageditemid of the AutoPlaylist. + * + * @return int + */ + public function getID() + { + return $this->auto_playlist_id; } - return $response; - } + public static function getAllAutoPlaylists($editable_only = false) + { + if ($editable_only && !MyRadio_User::getInstance()->hasAuth(AUTH_EDITCENTRALRES)) { + return []; + } + $result = self::$db->fetchColumn('SELECT auto_playlist_id FROM bapsplanner.auto_playlists ORDER BY name'); + $response = []; + foreach ($result as $id) { + $response[] = self::getInstance($id); + } - public static function findByName($name) { - $result = self::$db->fetch_column('SELECT auto_playlist_id FROM bapsplanner.auto_playlists WHERE name=$1', array($name)); + return $response; + } - if (empty($result)) - throw new MyRadioException('That auto playlist does not exist!'); - else - return self::getInstance($result[0]); - } + public static function findByName($name) + { + $result = self::$db->fetchColumn( + 'SELECT auto_playlist_id FROM bapsplanner.auto_playlists WHERE name=$1', + [$name] + ); - /** - * Returns an array of key information, useful for Twig rendering and JSON requests - * @todo Expand the information this returns - * @return Array - */ - public function toDataSource() { - return array( - 'title' => $this->getTitle(), - 'playlistid' => $this->getID(), - ); - } + if (empty($result)) { + throw new MyRadioException('That auto playlist does not exist!'); + } else { + return self::getInstance($result[0]); + } + } -} \ No newline at end of file + /** + * Returns an array of key information, useful for Twig rendering and JSON requests. + * @param array $mixins Mixins. Currently unused. + * @return array + * @todo Expand the information this returns + */ + public function toDataSource($mixins = []) + { + return [ + 'title' => $this->getTitle(), + 'playlistid' => $this->getID(), + ]; + } +} diff --git a/src/Classes/NIPSWeb/NIPSWeb_BAPSUtils.php b/src/Classes/NIPSWeb/NIPSWeb_BAPSUtils.php index 10080aaab..01fa5fb1f 100644 --- a/src/Classes/NIPSWeb/NIPSWeb_BAPSUtils.php +++ b/src/Classes/NIPSWeb/NIPSWeb_BAPSUtils.php @@ -5,211 +5,261 @@ * @package MyRadio_NIPSWeb */ +namespace MyRadio\NIPSWeb; + +use MyRadio\Config; +use MyRadio\MyRadioException; +use MyRadio\MyRadio\CoreUtils; +use MyRadio\ServiceAPI\MyRadio_Timeslot; + /** - * This class has helper functions for saving Show Planner show informaiton into legacy BAPS Show layout - * - * @version 20130508 - * @author Lloyd Wallis - * @package MyRadio_NIPSWeb + * This class has helper functions for saving Show Planner show informaiton into legacy BAPS Show layout. */ -class NIPSWeb_BAPSUtils extends ServiceAPI { +class NIPSWeb_BAPSUtils extends \MyRadio\ServiceAPI\ServiceAPI +{ + public static function getBAPSShowIDFromTimeslot(MyRadio_Timeslot $timeslot) + { + $result = self::$db->fetchColumn( + 'SELECT showid FROM baps_show + WHERE externallinkid=$1 LIMIT 1', + [$timeslot->getID()] + ); - public static function getBAPSShowIDFromTimeslot(MyRadio_Timeslot $timeslot) { + if (empty($result)) { + //No match. Create a show + $result = self::$db->fetchColumn( + 'INSERT INTO baps_show (userid, name, broadcastdate, externallinkid, viewable) + VALUES (4, $1, $2, $3, true) RETURNING showid', + [ + $timeslot->getMeta('title') + .'-' + .$timeslot->getID(), + CoreUtils::getTimestamp($timeslot->getStartTime()), + $timeslot->getID(), + ] + ); + } - $result = self::$db->fetch_column('SELECT showid FROM baps_show - WHERE externallinkid=$1 LIMIT 1', array($timeslot->getID())); + return (int) $result[0]; + } - if (empty($result)) { - //No match. Create a show - $result = self::$db->fetch_column('INSERT INTO baps_show - (userid, name, broadcastdate, externallinkid, viewable) - VALUES (4, $1, $2, $3, true) RETURNING showid', array($timeslot->getMeta('title') . '-' . $timeslot->getID(), - CoreUtils::getTimestamp($timeslot->getStartTime()), - $timeslot->getID())); + /** + * Takes a BAPS ShowID, and gets the channel references for the show + * If a listing for one or more channels does not exist, this method + * creates them automatically. + * + * @param int $showid The BAPS show id + * + * @return bool|array An array of BAPS Channels, or false on failure + */ + public static function getListingsForShow($showid) + { + $listings = self::$db->fetchAll( + 'SELECT * FROM baps_listing + WHERE showid=$1 ORDER BY channel ASC', + [(int) $showid] + ); + + if (!$listings) { + $listings = []; + } + + if (sizeof($listings) === 3) { + //There's already three, there's no need to check which exist + return $listings; + } + + /* + * @todo Wow I was lazy here. + */ + $channels = [false, false, false]; + + //Flag existing channels as, well, existing + foreach ($listings as $listing) { + $channels[$listing['channel']] = true; + } + + //Go over the channels and create the nonexistent ones + $change = false; + foreach ($channels as $channel => $exists) { + if (!$exists) { + self::$db->query( + 'INSERT INTO baps_listing (showid, name, channel) + VALUES ($1, \'Channel '.$channel.'\', $2)', + [$showid, $channel] + ); + $change = true; + } + } + //If the show definition has changed, recurse this method + if ($change) { + return self::getListingsForShow($showid); + } else { + return $listings; + } } - return (int) $result[0]; - } - - /** - * Takes a BAPS ShowID, and gets the channel references for the show - * If a listing for one or more channels does not exist, this method - * creates them automatically - * @param int $showid The BAPS show id - * @return boolean|Array An array of BAPS Channels, or false on failure - */ - public static function getListingsForShow($showid) { - $listings = self::$db->fetch_all('SELECT * FROM baps_listing - WHERE showid=$1 ORDER BY channel ASC', array((int) $showid)); - - if (!$listings) - $listings = array(); - - if (sizeof($listings) === 3) { - //There's already three, there's no need to check which exist - return $listings; + public static function saveListingsForTimeslot(MyRadio_Timeslot $timeslot) + { + //Get the timeslot's show plan + $tracks = $timeslot->getShowPlan(); + + //Get the listings related to the show + $showid = self::getBAPSShowIDFromTimeslot($timeslot); + $listings = self::getListingsForShow($showid); + + //Start a transaction for this change + self::$db->query('BEGIN'); + + foreach ($listings as $listing) { + //Delete the old format + self::$db->query('DELETE FROM baps_item WHERE listingid=$1', [$listing['listingid']], true); + //Add each new entry + $position = 1; + //if the listing isn't empty then write the tracks that are in there + if (isset($tracks[$listing['channel']])) { + foreach ($tracks[$listing['channel']] as $track) { + switch ($track['type']) { + case 'central': + $file = self::getTrackDetails($track['trackid'], $track['album']['recordid']); + self::$db->query( + 'INSERT INTO baps_item (listingid, position, libraryitemid, name1, name2) + VALUES ($1, $2, $3, $4, $5)', + [ + $listing['listingid'], + $position, + $file['libraryitemid'], + $file['title'], + $file['artist'], + ] + ); + break; + + case 'aux': + //Get the LegacyDB ID of the file + $fileitemid = self::getFileItemFromManagedID($track['managedid']); + self::$db->query( + 'INSERT INTO baps_item (listingid, position, fileitemid, name1) + VALUES ($1, $2, $3, $4)', + [ + (int) $listing['listingid'], + (int) $position, + (int) $fileitemid, + $track['title'], + ] + ); + break; + + default: + throw new MyRadioException('What do I even with this item?'); + } + ++$position; + } + } + } + + self::$db->query('COMMIT'); } /** - * @todo Wow I was lazy here. + * Gets the title, artist, and BapsWeb libraryitemid of a track. + * + * @param int $trackid The Track ID from the rec database + * @param int $recordid The Record ID from the rec database + * + * @return bool|array False on failure, or an array of the above */ - $channels = array(false, false, false); + public static function getTrackDetails($trackid, $recordid) + { + $trackid = (int) $trackid; + $recordid = (int) $recordid; + $result = self::$db->fetchOne( + 'SELECT title, artist, libraryitemid + FROM rec_track, baps_libraryitem + WHERE rec_track.trackid = baps_libraryitem.trackid + AND rec_track.trackid=$1 LIMIT 1', + [$trackid] + ); - //Flag existing channels as, well, existing - foreach ($listings as $listing) { - $channels[$listing['channel']] = true; - } + if (empty($result)) { + //Create the baps_libraryitem and recurse. pg_query_params doesn't like this... + $result = self::$db->query( + 'INSERT INTO baps_libraryitem + (trackid, recordid) VALUES ($1, $2)', + [$trackid, $recordid] + ); - //Go over the channels and create the nonexistent ones - $change = false; - foreach ($channels as $channel => $exists) { - if (!$exists) { - self::$db->query('INSERT INTO baps_listing (showid, name, channel) - VALUES ($1, \'Channel ' . $channel . '\', $2)', array($showid, $channel)); - $change = true; - } - } - //If the show definition has changed, recurse this method - if ($change) - return self::getListingsForShow($showid); - else - return $listings; - } - - public static function saveListingsForTimeslot(MyRadio_Timeslot $timeslot) { - //Get the timeslot's show plan - $tracks = $timeslot->getShowPlan(); - - //Get the listings related to the show - $showid = self::getBAPSShowIDFromTimeslot($timeslot); - $listings = self::getListingsForShow($showid); - - //Start a transaction for this change - self::$db->query('BEGIN'); - - foreach ($listings as $listing) { - //Delete the old format - self::$db->query('DELETE FROM baps_item WHERE listingid=$1', array($listing['listingid']), true); - //Add each new entry - $position = 1; - //if the listing isn't empty then write the tracks that are in there - if (isset($tracks[$listing['channel']])) { - foreach ($tracks[$listing['channel']] as $track) { - switch ($track['type']) { - case 'central': - $file = self::getTrackDetails($track['trackid'], $track['album']['recordid']); - self::$db->query('INSERT INTO baps_item (listingid, position, libraryitemid, name1, name2) - VALUES ($1, $2, $3, $4, $5)', array( - $listing['listingid'], - $position, - $file['libraryitemid'], - $file['title'], - $file['artist'] - ), true); - - break; - case 'aux': - //Get the LegacyDB ID of the file - $fileitemid = self::getFileItemFromManagedID($track['managedid']); - self::$db->query('INSERT INTO baps_item (listingid, position, fileitemid, name1) - VALUES ($1, $2, $3, $4)', array( - (int) $listing['listingid'], - (int) $position, - (int) $fileitemid, - $track['title'] - ), true); - break; - default: - throw new MyRadioException('What do I even with this item?'); - } - $position++; + return self::getTrackDetails($trackid, $recordid); } - } - } - self::$db->query('COMMIT'); - } - - /** - * Gets the title, artist, and BapsWeb libraryitemid of a track - * @param int $trackid The Track ID from the rec database - * @param int $recordid The Record ID from the rec database - * @return boolean|array False on failure, or an array of the above - */ - private static function getTrackDetails($trackid, $recordid) { - $trackid = (int) $trackid; - $recordid = (int) $recordid; - $result = self::$db->fetch_one('SELECT title, artist, libraryitemid - FROM rec_track, baps_libraryitem - WHERE rec_track.trackid = baps_libraryitem.trackid - AND rec_track.trackid=$1 LIMIT 1', array($trackid)); - - if (empty($result)) { - //Create the baps_libraryitem and recurse. pg_query_params doesn't like this... - $result = self::$db->query('INSERT INTO baps_libraryitem - (trackid, recordid) VALUES ($1, $2)', array($trackid, $recordid)); - return self::getTrackDetails($trackid, $recordid); + return $result; } - return $result; - } - - /** - * Returns the FileItemID from a ManagedItemID - */ - public static function getFileItemFromManagedID($auxid) { - $item = NIPSWeb_ManagedItem::getInstance($auxid); - - $legacy_path = Config::$music_smb_path . '\\membersmusic\\fileitems\\' . self::sanitisePath($item->getTitle()) . '_' . $auxid . '.mp3'; - //Make a hard link if it doesn't exist - $ln_path = Config::$music_central_db_path . '/membersmusic/fileitems/' . self::sanitisePath($item->getTitle()) . '_' . $auxid . '.mp3'; - if (!file_exists($ln_path)) { - if (!@link($item->getPath('mp3'), $ln_path)) { - trigger_error('Could not link ' . $item->getPath('mp3') . ' to ' . $ln_path); - } + /** + * Returns the FileItemID from a ManagedItemID. + */ + public static function getFileItemFromManagedID($auxid) + { + $item = NIPSWeb_ManagedItem::getInstance($auxid); + + $legacy_path = Config::$music_smb_path . '\\membersmusic\\fileitems\\' + . self::sanitisePath($item->getTitle()) . '_' . $auxid . '.mp3'; + //Make a hard link if it doesn't exist + $ln_path = Config::$music_central_db_path . '/membersmusic/fileitems/' + . self::sanitisePath($item->getTitle()) . '_' . $auxid . '.mp3'; + + if (!file_exists($ln_path) && !@link($item->getPath('mp3'), $ln_path)) { + trigger_error('Could not link '.$item->getPath('mp3').' to '.$ln_path); + } + $id = self::getFileItemFromPath($legacy_path); + + if (!$id) { + //Create it + $r = self::$db->fetchColumn( + 'INSERT INTO public.baps_fileitem (filename) VALUES ($1) RETURNING fileitemid', + [$legacy_path] + ); + return $r[0]; + } + return $id; } - $id = self::getFileItemFromPath($legacy_path); - if (!$id) { - //Create it - $r = self::$db->fetch_column('INSERT INTO public.baps_fileitem (filename) VALUES ($1) RETURNING fileitemid', array($legacy_path)); - return $r[0]; + public static function linkCentralLists(NIPSWeb_ManagedItem $item) + { + if (in_array($item->getFolder(), ['jingles', 'beds', 'adverts']) !== false) { + //Make a hard link if it doesn't exist + $ln_path = Config::$music_central_db_path . '/membersmusic/' + . $item->getFolder() . '/' . self::sanitisePath($item->getTitle()) . '.mp3'; + + if (!file_exists($ln_path) && !@link($item->getPath(), $ln_path)) { + trigger_error('Could not link '.$item->getPath().' to '.$ln_path); + } + } } - return $id; - } - - public static function linkCentralLists(NIPSWeb_ManagedItem $item) { - if (in_array($item->getFolder(), array('jingles', 'beds', 'adverts')) !== false) { - //Make a hard link if it doesn't exist - $ln_path = Config::$music_central_db_path . '/membersmusic/'.$item->getFolder().'/' . self::sanitisePath($item->getTitle()).'.mp3'; - if (!file_exists($ln_path)) { - if (!@link($item->getPath(), $ln_path)) { - trigger_error('Could not link '.$item->getPath() . ' to ' . $ln_path); + + /** + * Returns the ID of an item in the auxillary database based on its samba path. + * + * @param string $path The Samba Share location of the file to search for + * + * @return bool|int false on error or non existent, fileitemid otherwise + */ + public static function getFileItemFromPath($path) + { + $result = self::$db->fetchColumn('SELECT fileitemid FROM baps_fileitem WHERE filename=$1 LIMIT 1', [$path]); + if (empty($result)) { + return false; } - } + return (int) $result[0]; } - } - - /** - * Returns the ID of an item in the auxillary database based on its samba path - * @param string $path The Samba Share location of the file to search for - * @return boolean|int false on error or non existent, fileitemid otherwise - */ - public static function getFileItemFromPath($path) { - $result = self::$db->fetch_column('SELECT fileitemid FROM baps_fileitem - WHERE filename=$1 LIMIT 1', array($path)); - if (empty($result)) - return false; - - return (int) $result[0]; - } - - /** - * Ensure a string can be used as a filename - * @param type $file - */ - public static function sanitisePath($file) { - return trim(preg_replace("/[^0-9^a-z^,^_^.^\(^\)^-^ ]/i", "", str_replace('..', '.', $file))); - } + /** + * Ensure a string can be used as a filename. + * + * @param type $file + */ + public static function sanitisePath($file) + { + return trim(preg_replace("/[^0-9^a-z^,^_^.^\(^\)^-^ ]/i", '', str_replace('..', '.', $file))); + } } diff --git a/src/Classes/NIPSWeb/NIPSWeb_ManagedItem.php b/src/Classes/NIPSWeb/NIPSWeb_ManagedItem.php index 4c15d97f0..3b2cf9430 100644 --- a/src/Classes/NIPSWeb/NIPSWeb_ManagedItem.php +++ b/src/Classes/NIPSWeb/NIPSWeb_ManagedItem.php @@ -1,270 +1,341 @@ - * @package MyRadio_NIPSWeb - * @uses \Database + * The NIPSWeb_ManagedItem class helps provide control and access to Beds and Jingles and similar not-PPL resources. + * + * @uses \Database */ -class NIPSWeb_ManagedItem extends ServiceAPI { - private $managed_item_id; - - private $managed_playlist; - - private $folder; - - private $title; - - private $length; - - private $bpm; - - private $expirydate; - - private $member; - - /** - * Initiates the ManagedItem variables - * @param int $resid The ID of the managed resource to initialise - * @param NIPSWeb_ManagedPlaylist $playlistref If the playlist is requesting this item, then pass the playlist object - * @todo Length, BPM - * @todo Seperate Managed Items and Managed User Items. The way they were implemented was a horrible hack, for which - * I am to blame. I should go to hell for it, seriously - Lloyd - */ - protected function __construct($resid, $playlistref = null) { - $this->managed_item_id = $resid; - //*dies* - $result = self::$db->fetch_one('SELECT manageditemid, title, length, bpm, NULL AS folder, memberid, expirydate, - managedplaylistid - FROM bapsplanner.managed_items WHERE manageditemid=$1 - UNION SELECT manageditemid, title, length, bpm, managedplaylistid AS folder, NULL AS memberid, NULL AS expirydate, - NULL as managedplaylistid - FROM bapsplanner.managed_user_items WHERE manageditemid=$1 - LIMIT 1', - array($resid)); - if (empty($result)) { - throw new MyRadioException('The specified NIPSWeb Managed Item or Managed User Item does not seem to exist'); - return; +class NIPSWeb_ManagedItem extends \MyRadio\ServiceAPI\ServiceAPI +{ + private $managed_item_id; + + private $managed_playlist; + + private $folder; + + private $title; + + private $length; + + private $bpm; + + private $expirydate; + + private $member; + + /** + * Initiates the ManagedItem variables. + * + * @param int $resid The ID of the managed resource to initialise + * @param NIPSWeb_ManagedPlaylist $playlistref If the playlist is requesting this item, then pass the playlist obj + * + * @todo Seperate Managed Items and Managed User Items. The way they were implemented was a horrible hack, for which + * I am to blame. I should go to hell for it, seriously - Lloyd + */ + protected function __construct($resid, $playlistref = null) + { + $this->managed_item_id = $resid; + //*dies* + $result = self::$db->fetchOne( + 'SELECT manageditemid, title, length, bpm, NULL AS folder, memberid, expirydate, managedplaylistid + FROM bapsplanner.managed_items WHERE manageditemid=$1 + UNION + SELECT manageditemid, title, length, bpm, managedplaylistid AS folder, + NULL AS memberid, NULL AS expirydate, NULL as managedplaylistid + FROM bapsplanner.managed_user_items WHERE manageditemid=$1 + LIMIT 1', + [$resid] + ); + + if (empty($result)) { + throw new MyRadioException( + 'The specified NIPSWeb Managed Item or Managed User Item does not seem to exist', + 404 + ); + } + + $this->managed_playlist = empty( + $result['managedplaylistid'] + ) ? null : + (($playlistref instanceof NIPSWeb_ManagedPlaylist) ? $playlistref : + NIPSWeb_ManagedPlaylist::getInstance($result['managedplaylistid']) + ); + $this->folder = $result['folder']; + $this->title = $result['title']; + $this->length = strtotime('1970-01-01 '.$result['length']. ' UTC'); + $this->bpm = (int) $result['bpm']; + $this->expirydate = $result['expirytime'] ? strtotime($result['expirydate']) : null; + $this->member = empty($result['memberid']) ? null : MyRadio_User::getInstance($result['memberid']); } - - $this->managed_playlist = empty($result['managedplaylistid']) ? null : - (($playlistref instanceof NIPSWeb_ManagedPlaylist) ? $playlistref : - NIPSWeb_ManagedPlaylist::getInstance($result['managedplaylistid'])); - $this->folder = $result['folder']; - $this->title = $result['title']; - $this->length = strtotime('1970-01-01 '.$result['length']); - $this->bpm = (int)$result['bpm']; - $this->expirydate = strtotime($result['expirydate']); - $this->member = empty($result['memberid']) ? null : MyRadio_User::getInstance($result['memberid']); - } - - /** - * Get the Title of the ManagedItem - * @return String - */ - public function getTitle() { - return $this->title; - } - - /** - * Get the unique manageditemid of the ManagedItem - * @return int - */ - public function getID() { - return $this->managed_item_id; - } - - /** - * Get the length of the ManagedItem, in seconds - * @todo Not Implemented as Length not stored in DB - * @return int - */ - public function getLength() { - return $this->length; - } - - /** - * Get the path of the ManagedItem - * @param String $ext One of the supported file types - * @return string - */ - public function getPath($extension = 'mp3') { - return Config::$music_central_db_path.'/'.($this->managed_playlist ? $this->managed_playlist->getFolder() : $this->folder).'/'.$this->getID().'.'.$extension; - } - - public function getFolder() { - $dir = Config::$music_central_db_path.'/'.($this->managed_playlist ? $this->managed_playlist->getFolder() : $this->folder); - if (!is_dir($dir)) { - if (!mkdir($dir, 0777, true)) { - return false; - } - } - return $dir; - } - - /** - * Returns an array of key information, useful for Twig rendering and JSON requests - * @todo Expand the information this returns - * @return Array - */ - public function toDataSource() { - return array( - 'type' => 'aux', //Legacy NIPSWeb Views - 'summary' => $this->getTitle(), //Again, freaking NIPSWeb - 'title' => $this->getTitle(), - 'managedid' => $this->getID(), - 'length' => CoreUtils::happyTime($this->getLength() > 0 ? $this->getLength() : 0, true, false), - 'trackid' => $this->getID(), - 'recordid' => 'ManagedDB', //Legacy NIPSWeb Views - 'auxid' => 'managed:' . $this->getID() //Legacy NIPSWeb Views - ); - } - - public static function cacheItem($tmp_path) { - if (!isset($_SESSION['myury_nipsweb_file_cache_counter'])) $_SESSION['myury_nipsweb_file_cache_counter'] = 0; - if (!is_dir(Config::$audio_upload_tmp_dir)) { - mkdir(Config::$audio_upload_tmp_dir); + + /** + * Get the Title of the ManagedItem. + * + * @return string + */ + public function getTitle() + { + return $this->title; } - - $filename = session_id() . '-' . ++$_SESSION['myury_nipsweb_file_cache_counter'] . '.mp3'; - - move_uploaded_file($tmp_path, Config::$audio_upload_tmp_dir . '/' . $filename); - - $getID3 = new getID3; - $fileInfo = $getID3->analyze(Config::$audio_upload_tmp_dir . '/' . $filename); - - //The entire $fileInfo array will break Session. - $_SESSION['uploadInfo'][$filename] = array( - 'fileformat' => $fileInfo['fileformat'], - 'playtime_seconds' => $fileInfo['playtime_seconds'] - ); - - // File quality checks - if ($fileInfo['audio']['bitrate'] < 192000) { - return array('status' => 'FAIL', 'error' => 'Bitrate is below 192kbps.', 'fileid' => $filename, 'bitrate' => $fileInfo['audio']['bitrate']); + + /** + * Get the unique manageditemid of the ManagedItem. + * + * @return int + */ + public function getID() + { + return $this->managed_item_id; } - if (strpos($fileInfo['audio']['channelmode'], 'stereo') === false) { - return array('status' => 'FAIL', 'error' => 'Item is not stereo.', 'fileid' => $filename, 'channelmode' => $fileInfo['audio']['channelmode']); + + /** + * Get the length of the ManagedItem, in seconds. + * + * @return int Length of track in seconds + */ + public function getLength() + { + return $this->length; } - return array( - 'fileid' => $filename, - ); - } - public static function storeItem($tmpid, $title) { + /** + * Get the expiry date of the ManagedItem + * + * @return int Timestamp of expiry date. + */ + public function getExpiryDate() + { + return $this->expirydate; + } + + /** + * Get if the ManagedItem has expired. + * + * @return bool Has the item expired? + */ + public function isExpired() + { + $expires = $this->getExpiryDate(); + if ($expires != null) { + return $expires <= strtotime("now"); + } else { + return false; + } + } - $options = array( - 'title' => $title, - 'expires' => $_REQUEST['expires'], - 'auxid' => $_REQUEST['auxid'], - 'duration' => $_SESSION['uploadInfo'][$tmpid]['playtime_seconds'], - ); + /** + * Get the path of the ManagedItem. + * + * @param string $ext One of the supported file types + * + * @return string + */ + public function getPath($extension = 'mp3') + { + return Config::$music_central_db_path.'/' + .($this->managed_playlist ? $this->managed_playlist->getFolder() : $this->folder) + .'/'.$this->getID().'.'.$extension; + } - $item = self::create($options); + public function getFolder() + { + $dir = Config::$music_central_db_path.'/' + .($this->managed_playlist ? $this->managed_playlist->getFolder() : $this->folder); + if (!is_dir($dir)) { + if (!mkdir($dir, 0777, true)) { + return false; + } + } - if (!$item) { - //Database transaction failed. - return array('status' => 'FAIL', 'error' => 'A database kerfuffle occured.', 'fileid' => $_REQUEST['fileid']); + return $dir; } /** - * Store three versions of the track: - * 1- 192kbps MP3 for BAPS and Chrome/IE - * 2- 192kbps OGG for Safari/Firefox - * 3- Original file for potential future conversions + * Returns an array of key information, useful for Twig rendering and JSON requests. + * @param array $mixins Mixins. Currently unused + * @return array + * @todo Expand the information this returns */ - $tmpfile = Config::$audio_upload_tmp_dir.'/'.$tmpid; - - if (!$item->getFolder()) { - //Creating folders failed. - return array('status' => 'FAIL', 'error' => 'Folders could not be created.', 'fileid' => $_REQUEST['fileid']); + public function toDataSource($mixins = []) + { + return [ + 'type' => 'aux', //Legacy NIPSWeb Views + 'summary' => $this->getTitle(), //Again, freaking NIPSWeb + 'title' => $this->getTitle(), + 'managedid' => $this->getID(), + 'length' => CoreUtils::intToTime($this->getLength() > 0 ? $this->getLength() : 0), + 'trackid' => $this->getID(), + 'expirydate' => $this->getExpiryDate(), + 'expired' => $this->isExpired(), + 'recordid' => 'ManagedDB', //Legacy NIPSWeb Views + 'auxid' => 'managed:'.$this->getID(), //Legacy NIPSWeb Views + ]; } - $dbfile = $item->getFolder().'/'.$item->getID(); - //Convert it with ffmpeg - // BAPS needs stdout > to file - shell_exec("nice -n 15 ffmpeg -i '$tmpfile' -ab 192k -f mp3 - > '{$dbfile}.mp3'"); - shell_exec("nice -n 15 ffmpeg -i '$tmpfile' -acodec libvorbis -ab 192k '{$dbfile}.ogg'"); - rename($tmpfile, $dbfile.'.'.$_SESSION['uploadInfo'][$tmpid]['fileformat'].'.orig'); + public static function cacheItem($tmp_path) + { + if (!isset($_SESSION['myradio_nipsweb_file_cache_counter'])) { + $_SESSION['myradio_nipsweb_file_cache_counter'] = 0; + } + if (!is_dir(Config::$audio_upload_tmp_dir)) { + mkdir(Config::$audio_upload_tmp_dir); + } - if (!file_exists($dbfile.'.mp3') || !file_exists($dbfile.'.ogg')) { - //Conversion failed! - return array('status' => 'FAIL', 'error' => 'Conversion with ffmpeg failed.', 'fileid' => $_REQUEST['fileid']); - } - elseif (!file_exists($dbfile.'.'.$_SESSION['uploadInfo'][$tmpid]['fileformat'].'.orig')) { - return array('status' => 'FAIL', 'error' => 'Could not move file to library.', 'fileid' => $_REQUEST['fileid']); + $filename = session_id().'-'.++$_SESSION['myradio_nipsweb_file_cache_counter'].'.mp3'; + + move_uploaded_file($tmp_path, Config::$audio_upload_tmp_dir.'/'.$filename); + + $getID3 = new \getID3(); + $fileInfo = $getID3->analyze(Config::$audio_upload_tmp_dir.'/'.$filename); + //The entire $fileInfo array will break Session. + $_SESSION['uploadInfo'][$filename] = [ + 'fileformat' => $fileInfo['fileformat'], + 'playtime_seconds' => $fileInfo['playtime_seconds'], + ]; + + // File quality checks + if ($fileInfo['audio']['bitrate'] < 192000) { + return [ + 'status' => 'FAIL', + 'error' => 'Bitrate is below 192kbps.', + 'fileid' => $filename, + 'bitrate' => $fileInfo['audio']['bitrate'] + ]; + } + if (strpos($fileInfo['audio']['channelmode'], 'stereo') === false) { + return [ + 'status' => 'FAIL', + 'error' => 'Item is not stereo.', + 'fileid' => $filename, + 'channelmode' => $fileInfo['audio']['channelmode'] + ]; + } + + return [ + 'fileid' => $filename, + ]; } - - NIPSWeb_BAPSUtils::linkCentralLists($item); - - return array('status' => 'OK', 'title' => $title); - } - - /** - * Create a new NIPSWEB_ManagedItem with the provided options - * @param Array $options - * title (required): Title of the item. - * duration (required): Duration of the item, in seconds - * auxid (required): The auxid of the playlist - * bpm: The beats per minute of the item - * expires: The expiry date of the item - * @return NIPSWEB_ManagedItem a shiny new NIPSWEB_ManagedItem with the provided options - * @throws MyRadioException - */ - public static function create($options) { - self::wakeup(); - - $required = array('title', 'duration', 'auxid'); - foreach ($required as $require) { - if (empty($options[$require])) throw new MyRadioException($require.' is required to create an Item.', 400); + + public static function storeItem($tmpid, $title) + { + $options = [ + 'title' => $title, + 'expires' => $_REQUEST['expires'], + 'auxid' => $_REQUEST['auxid'], + 'duration' => $_SESSION['uploadInfo'][$tmpid]['playtime_seconds'], + ]; + + $item = self::create($options); + if (!$item) { + //Database transaction failed. + return ['status' => 'FAIL', 'error' => 'A database kerfuffle occured.', 'fileid' => $_REQUEST['fileid']]; + } + if (!$item->getFolder()) { + //Creating folders failed. + return ['status' => 'FAIL', 'error' => 'Folders could not be created.', 'fileid' => $_REQUEST['fileid']]; + } + + /* + * Store three versions of the track: + * 1- 192kbps MP3 for BAPS and Chrome/IE + * 2- 192kbps OGG for Safari/Firefox + * 3- Original file for potential future conversions + */ + $tmpfile = Config::$audio_upload_tmp_dir.'/'.$tmpid; + $dbfile = $item->getFolder().'/'.$item->getID(); + + try { + CoreUtils::encodeTrack($tmpfile, $dbfile); + } catch (MyRadioException $e) { + return ['status' => 'FAIL', 'error' => $e->getMessage(), 'fileid' => $_REQUEST['fileid']]; + } + + NIPSWeb_BAPSUtils::linkCentralLists($item); + + return ['status' => 'OK', 'title' => $title]; } - //BPM null - if (empty($options['bpm'])) $options['bpm'] = null; - //Expires null - if (empty($options['expires'])) $options['expires'] = null; - - //Decode the auxid to figure out what/where we're adding - if (strpos($options['auxid'], 'user-') !== false) { - //This is a personal resource - $path = str_replace('user-', 'membersmusic/', $options['auxid']); - $result = self::$db->query('INSERT INTO bapsplanner.managed_user_items (managedplaylistid, title, length, bpm) - VALUES ($1, $2, $3, $4) RETURNING manageditemid', - array( + + /** + * Create a new NIPSWEB_ManagedItem with the provided options. + * + * @param array $options + * title (required): Title of the item. + * duration (required): Duration of the item, in seconds + * auxid (required): The auxid of the playlist + * bpm: The beats per minute of the item + * expires: The expiry date of the item + * + * @return NIPSWEB_ManagedItem a shiny new NIPSWEB_ManagedItem with the provided options + * + * @throws MyRadioException + */ + public static function create($options) + { + self::wakeup(); + + $required = ['title', 'duration', 'auxid']; + foreach ($required as $require) { + if (empty($options[$require])) { + throw new MyRadioException($require.' is required to create an Item.', 400); + } + } + //BPM null + if (empty($options['bpm'])) { + $options['bpm'] = null; + } + //Expires null + if (empty($options['expires'])) { + $options['expires'] = null; + } + + //Decode the auxid to figure out what/where we're adding + if (strpos($options['auxid'], 'user-') !== false) { + //This is a personal resource + $path = str_replace('user-', 'membersmusic/', $options['auxid']); + $q = 'INSERT INTO bapsplanner.managed_user_items (managedplaylistid, title, length, bpm) + VALUES ($1, $2, $3, $4) RETURNING manageditemid'; + $p = [ $path, trim($options['title']), - CoreUtils::intToTime($options['duration']), + CoreUtils::intToTime(floor($options['duration'])), $options['bpm'], - )); - } - else { - //This is a central resource - $result = self::$db->fetch_column('SELECT managedplaylistid FROM bapsplanner.managed_playlists WHERE managedplaylistid=$1 LIMIT 1', array(str_replace('aux-', '', $options['auxid']))); - if (empty($result)) - throw new MyRadioException($options['auxid'].' is not a valid playlist!'); - $playlistid = $result[0]; - - $result = self::$db->query('INSERT INTO bapsplanner.managed_items (managedplaylistid, title, length, bpm, expirydate, memberid) - VALUES ($1, $2, $3, $4, $5, $6) RETURNING manageditemid', - array( - $playlistid, - trim($options['title']), - CoreUtils::intToTime($options['duration']), - $options['bpm'], - $options['expires'], - $_SESSION['memberid'], - )); + ]; + } else { + //This is a central resource + $result = self::$db->fetchColumn( + 'SELECT managedplaylistid FROM bapsplanner.managed_playlists WHERE managedplaylistid=$1 LIMIT 1', + [str_replace('aux-', '', $options['auxid'])] + ); + if (empty($result)) { + throw new MyRadioException($options['auxid'].' is not a valid playlist!'); + } + $playlistid = $result[0]; + + $q = 'INSERT INTO bapsplanner.managed_items (managedplaylistid, title, length, bpm, expirydate, memberid) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING manageditemid'; + $p = [ + $playlistid, + trim($options['title']), + CoreUtils::intToTime(floor($options['duration'])), + $options['bpm'], + $options['expires'], + $_SESSION['memberid'], + ]; + } + + $id = self::$db->fetchAll($q, $p); + + return self::getInstance($id[0]['manageditemid']); } - - $id = self::$db->fetch_all($result); - - return self::getInstance($id[0]['manageditemid']); - } } diff --git a/src/Classes/NIPSWeb/NIPSWeb_ManagedPlaylist.php b/src/Classes/NIPSWeb/NIPSWeb_ManagedPlaylist.php index 89ffbd84e..ec1473ebe 100644 --- a/src/Classes/NIPSWeb/NIPSWeb_ManagedPlaylist.php +++ b/src/Classes/NIPSWeb/NIPSWeb_ManagedPlaylist.php @@ -2,118 +2,140 @@ /** * This file provides the NIPSWeb_ManagedPlaylist class for MyRadio - Contains Jingles etc. - * @package MyRadio_NIPSWeb */ +namespace MyRadio\NIPSWeb; + +use MyRadio\MyRadioException; +use MyRadio\ServiceAPI\MyRadio_User; /** - * The NIPSWeb_ManagedPlaylist class helps provide control and access to managed playlists - * - * @version 20130802 - * @author Lloyd Wallis - * @package MyRadio_NIPSWeb - * @uses \Database + * The NIPSWeb_ManagedPlaylist class helps provide control and access to managed playlists. + * + * @uses \Database */ -class NIPSWeb_ManagedPlaylist extends ServiceAPI { +class NIPSWeb_ManagedPlaylist extends \MyRadio\ServiceAPI\ServiceAPI +{ + /** + * The Singleton store for ManagedPlaylist objects. + * + * @var NIPSWeb_ManagedPlaylist + */ + private static $playlists = []; + private $managed_playlist_id; + protected $items; + protected $name; + protected $folder; + private $item_ttl; + + /** + * Initiates the ManagedPlaylist variables. + * + * @param int $playlistid The ID of the managed playlist to initialise + * Note: Only links *non-expired* items + */ + protected function __construct($playlistid) + { + $this->managed_playlist_id = $playlistid; + $result = self::$db->fetchOne( + 'SELECT * FROM bapsplanner.managed_playlists WHERE managedplaylistid=$1 LIMIT 1', + [$playlistid] + ); + if (empty($result)) { + throw new MyRadioException('The specified NIPSWeb Managed Playlist does not seem to exist', 404); - /** - * The Singleton store for ManagedPlaylist objects - * @var NIPSWeb_ManagedPlaylist - */ - private static $playlists = array(); - private $managed_playlist_id; - protected $items; - protected $name; - protected $folder; - private $item_ttl; + return; + } - /** - * Initiates the ManagedPlaylist variables - * @param int $playlistid The ID of the managed playlist to initialise - * Note: Only links *non-expired* items - */ - protected function __construct($playlistid) { - $this->managed_playlist_id = $playlistid; - $result = self::$db->fetch_one('SELECT * FROM bapsplanner.managed_playlists WHERE managedplaylistid=$1 LIMIT 1', array($playlistid)); - if (empty($result)) { - throw new MyRadioException('The specified NIPSWeb Managed Playlist does not seem to exist'); - return; + $this->name = $result['name']; + $this->folder = $result['folder']; + $this->item_ttl = $result['item_ttl']; } - $this->name = $result['name']; - $this->folder = $result['folder']; - $this->item_ttl = $result['item_ttl']; - } + /** + * Return the NIPSWeb_ManagedItems that belong to this playlist. + * + * @return Array[NIPSWeb_ManagedItem] + */ + public function getItems() + { + if (empty($this->items)) { + $items = self::$db->fetchColumn( + 'SELECT manageditemid FROM bapsplanner.managed_items WHERE managedplaylistid=$1 + AND (expirydate IS NULL OR expirydate > NOW()) + ORDER BY title', + [$this->managed_playlist_id] + ); - /** - * Return the NIPSWeb_ManagedItems that belong to this playlist - * @return Array[NIPSWeb_ManagedItem] - */ - public function getItems() { - if (empty($this->items)) { - $items = self::$db->fetch_column('SELECT manageditemid FROM bapsplanner.managed_items WHERE managedplaylistid=$1 - AND (expirydate IS NULL OR expirydate > NOW()) - ORDER BY title', array($this->managed_playlist_id)); - $this->items = array(); - foreach ($items as $id) { - /** - * Pass this to the ManagedItem - it's called Dependency Injection and prevents loops and looks pretty - * http://stackoverflow.com/questions/4903387/can-2-singleton-classes-reference-each-other - * http://www.phparch.com/2010/03/static-methods-vs-singletons-choose-neither/ - */ - $this->items[] = NIPSWeb_ManagedItem::getInstance((int) $id, $this); - } - } - return $this->items; - } + $this->items = []; + foreach ($items as $id) { + /* + * Pass this to the ManagedItem - it's called Dependency Injection and prevents loops and looks pretty + * http://stackoverflow.com/questions/4903387/can-2-singleton-classes-reference-each-other + * http://www.phparch.com/2010/03/static-methods-vs-singletons-choose-neither/ + */ + $this->items[] = NIPSWeb_ManagedItem::getInstance((int) $id, $this); + } + } - /** - * Get the Title of the ManagedPlaylist - * @return String - */ - public function getTitle() { - return $this->name; - } + return $this->items; + } - /** - * Get the unique manageditemid of the ManagedPlaylist - * @return int - */ - public function getID() { - return $this->managed_playlist_id; - } + /** + * Get the Title of the ManagedPlaylist. + * + * @return string + */ + public function getTitle() + { + return $this->name; + } - /** - * Get the unique path of the ManagedPlaylist - * @return String - */ - public function getFolder() { - return $this->folder; - ; - } + /** + * Get the unique manageditemid of the ManagedPlaylist. + * + * @return int + */ + public function getID() + { + return $this->managed_playlist_id; + } - public static function getAllManagedPlaylists($editable_only = false) { - if ($editable_only && !MyRadio_User::getInstance()->hasAuth(AUTH_EDITCENTRALRES)) - return array(); - $result = self::$db->fetch_column('SELECT managedplaylistid FROM bapsplanner.managed_playlists ORDER BY name'); - $response = array(); - foreach ($result as $id) { - $response[] = self::getInstance($id); + /** + * Get the unique path of the ManagedPlaylist. + * + * @return string + */ + public function getFolder() + { + return $this->folder; } - return $response; - } + public static function getAllManagedPlaylists($editable_only = false) + { + if ($editable_only && !MyRadio_User::getInstance()->hasAuth(AUTH_EDITCENTRALRES)) { + return []; + } + $result = self::$db->fetchColumn('SELECT managedplaylistid FROM bapsplanner.managed_playlists ORDER BY name'); + $response = []; + foreach ($result as $id) { + $response[] = self::getInstance($id); + } - /** - * Returns an array of key information, useful for Twig rendering and JSON requests - * @todo Expand the information this returns - * @return Array - */ - public function toDataSource() { - return array( - 'title' => $this->getTitle(), - 'managedid' => $this->getID(), - 'folder' => $this->getFolder(), - ); - } + return $response; + } + /** + * Returns an array of key information, useful for Twig rendering and JSON requests. + * @param array $mixins Mixins. Currently unused + * @return array + * @todo Expand the information this returns + */ + public function toDataSource($mixins = []) + { + return [ + 'title' => $this->getTitle(), + 'managedid' => $this->getID(), + 'folder' => $this->getFolder(), + ]; + } } diff --git a/src/Classes/NIPSWeb/NIPSWeb_ManagedUserPlaylist.php b/src/Classes/NIPSWeb/NIPSWeb_ManagedUserPlaylist.php index bc8bdcb6a..b5fe2d0dd 100644 --- a/src/Classes/NIPSWeb/NIPSWeb_ManagedUserPlaylist.php +++ b/src/Classes/NIPSWeb/NIPSWeb_ManagedUserPlaylist.php @@ -1,90 +1,116 @@ - * @package MyRadio_NIPSWeb - * @uses \Database + * + * @uses \Database */ -class NIPSWeb_ManagedUserPlaylist extends NIPSWeb_ManagedPlaylist { +class NIPSWeb_ManagedUserPlaylist extends NIPSWeb_ManagedPlaylist +{ + /** + * Initiates the UserPlaylist variables. + * + * @param int $playlistid The folder of the user playlist to initialise, e.g. 7449/beds + * Note: Only links *non-expired* items + */ + protected function __construct($playlistid) + { + $this->folder = $playlistid; - /** - * Initiates the UserPlaylist variables - * @param int $playlistid The folder of the user playlist to initialise, e.g. 7449/beds - * Note: Only links *non-expired* items - */ - protected function __construct($playlistid) { - $this->folder = $playlistid; + $this->name = self::getNameFromFolder($this->folder); + } - $this->name = self::getNameFromFolder($this->folder); - } + /** + * Get the User Playlist Name from the Folder path. + * + * @param string $id Folder + * + * @return string the playlist name + */ + public static function getNameFromFolder($id) + { + $data = explode('/', $id); + switch ($data[sizeof($data) - 1]) { + case 'jingles': + return 'My Jingles'; + break; + case 'beds': + return 'My Beds'; + break; + case 'links': + return 'My Links'; + break; + case 'sfx': + return 'My Sound Effects'; + break; + case 'other': + return 'My Misc Things'; + break; + default: + return 'ERR_USR_PRESET_NOT_FOUND: '.$id; + break; + } + } - /** - * Get the User Playlist Name from the Folder path. This is "My Beds" or "My Jingles" - * @param string $id Folder - * @return string "My Beds" or "My Jingles" - */ - public static function getNameFromFolder($id) { - $data = explode('/', $id); - switch ($data[sizeof($data) - 1]) { - case 'jingles': - return 'My Jingles'; - break; - case 'beds': - return 'My Beds'; - break; - default: - return 'ERR_USR_PRESET_NOT_FOUND: ' . $id; - break; + /** + * Get the unique folder of the ManagedUserPlaylist. + * + * @return string + */ + public function getID() + { + return $this->getFolder(); } - } - /** - * Get the unique folder of the ManagedUserPlaylist - * @return String - */ - public function getID() { - return $this->getFolder(); - } + /** + * Return the NIPSWeb_ManagedItems that belong to this playlist. + * + * @return Array[NIPSWeb_ManagedItem] + */ + public function getItems() + { + if (empty($this->items)) { + $items = self::$db->fetchColumn( + 'SELECT manageditemid FROM bapsplanner.managed_user_items + WHERE managedplaylistid=$1 ORDER BY title', + ['membersmusic/'.$this->folder] + ); + $this->items = []; + foreach ($items as $id) { + /* + * Pass this to the ManagedItem - it's called Dependency Injection and prevents loops and looks pretty + * http://stackoverflow.com/questions/4903387/can-2-singleton-classes-reference-each-other + * http://www.phparch.com/2010/03/static-methods-vs-singletons-choose-neither/ + */ + $this->items[] = NIPSWeb_ManagedItem::getInstance((int) $id, $this); + } + } - /** - * Return the NIPSWeb_ManagedItems that belong to this playlist - * @return Array[NIPSWeb_ManagedItem] - */ - public function getItems() { - if (empty($this->items)) { - $items = self::$db->fetch_column('SELECT manageditemid FROM bapsplanner.managed_user_items - WHERE managedplaylistid=$1 ORDER BY title', array('membersmusic/' . $this->folder)); - $this->items = array(); - foreach ($items as $id) { - /** - * Pass this to the ManagedItem - it's called Dependency Injection and prevents loops and looks pretty - * http://stackoverflow.com/questions/4903387/can-2-singleton-classes-reference-each-other - * http://www.phparch.com/2010/03/static-methods-vs-singletons-choose-neither/ - */ - $this->items[] = NIPSWeb_ManagedItem::getInstance((int) $id, $this); - } + return $this->items; } - return $this->items; - } - - /** - * Returns the managed user playlists for the given user - * @param MyRadio_User $user - * @return array of My Beds and My Jingles playlists for the user - */ - public static function getAllManagedUserPlaylistsFor($user) { - return array( - self::getInstance($user->getID() . '/beds'), - self::getInstance($user->getID() . '/jingles') - ); - } + /** + * Returns the managed user playlists for the given user. + * + * + * @return array of Managed User Playlists for the current user. + */ + public static function getAllManagedUserPlaylists() + { + $user = MyRadio_User::getInstance(); + return [ + self::getInstance($user->getID().'/beds'), + self::getInstance($user->getID().'/jingles'), + self::getInstance($user->getID().'/links'), + self::getInstance($user->getID().'/sfx'), + self::getInstance($user->getID().'/other') + ]; + } } diff --git a/src/Classes/NIPSWeb/NIPSWeb_TimeslotItem.php b/src/Classes/NIPSWeb/NIPSWeb_TimeslotItem.php index 702b96ffa..baddec113 100644 --- a/src/Classes/NIPSWeb/NIPSWeb_TimeslotItem.php +++ b/src/Classes/NIPSWeb/NIPSWeb_TimeslotItem.php @@ -1,119 +1,178 @@ - * @package MyRadio_NIPSWeb - * @uses \Database + * The NIPSWeb_TimeslotItem class helps provide Show Planner with access to all resource types a timeslot item could be. + * + * @uses \Database */ -class NIPSWeb_TimeslotItem extends ServiceAPI { - private $timeslot_item_id; - - private $item; - - private $channel; - - private $weight; - - /** - * Initiates the TimeslotItem variables - * @param int $resid The timeslot_item_id of the resource to initialise - * @param NIPSWeb_ManagedPlaylist $playlistref If the playlist is requesting this item, then pass the playlist object - */ - protected function __construct($resid, $playlistref = null) { - $this->timeslot_item_id = $resid; - //*dies* - $result = self::$db->fetch_one('SELECT * FROM bapsplanner.timeslot_items where timeslot_item_id=$1 LIMIT 1', - array($resid)); - if (empty($result)) { - throw new MyRadioException('The specified Timeslot Item does not seem to exist'); - return; +class NIPSWeb_TimeslotItem extends \MyRadio\ServiceAPI\ServiceAPI +{ + private $timeslot_item_id; + + private $item_id; + + private $item_type; + + private $item_playlist_ref; + + private $channel; + + private $weight; + + private $cue; + + /** + * Initiates the TimeslotItem variables. + * + * @param int $resid The timeslot_item_id of the resource to initialise + * @param NIPSWeb_ManagedPlaylist $playlistref If the playlist is requesting this item, then pass the playlist obj + */ + protected function __construct($resid, $playlistref = null) + { + $this->timeslot_item_id = $resid; + //*dies* + $result = self::$db->fetchOne( + 'SELECT * FROM bapsplanner.timeslot_items where timeslot_item_id=$1 LIMIT 1', + [$resid] + ); + + if (empty($result)) { + throw new MyRadioException('The specified Timeslot Item does not seem to exist', 404); + + return; + } + + /* + * @todo detect definition of multiple track types in an entry and fail out + */ + if ($result['rec_track_id'] != null) { + //CentralDB + $this->item_type = "CentralDB"; + $this->item_id = $result['rec_track_id']; + } elseif ($result['managed_item_id'] != null) { + //ManagedDB (Central Beds, Jingles...) + $this->item_type = "ManagedDB"; + $this->item_id = $result['managed_item_id']; + $this->item_playlist_ref = $playlistref; + } + + $this->channel = (int) $result['channel_id']; + $this->weight = (int) $result['weight']; + $this->cue = (int) $result['cue']; } - + /** - * @todo detect definition of multiple track types in an entry and fail out - */ - if ($result['rec_track_id'] != null) { - //CentralDB - $this->item = MyRadio_Track::getInstance($result['rec_track_id']); - } elseif ($result['managed_item_id'] != null) { - //ManagedDB (Central Beds, Jingles...) - $this->item = NIPSWeb_ManagedItem::getInstance($result['managed_item_id'], $playlistref); - } - - $this->channel = (int)$result['channel_id']; - $this->weight = (int)$result['weight']; - } - - /** - * Get the unique timeslotitemid of the TimeslotItem - * @return int - */ - public function getID() { - return $this->timeslot_item_id; - } - - public function getChannel() { - return $this->channel; - } - - public function getWeight() { - return $this->weight; - } - - public function getItem() { - return $this->item; - } - - public function setLocation($channel, $weight) { - $this->channel = (int) $channel; - $this->weight = (int) $weight; - self::$db->query('UPDATE bapsplanner.timeslot_items SET channel_id=$1, weight=$2 WHERE timeslot_item_id=$3', - array($this->channel, $this->weight, $this->getID())); - $this->updateCacheObject(); - } - - public function remove() { - self::$db->query('DELETE FROM bapsplanner.timeslot_items WHERE timeslot_item_id=$1', - array($this->getID())); - $this->removeInstance(); - unset($this); - } - - public static function create_managed($timeslot, $manageditemid, $channel, $weight) { - $result = self::$db->fetch_column('INSERT INTO bapsplanner.timeslot_items (timeslot_id, managed_item_id, channel_id, weight) - VALUES ($1, $2, $3, $4) RETURNING timeslot_item_id', - array($timeslot, $manageditemid, $channel, $weight)); - - return self::getInstance($result[0]); - } - - public static function create_central($timeslot, $trackid, $channel, $weight) { - $result = self::$db->fetch_column('INSERT INTO bapsplanner.timeslot_items (timeslot_id, rec_track_id, channel_id, weight) - VALUES ($1, $2, $3, $4) RETURNING timeslot_item_id', - array($timeslot, $trackid, $channel, $weight)); - - return self::getInstance($result[0]); - } - - /** - * Returns an array of key information, useful for Twig rendering and JSON requests - * @todo Expand the information this returns - * @return Array - */ - public function toDataSource() { - return array_merge(array( - 'timeslotitemid' => $this->getID(), - 'channel' => $this->getChannel(), - 'weight' => $this->getWeight() - ), - $this->getItem()->toDataSource() - ); - } + * Get the unique timeslotitemid of the TimeslotItem. + * + * @return int + */ + public function getID() + { + return $this->timeslot_item_id; + } + + public function getChannel() + { + return $this->channel; + } + + public function getWeight() + { + return $this->weight; + } + + /** + * Get the cue point of this timeslotitem, in seconds. + */ + public function getCue() + { + return $this->cue; + } + + public function getItem() + { + if ($this->item_type == "CentralDB") { + return MyRadio_Track::getInstance($this->item_id); + } elseif ($this->item_type == "ManagedDB") { + return NIPSWeb_ManagedItem::getInstance($this->item_id, $this->item_playlist_ref); + } + } + + public function setLocation($channel, $weight) + { + $this->channel = (int) $channel; + $this->weight = (int) $weight; + self::$db->query( + 'UPDATE bapsplanner.timeslot_items SET channel_id=$1, weight=$2 WHERE timeslot_item_id=$3', + [$this->channel, $this->weight, $this->getID()] + ); + $this->updateCacheObject(); + } + + public function setCue($secs) + { + $this->cue = (int) $secs; + self::$db->query( + 'UPDATE bapsplanner.timeslot_items SET cue=$1 WHERE timeslot_item_id=$2', + [$this->cue, $this->getID()] + ); + $this->updateCacheObject(); + } + + public function remove() + { + self::$db->query( + 'DELETE FROM bapsplanner.timeslot_items WHERE timeslot_item_id=$1', + [$this->getID()] + ); + $this->removeInstance(); + } + + public static function createManaged($timeslot, $manageditemid, $channel, $weight) + { + $result = self::$db->fetchColumn( + 'INSERT INTO bapsplanner.timeslot_items (timeslot_id, managed_item_id, channel_id, weight) + VALUES ($1, $2, $3, $4) RETURNING timeslot_item_id', + [$timeslot, $manageditemid, $channel, $weight] + ); + + return self::getInstance($result[0]); + } + + public static function createCentral($timeslot, $trackid, $channel, $weight) + { + $result = self::$db->fetchColumn( + 'INSERT INTO bapsplanner.timeslot_items (timeslot_id, rec_track_id, channel_id, weight) + VALUES ($1, $2, $3, $4) RETURNING timeslot_item_id', + [$timeslot, $trackid, $channel, $weight] + ); + + return self::getInstance($result[0]); + } + + /** + * Returns an array of key information, useful for Twig rendering and JSON requests. + * @param array $mixins Mixins. Currently unused + * @return array + * @todo Expand the information this returns + */ + public function toDataSource($mixins = []) + { + return array_merge( + [ + 'timeslotitemid' => $this->getID(), + 'channel' => $this->getChannel(), + 'weight' => $this->getWeight(), + 'cue' => $this->getCue() + ], + $this->getItem()->toDataSource() + ); + } } diff --git a/src/Classes/NIPSWeb/NIPSWeb_Token.php b/src/Classes/NIPSWeb/NIPSWeb_Token.php index cfeafee3d..f665e138c 100644 --- a/src/Classes/NIPSWeb/NIPSWeb_Token.php +++ b/src/Classes/NIPSWeb/NIPSWeb_Token.php @@ -1,51 +1,72 @@ - * @package MyRadio_NIPSWeb - * @uses \Database + * + * @uses \Database */ -class NIPSWeb_Token extends ServiceAPI { - public static function createToken($trackid) { - return true; - } - - public static function hasToken($trackid) { - return true; - } - - public function getID() { - return $this->id; - } - - /** - * Generate a unique session token - this is as Show Planner clients need to be more unique than a session id - * in case the user has more than one instance of the planner open. - * These IDs are also tied to the timeslot they were using at the time. - * @return int a unique edit token - */ - public static function getEditToken() { - $r = self::$db->fetch_column('INSERT INTO bapsplanner.client_ids (show_season_timeslot_id, session_id) - VALUES ($1, $2) RETURNING client_id', array($_SESSION['timeslotid'], session_id())); - return (int)$r[0]; - } - - /** - * Returns the Timeslot ID the edit token is assigned to - * @param int $client_id - * @return int - */ - public static function getEditTokenTimeslot($client_id) { - $r = self::$db->fetch_column('SELECT show_season_timeslot_id FROM bapsplanner.client_ids - WHERE client_id=$1 LIMIT 1', array($client_id)); - return (int)$r[0]; - } +class NIPSWeb_Token extends \MyRadio\ServiceAPI\ServiceAPI +{ + public static function createToken($trackid) + { + return true; + } + + public static function hasToken($trackid) + { + return true; + } + + public function getID() + { + return $this->id; + } + + /** + * Generate a unique session token - this is as Show Planner clients need to be more unique than a session id + * in case the user has more than one instance of the planner open. + * These IDs are also tied to the timeslot they were using at the time. + * + * @return int a unique edit token + */ + public static function getEditToken() + { + $r = self::$db->fetchColumn( + 'INSERT INTO bapsplanner.client_ids (show_season_timeslot_id, session_id) + VALUES ($1, $2) RETURNING client_id', + [$_SESSION['timeslotid'], session_id()] + ); + + if (empty($r)) { + throw new MyRadioException('Failed to generate Show Planner edit token.', 500); + } + + return (int) $r[0]; + } + + /** + * Returns the Timeslot ID the edit token is assigned to. + * + * @param int $client_id + * + * @return int + */ + public static function getEditTokenTimeslot($client_id) + { + $r = self::$db->fetchColumn( + 'SELECT show_season_timeslot_id FROM bapsplanner.client_ids + WHERE client_id=$1 LIMIT 1', + [$client_id] + ); + + return (int) $r[0]; + } } diff --git a/src/Classes/NIPSWeb/NIPSWeb_Views.php b/src/Classes/NIPSWeb/NIPSWeb_Views.php index 1b5cf427d..69ce8d6a4 100644 --- a/src/Classes/NIPSWeb/NIPSWeb_Views.php +++ b/src/Classes/NIPSWeb/NIPSWeb_Views.php @@ -4,153 +4,156 @@ * and open the template in the editor. */ +namespace MyRadio\NIPSWeb; + /** - * Description of NIPSWeb_Views - * - * @author Lloyd Wallis + * Description of NIPSWeb_Views. */ -class NIPSWeb_Views { - - public static function serveMP3($path) { - //Set mp3 headers - header('Content-Type: audio/mpeg'); +class NIPSWeb_Views +{ + public static function serveMP3($path) + { + //Set mp3 headers + header('Content-Type: audio/mpeg'); - /** - * Partial content support - this is required to set audio.currentTime - * it will also help mitigate some issues with tracks pausing to buffer halfway through - */ - if (!empty($_SERVER['HTTP_RANGE'])) { - //Yeah, a byte range has been requested. We only serve part of the file at this time - self::rangeDownload($path); - } else { - //This is a dumb read-whole-file request - /** - * @todo Investigate whether whole file requests are ever used if partial is available - */ - //Get the size of the file - header('Content-Length: ' . filesize($path)); - //Make sure it doesn't suddently not - header('Connection: Keep-Alive'); - //Read the file - readfile($path); + /* + * Partial content support - this is required to set audio.currentTime + * it will also help mitigate some issues with tracks pausing to buffer halfway through + */ + if (!empty($_SERVER['HTTP_RANGE'])) { + //Yeah, a byte range has been requested. We only serve part of the file at this time + self::rangeDownload($path); + } else { + //This is a dumb read-whole-file request + /* + * @todo Investigate whether whole file requests are ever used if partial is available + */ + //Get the size of the file + header('Content-Length: '.filesize($path)); + //Make sure it doesn't suddently not + header('Connection: Keep-Alive'); + //Read the file + readfile($path); + } } - } - - public static function serveOGG($path) { - //Set mp3 headers - header('Content-Type: audio/ogg'); - /** - * Partial content support - this is required to set audio.currentTime - * it will also help mitigate some issues with tracks pausing to buffer halfway through - */ - if (!empty($_SERVER['HTTP_RANGE'])) { - //Yeah, a byte range has been requested. We only serve part of the file at this time - self::rangeDownload($path); - } else { - //This is a dumb read-whole-file request - /** - * @todo Investigate whether whole file requests are ever used if partial is available - */ - //Get the size of the file - header('Content-Length: ' . filesize($path)); - //Make sure it doesn't suddently not - header('Connection: Keep-Alive'); - //Read the file - readfile($path); + public static function serveOGG($path) + { + //Set mp3 headers + header('Content-Type: audio/ogg'); + + /* + * Partial content support - this is required to set audio.currentTime + * it will also help mitigate some issues with tracks pausing to buffer halfway through + */ + if (!empty($_SERVER['HTTP_RANGE'])) { + //Yeah, a byte range has been requested. We only serve part of the file at this time + self::rangeDownload($path); + } else { + //This is a dumb read-whole-file request + /* + * @todo Investigate whether whole file requests are ever used if partial is available + */ + //Get the size of the file + header('Content-Length: '.filesize($path)); + //Make sure it doesn't suddently not + header('Connection: Keep-Alive'); + //Read the file + readfile($path); + } } - } - - /** - * Allows Partial Content Downloads - useful for audio and video streaming HTML5 stuff - * From http://forums.phpfreaks.com/topic/106711-php-code-which-supports-byte-range-downloads-for-iphone/ - * @param String $file path to the file - */ - public static function rangeDownload($file) { - $fp = @fopen($file, 'rb'); - $size = filesize($file); // File size - $length = $size; // Content length - $start = 0; // Start byte - $end = $size - 1; // End byte - // Now that we've gotten so far without errors we send the accept range header - /* At the moment we only support single ranges. - * Multiple ranges requires some more work to ensure it works correctly - * and comply with the spesifications: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 - * - * Multirange support annouces itself with: - * header('Accept-Ranges: bytes'); + /** + * Allows Partial Content Downloads - useful for audio and video streaming HTML5 stuff + * From http://forums.phpfreaks.com/topic/106711-php-code-which-supports-byte-range-downloads-for-iphone/. * - * Multirange content must be sent with multipart/byteranges mediatype, - * (mediatype = mimetype) - * as well as a boundry header to indicate the various chunks of data. + * @param string $file path to the file */ - header("Accept-Ranges: 0-$length"); - // header('Accept-Ranges: bytes'); - // multipart/byteranges - // http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 - if (isset($_SERVER['HTTP_RANGE'])) { - $c_start = $start; - $c_end = $end; - // Extract the range string - list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2); - // Make sure the client hasn't sent us a multibyte range - if (strpos($range, ',') !== false) { - // (?) Shoud this be issued here, or should the first - // range be used? Or should the header be ignored and - // we output the whole content? - header('HTTP/1.1 416 Requested Range Not Satisfiable'); - header("Content-Range: bytes $start-$end/$size"); - // (?) Echo some info to the client? - exit; - } - // If the range starts with an '-' we start from the beginning - // If not, we forward the file pointer - // And make sure to get the end byte if spesified - if ($range{0} == '-') { - // The n-number of the last bytes is requested - $c_start = $size - substr($range, 1); - } else { - $range = explode('-', $range); - $c_start = $range[0]; - $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size; - } - /* Check the range and make sure it's treated according to the specs. - * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html - */ - // End bytes can not be larger than $end. - $c_end = ($c_end > $end) ? $end : $c_end; - // Validate the requested range and return an error if it's not correct. - if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) { - header('HTTP/1.1 416 Requested Range Not Satisfiable'); - header("Content-Range: bytes $start-$end/$size"); - // (?) Echo some info to the client? - exit; - } - $start = $c_start; - $end = $c_end; - $length = $end - $start + 1; // Calculate new content length - fseek($fp, $start); - header('HTTP/1.1 206 Partial Content'); - } - // Notify the client the byte range we'll be outputting - header("Content-Range: bytes $start-$end/$size"); - header("Content-Length: $length"); + public static function rangeDownload($file) + { + $fp = @fopen($file, 'rb'); - // Start buffered download - $buffer = 1024 * 8; - while (!feof($fp) && ($p = ftell($fp)) <= $end) { - if ($p + $buffer > $end) { - // In case we're only outputtin a chunk, make sure we don't - // read past the length - $buffer = $end - $p + 1; - } - set_time_limit(0); // Reset time limit for big files - echo fread($fp, $buffer); - flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit. - } + $size = filesize($file); // File size + $length = $size; // Content length + $start = 0; // Start byte + $end = $size - 1; // End byte + // Now that we've gotten so far without errors we send the accept range header + /* At the moment we only support single ranges. + * Multiple ranges requires some more work to ensure it works correctly + * and comply with the spesifications: http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 + * + * Multirange support annouces itself with: + * header('Accept-Ranges: bytes'); + * + * Multirange content must be sent with multipart/byteranges mediatype, + * (mediatype = mimetype) + * as well as a boundry header to indicate the various chunks of data. + */ + header("Accept-Ranges: 0-$length"); + // header('Accept-Ranges: bytes'); + // multipart/byteranges + // http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.2 + if (isset($_SERVER['HTTP_RANGE'])) { + $c_start = $start; + $c_end = $end; + // Extract the range string + list(, $range) = explode('=', $_SERVER['HTTP_RANGE'], 2); + // Make sure the client hasn't sent us a multibyte range + if (strpos($range, ',') !== false) { + // (?) Shoud this be issued here, or should the first + // range be used? Or should the header be ignored and + // we output the whole content? + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + header("Content-Range: bytes $start-$end/$size"); + // (?) Echo some info to the client? + exit; + } + // If the range starts with an '-' we start from the beginning + // If not, we forward the file pointer + // And make sure to get the end byte if spesified + if ($range[0] == '-') { + // The n-number of the last bytes is requested + $c_start = $size - substr($range, 1); + } else { + $range = explode('-', $range); + $c_start = $range[0]; + $c_end = (isset($range[1]) && is_numeric($range[1])) ? $range[1] : $size; + } + /* Check the range and make sure it's treated according to the specs. + * http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html + */ + // End bytes can not be larger than $end. + $c_end = ($c_end > $end) ? $end : $c_end; + // Validate the requested range and return an error if it's not correct. + if ($c_start > $c_end || $c_start > $size - 1 || $c_end >= $size) { + header('HTTP/1.1 416 Requested Range Not Satisfiable'); + header("Content-Range: bytes $start-$end/$size"); + // (?) Echo some info to the client? + exit; + } + $start = $c_start; + $end = $c_end; + $length = $end - $start + 1; // Calculate new content length + fseek($fp, $start); + header('HTTP/1.1 206 Partial Content'); + } + // Notify the client the byte range we'll be outputting + header("Content-Range: bytes $start-$end/$size"); + header("Content-Length: $length"); - fclose($fp); - } + // Start buffered download + $buffer = 1024 * 8; + while (!feof($fp) && ($p = ftell($fp)) <= $end) { + if ($p + $buffer > $end) { + // In case we're only outputtin a chunk, make sure we don't + // read past the length + $buffer = $end - $p + 1; + } + set_time_limit(0); // Reset time limit for big files + echo fread($fp, $buffer); + flush(); // Free up memory. Otherwise large files will trigger PHP's memory limit. + } -} \ No newline at end of file + fclose($fp); + } +} diff --git a/src/Classes/NIPSWeb/getID3.php b/src/Classes/NIPSWeb/getID3.php deleted file mode 100644 index af1ef4d67..000000000 --- a/src/Classes/NIPSWeb/getID3.php +++ /dev/null @@ -1,2 +0,0 @@ - - * @package MyRadio_SIS + * This class has helper functions for building SIS. */ -class SIS_Messages extends ServiceAPI { - - const MSG_STATUS_UNREAD = 1; - const MSG_STATUS_READ = 2; - const MSG_STATUS_DELETED = 3; - const MSG_STATUS_JUNK = 4; - const MSG_STATUS_ABUSIVE = 5; +class SIS_Messages extends ServiceAPI +{ + const MSG_STATUS_UNREAD = 1; + const MSG_STATUS_READ = 2; + const MSG_STATUS_DELETED = 3; + const MSG_STATUS_JUNK = 4; + const MSG_STATUS_ABUSIVE = 5; - /** - * Returns an array of messages - * @param int $timeslotid What timeslot to fetch messages for - * @param int $offset Only message IDs greater than this will be returned - * @return array An array of SIS messages - */ - public static function getMessages($timeslotid, $offset = 0) { - return MyRadio_Timeslot::getInstance($timeslotid)->getMessages($offset); - } + /** + * Returns an array of messages. + * + * @param int $timeslotid What timeslot to fetch messages for + * @param int $offset Only message IDs greater than this will be returned + * + * @return array An array of SIS messages + */ + public static function getMessages($timeslotid, $offset = 0) + { + return MyRadio_Timeslot::getInstance($timeslotid)->getMessages($offset); + } - /** - * Update the status of a message - * @param int $id The ID of the message to update - * @param int $status The new status of the message - */ - public static function setMessageStatus($id, $status = self::MSG_STATUS_READ) { - self::$db->query('UPDATE sis2.messages SET statusid=$1 WHERE commid=$2', - array($status, $id)); - } -} \ No newline at end of file + /** + * Update the status of a message. + * + * @param int $id The ID of the message to update + * @param int $status The new status of the message + */ + public static function setMessageStatus($id, $status = self::MSG_STATUS_READ) + { + self::$db->query( + 'UPDATE sis2.messages SET statusid=$1 WHERE commid=$2', + [$status, $id] + ); + } +} diff --git a/src/Classes/SIS/SIS_Remote.php b/src/Classes/SIS/SIS_Remote.php old mode 100755 new mode 100644 index 7b8b79f3c..63fd66634 --- a/src/Classes/SIS/SIS_Remote.php +++ b/src/Classes/SIS/SIS_Remote.php @@ -5,66 +5,124 @@ * @package MyRadio_SIS */ +namespace MyRadio\SIS; + +use MyRadio\Config; +use MyRadio\ServiceAPI\ServiceAPI; +use MyRadio\ServiceAPI\MyRadio_Selector; +use MyRadio\ServiceAPI\MyRadio_Webcam; +use MyRadio\MyRadio\MyRadioNews; + /** - * This class has helper functions for long-polling SIS - * - * @version 20131101 - * @author Andy Durant - * @package MyRadio_SIS + * This class has helper functions for long-polling SIS. */ -class SIS_Remote extends ServiceAPI { +class SIS_Remote extends ServiceAPI +{ + /** + * Gets the latest presenter info. + * + * @param array $session phpSession variable + * + * @return array presenter info data + */ + public static function queryPresenterInfo($session) + { + $time = 0; + if (isset($_REQUEST['presenterinfo-lasttime'])) { + $time = (int) $_REQUEST['presenterinfo-lasttime']; + } + if ($time < time() - 300) { + $response = MyRadioNews::getLatestNewsItem(Config::$presenterinfo_feed); + + return [ + 'presenterinfo' => ['time' => time(), 'info' => $response], + ]; + } else { + return []; + } + } - /** - * Gets the latest messages for the selected timeslot - * @param array $session phpSession variable - * @return array message data - */ - public static function query_messages($session) { - $response = SIS_Messages::getMessages($session['timeslotid'], isset($_REQUEST['messages_highest_id']) ? $_REQUEST['messages_highest_id'] : 0); + /** + * Gets the latest messages for the selected timeslot. + * + * @param array $session phpSession variable + * + * @return array message data + */ + public static function queryMessages($session) + { + $response = SIS_Messages::getMessages( + $session['timeslotid'], + isset($_REQUEST['messages_highest_id']) ? $_REQUEST['messages_highest_id'] : 0 + ); - if (!empty($response) && $response !== false) { - return array('messages' => $response); - } - } + if (!empty($response) && $response !== false) { + return ['messages' => $response]; + } + } - /** - * Gets the latest tracklist data for the selected timeslot - * @param array $session phpSession variable - * @return array tracklist data - */ - public static function query_tracklist($session) { - $response = SIS_Tracklist::getTrackListing($session['timeslotid'], isset($_REQUEST['tracklist_highest_id']) ? $_REQUEST['tracklist_highest_id'] : 0); + /** + * Gets the latest tracklist data for the selected timeslot. + * + * @param array $session phpSession variable + * + * @return array tracklist data + */ + public static function queryTracklist($session) + { + $response = SIS_Tracklist::getTrackListing( + $session['timeslotid'], + isset($_REQUEST['tracklist_highest_id']) ? $_REQUEST['tracklist_highest_id'] : 0 + ); - if (!empty($response) && $response !== false) { - return array('tracklist' => $response); - } - - } + if (!empty($response) && $response !== false) { + return ['tracklist' => $response]; + } + } - /** - * Gets the latest selector status - * @param array $session phpSession variable - * @return array selector status - */ - public static function query_selector($session) { - $response = MyRadio_Selector::getStatusAtTime(time()); + /** + * Gets the latest selector status. + * + * @param array $session phpSession variable + * + * @return array selector status + */ + public static function querySelector($session) + { + $time = 0; + if (isset($_REQUEST['selector-lasttime'])) { + $time = (int) $_REQUEST['selector-lasttime']; + } - if ($response['lastmod'] > $_REQUEST['selector_lastmod']) { - return array('selector' => $response); - } - } + $response = MyRadio_Selector::getStatusAtTime(); - /** - * Gets the latest webcam status - * @param array $session phpSession variable - * @return array webcam status - */ - public static function query_webcam($session) { - $response = MyRadio_Webcam::getCurrentWebcam(); + if ($response['lastmod'] > $time) { + return ['selector' => $response]; + } + } - if ($response['current'] != $_REQUEST['webcam_id']) { - return array('webcam' => $response); - } - } + /** + * Gets the latest webcam status. + * + * @param array $session phpSession variable + * + * @return array webcam status + */ + public static function queryWebcam($session) + { + $response = MyRadio_Webcam::getCurrentWebcam(); + $camera = null; + if (isset($_REQUEST['webcam-id'])) { + $camera = $_REQUEST['webcam-id']; + } -} \ No newline at end of file + if ($response['camera'] !== $camera) { + return [ + 'webcam' => [ + 'status' => $response, + 'streams' => MyRadio_Webcam::getStreams(), + ], + ]; + } + } +} diff --git a/src/Classes/SIS/SIS_Tracklist.php b/src/Classes/SIS/SIS_Tracklist.php old mode 100755 new mode 100644 index 3b6ecdae9..3e30054d2 --- a/src/Classes/SIS/SIS_Tracklist.php +++ b/src/Classes/SIS/SIS_Tracklist.php @@ -5,108 +5,110 @@ * @package MyRadio_SIS */ +namespace MyRadio\SIS; + +use MyRadio\ServiceAPI\ServiceAPI; +use MyRadio\ServiceAPI\MyRadio_TracklistItem; +use MyRadio\ServiceAPI\MyRadio_Track; + /** - * This class has helper functions for SIS tracklisting - * - * @version 20131011 - * @author Andy Durant - * @package MyRadio_SIS + * This class has helper functions for SIS tracklisting. */ -class SIS_Tracklist extends ServiceAPI { +class SIS_Tracklist extends ServiceAPI +{ + /** + * Get track info tracklisted for a timeslot. + * + * @param int $timeslotid ID of timslot to get tracklist for + * @param int $offset tracklist logid to offset by + * + * @return array tracks in tracklist for the timeslot from the offset + */ + public static function getTrackListing($timeslotid, $offset = 0) + { + $tracklist = MyRadio_TracklistItem::getTracklistForTimeslot($timeslotid, $offset); + $tracks = []; + foreach ($tracklist as $tracklistitem) { + $track = $tracklistitem->getTrack(); + if (is_array($track)) { + $tracks[] = [ + 'playtime' => $tracklistitem->getStartTime(), + 'title' => $track['title'], + 'artist' => $track['artist'], + 'album' => $track['album'], + 'trackid' => 'custom', + 'id' => $tracklistitem->getID(), + ]; + } else { + $tracks[] = [ + 'playtime' => $tracklistitem->getStartTime(), + 'title' => $track->getTitle(), + 'artist' => $track->getArtist(), + 'album' => $track->getAlbum()->getTitle(), + 'trackid' => $track->getID(), + 'id' => $tracklistitem->getID(), + ]; + } + } - /** - * Get track info tracklisted for a timeslot - * @param integer $timeslotid ID of timslot to get tracklist for - * @param integer $offset tracklist logid to offset by - * @return array tracks in tracklist for the timeslot from the offset - */ - public static function getTrackListing($timeslotid, $offset = 0) { - $tracklist = MyRadio_TracklistItem::getTracklistForTimeslot($timeslotid, $offset); - $tracks = array(); - foreach ($tracklist as $tracklistitem) { - $track = $tracklistitem->getTrack(); - if (is_array($track)) { - $tracks[] = array( - 'playtime' => $tracklistitem->getStartTime(), - 'title' => $track['title'], - 'artist' => $track['artist'], - 'album' => $track['album'], - 'id' => $tracklistitem->getID() - ); - } - else { - $tracks[] = array( - 'playtime' => $tracklistitem->getStartTime(), - 'title' => $track->getTitle(), - 'artist' => $track->getArtist(), - 'album' => $track->getAlbum()->getTitle(), - 'id' => $tracklistitem->getID() - ); - } - } - return $tracks; - } + return $tracks; + } - /** - * Adds a non-database track to the tracklist - * @param string $tname track name - * @param string $artist track artist - * @param string $album track album - * @param time $time php time - * @param string $source tracklistig source - * @param int $timeslotid ID of timeslot to tracklist to - * @return none - */ - public static function insertTrackNoRec($tname, $artist, $album, $time, $source, $timeslotid) { - self::$db->query('BEGIN'); + /** + * Adds a non-database track to the tracklist. + * + * @param string $tname track name + * @param string $artist track artist + * @param string $album track album + * @param string $source tracklistig source + * @param int $timeslotid ID of timeslot to tracklist to + * + * @return none + */ + public static function insertTrackNoRec($tname, $artist, $album, $source, $timeslotid) + { + self::$db->query('BEGIN'); - $audiologid = self::$db->fetch_one('INSERT INTO tracklist.tracklist (source, timeslotid) - VALUES ($1, $2) RETURNING audiologid', - array($source, $timeslotid)); + $audiologid = self::$db->fetchOne( + 'INSERT INTO tracklist.tracklist (source, timeslotid) VALUES ($1, $2) RETURNING audiologid', + [$source, $timeslotid] + ); - self::$db->query('INSERT INTO tracklist.track_notrec (audiologid, artist, album, track) - VALUES ($1, $2, $3, $4)', - array($audiologid['audiologid'], $artist, $album, $tname)); + self::$db->query( + 'INSERT INTO tracklist.track_notrec (audiologid, artist, album, track) VALUES ($1, $2, $3, $4)', + [$audiologid['audiologid'], $artist, $album, $tname] + ); - self::$db->query('COMMIT'); - } + self::$db->query('COMMIT'); + } - /** - * checks if track is in database - * @param string $artist track artist - * @param string $album track album - * @param string $tname track name - * @return array result of db query - */ - public static function checkTrackOK($artist, $album, $tname) { - $result = self::$db->fetch_all('SELECT DISTINCT trk.title AS track, rec.title AS album, trk.artist AS artist, trk.trackid AS trackid, rec.recordid AS recordid - FROM rec_track trk - INNER JOIN rec_record rec ON ( rec.recordid = trk.recordid ) - WHERE trk.artist ILIKE $4 || $1 || $4 - AND rec.title ILIKE $4 || $2 || $4 - AND trk.title ILIKE $4 || $3 || $4 - ORDER BY trk.title ASC LIMIT 10', - array($artist, $album, $tname, '%')); - return $result; - } + public static function insertTrackRec(MyRadio_Track $track, $source, $timeslotid) + { + self::$db->query('BEGIN'); - public static function insertTrackRec($trackid, $recid, $time, $source, $timeslotid) { - self::$db->query('BEGIN'); + $audiologid = self::$db->fetchOne( + 'INSERT INTO tracklist.tracklist (source, timeslotid) + VALUES ($1, $2) RETURNING audiologid', + [$source, $timeslotid] + ); - $audiologid = self::$db->fetch_one('INSERT INTO tracklist.tracklist (source, timeslotid) - VALUES ($1, $2) RETURNING audiologid', - array($source, $timeslotid)); + self::$db->query( + 'INSERT INTO tracklist.track_rec (audiologid, recordid, trackid) + VALUES ($1, $2, $3)', + [$audiologid['audiologid'], $track->getAlbum()->getID(), $track->getID()] + ); - self::$db->query('INSERT INTO tracklist.track_rec (audiologid, recordid, trackid) - VALUES ($1, $2, $3)', - array($audiologid['audiologid'], $recid, $trackid)); + self::$db->query('COMMIT'); - self::$db->query('COMMIT'); - } + return true; + } - public static function markTrackDeleted($tracklistid){ - self::$db->query('UPDATE tracklist.tracklist SET state = \'d\' - WHERE audiologid = $1', - array($tracklistid)); - } -} \ No newline at end of file + public static function markTrackDeleted($tracklistid) + { + self::$db->query( + 'UPDATE tracklist.tracklist SET state = \'d\' + WHERE audiologid = $1', + [$tracklistid] + ); + } +} diff --git a/src/Classes/SIS/SIS_Utils.php b/src/Classes/SIS/SIS_Utils.php index 53ae10b58..f66f4735a 100644 --- a/src/Classes/SIS/SIS_Utils.php +++ b/src/Classes/SIS/SIS_Utils.php @@ -1,181 +1,160 @@ - - * @package MyRadio_SIS - */ -class SIS_Utils extends ServiceAPI { - - /** - * Creates a list of files from a given directory with an optional filename - * @param String $d Path to directory - * @param String $x File Extension (optional) - * @return Array List of files - */ - private static function file_list($d,$x){ - return array_diff(scandir(__DIR__.'/../../'.$d),array('.','..')); - } - - /** - * Checks whether the client IP is a machine authorised for full control - * @param String $ip The IP address to check. If null, will use the REMOTE_ADDR server property - * @return boolean|int The studio's ID number or false if unauthorised - */ - private static function isAuthenticatedMachine($ip = null) { - if (is_null($ip)) { - $ip = $_SERVER['REMOTE_ADDR']; - } - - foreach (Config::$studios as $key => $studio) { - if (in_array($ip, $studio['authenticated_machines'])) { - //This client is authorised - return $key; - } - } - return False; - } - - /** - * Gets module data (tabs or plugins) that are enabled - * @param String $moduleFolder The folder to read the modules from - * @return Array moduleInfo - */ - private static function getModules($moduleFolder) { - $modules = self::file_list($moduleFolder,'php'); - $loadedModules = array(); - if ($modules !== false) { - foreach ($modules as $key => $module) { - include $moduleFolder.'/'.$module; - if (!isset($moduleInfo)) { - trigger_error('Error with $module: \$moduleInfo must be set for each module.'); - continue; - } - if (isset($moduleInfo['enabled']) && ($moduleInfo['enabled'] != true)) { - continue; - } - $loadedModules[] = $moduleInfo; - } - return $loadedModules; - } - return false; - } - - /** - * Gets the module data (tabs or plugins) based on the active users permissions - * @param String $moduleFolder The folder to read the modules from - * @return Array moduleInfo - */ - private static function getModulesForUser($moduleFolder) { - $modules = self::getModules($moduleFolder); - $loadedModules = array(); - if ($modules !== false) { - foreach ($modules as $key => $module) { - $notAuth = (isset($module['required_permission']) && !CoreUtils::hasPermission($module['required_permission'])); - /** - * @todo Replace with MyRadio built in location Auth - */ - $notStudio = (isset($module['required_location']) && ($module['required_location'] === True && self::isAuthenticatedMachine() === False)); - - if ($notAuth && $notStudio) { - continue; - } - $loadedModules[] = $module; - } - return $loadedModules; - } - return false; - } - - /** - * Gets the plugin data from the configured sis_plugin_folder - * @return Array pluginInfo - */ - public static function getPlugins() { - return self::getModulesForUser(Config::$sis_plugin_folder); - } - - /** - * Gets the tab data from the configured sis_tab_folder - * @return Array tabInfo - */ - public static function getTabs() { - return self::getModulesForUser(Config::$sis_tab_folder); - } - - /** - * Looks up IP location from Campus Network Data or GeoIP - * @param String $ip IP address to lookup - * @return String Location - */ - public static function ipLookup($ip) { - $query = self::$db->query('SELECT iscollege, description FROM l_subnet WHERE subnet >> $1 ORDER BY description ASC', array($ip)); - - $location = array(); - - if (($query === null) or (pg_num_rows($query) == 0)) { - $geoip = geoip_record_by_name($ip); - $location[0] = ($geoip === FALSE) ? 'Unknown' : empty($geoip['city']) ? "{$geoip['country_name']}" : utf8_encode($geoip['city']).", {$geoip['country_name']}"; - return $location; - } - $q = self::$db->fetch_all($query); - foreach ($q as $k) { - $location[] = $k['description']; - $location[] = ($k['iscollege'] == 't') ? 'College Bedroom' : 'Study Room / Labs / Wifi'; - } - return $location; - } - - /** - * Read the loaded modules and returns the poll functions, if configured - * @param array $modules the loaded modules - * @return array $pollFuncs functions to run for LongPolling - */ - public static function readPolls($modules) { - if ($modules !== false) { - $pollFuncs = array(); - foreach ($modules as $module) { - if (isset($module['pollfunc'])) { - $pollFuncs[] = $module['pollfunc']; - } - } - return $pollFuncs; - } - return false; - } - - /** - * Check whether to load the Getting Started tab - * @return boolean - */ - public static function getShowHelpTab($memberid) { - $result = self::$db->fetch_column('SELECT helptab FROM sis2.member_options WHERE memberid=$1 LIMIT 1', - [$memberid]); - if (empty($result)) { - self::setHelpTab($memberid); - return true; - } - return ($result[0] === 't'); - } - - /** - * Prevent showing the getting started tab at startup - */ - public static function hideHelpTab($memberid) { - $result = self::$db->query('UPDATE sis2.member_options SET helptab=false WHERE memberid=$1', - [$memberid]); - } - - private static function setHelpTab($memberid) { - self::$db->query('INSERT INTO sis2.member_options (memberid, helptab) - VALUES ($1, false)', - [$memberid]); - } -} \ No newline at end of file +> $1 ORDER BY description ASC'; + $params = [$ip]; + + $query = self::$db->query($sql, $params); + + $location = []; + + if (($query === null) or (pg_num_rows($query) == 0)) { + MyRadioGeoIP::wakeup(); + try { + $geoip = MyRadioGeoIP::getInstance()->city($ip); + if (empty($geoip->city)) { + $location[0] = $geoip->country->name; + } else { + $location[0] = utf8_encode($geoip->city->name) . ", " . $geoip->country->name; + } + } catch (AddressNotFoundException $e) { + $location[0] = 'Unknown'; + } + + return $location; + } + $q = self::$db->fetchAll($sql, $params); + foreach ($q as $k) { + $location[] = $k['description']; + $location[] = ($k['iscollege'] == 't') ? 'College Bedroom' : 'Study Room / Labs / Wifi'; + } + + return $location; + } + + /** + * Read the loaded modules and returns the poll functions, if configured. + * + * @param array $modules the loaded modules + * + * @return array $pollFuncs functions to run for LongPolling + */ + public static function readPolls($modules) + { + if ($modules !== false) { + $pollFuncs = []; + foreach ($modules as $module) { + if (stream_resolve_include_path('Models/SIS/modules/'.$module.'.php')) { + require 'Models/SIS/modules/'.$module.'.php'; + if (isset($moduleInfo['pollfunc'])) { + $pollFuncs[] = $moduleInfo['pollfunc']; + } + } + } + + return $pollFuncs; + } + + return false; + } + + /** + * Checks message for suspected spam strings. + * + * @param string $message text to test for spam + * + * @return bool spam true, else false + */ + public static function checkMessageSpam($message) + { + if (strlen($message) > 1000) { + return true; + } + if (!empty(Config::$spam)) { + foreach (Config::$spam as $needle) { + if (stripos($message, $needle) !== false) { + return true; + } + } + return false; + } else { + return false; + } + } + + /** + * Checks message for suspected social engineering attack. + * + * @param string $message text to test for social engineering + * + * @return mixed warning string or false + */ + public static function checkMessageSocialEngineering($message) + { + if (!empty(Config::$social_engineering_trigger)) { + foreach (Config::$social_engineering_trigger as $trigger) { + if (stripos($message, $trigger) !== false) { + return Config::$social_engineering_warning; + } else { + return false; + } + } + } else { + return false; + } + } +} diff --git a/src/Classes/ServiceAPI/Artist.php b/src/Classes/ServiceAPI/Artist.php deleted file mode 100644 index c0e144577..000000000 --- a/src/Classes/ServiceAPI/Artist.php +++ /dev/null @@ -1,141 +0,0 @@ - - * @todo The completion of this module is impossible as Artists do not have - * unique identifiers. For this to happen, BAPS needs to be replaced/updated - * @package MyRadio_Core - * @uses \Database - */ -class Artist extends ServiceAPI { - - /** - * Initiates the Artist object - * @param int $artistid The ID of the Artist to initialise - */ - protected function __construct($artistid) { - $this->artistid = $artistid; - throw new MyRadioException('Not implemented Artist::__construct'); - } - - /** - * Returns an Array of Artists matching the given partial name - * @param String $title A partial or total title to search for - * @param int $limit The maximum number of tracks to return - * @return Array 2D with each first dimension an Array as follows:
    - * title: The name of the artist
    - * artistid: Always 0 until Artist support is implemented - */ - public static function findByName($title, $limit) { - $title = trim($title); - return self::$db->fetch_all('SELECT DISTINCT rec_track.artist AS title, 0 AS artistid - FROM rec_track WHERE rec_track.artist ILIKE \'%\' || $1 || \'%\' LIMIT $2', - array($title, $limit)); - } - - /** - * - * @param Array $options One or more of the following: - * title: String title of the track - * artist: String artist name of the track - * digitised: If true, only return digitised tracks. If false, return any. - * limit: Maximum number of items to return. 0 = No Limit - * trackid: int Track id - * lastfmverified: Boolean whether or not verified with Last.fm Fingerprinter. Default any. - * random: If true, sort randomly - * idsort: If true, sort by trackid - * custom: A custom SQL WHERE clause - * precise: If true, will only return exact matches for artist/title - * nocorrectionproposed: If true, will only return items with no correction proposed. - * clean: Default any. 'y' for clean tracks, 'n' for dirty, 'u' for unknown. - * - */ - public static function findByOptions($options) { - self::wakeup(); - - if (empty($options['title'])) { - $options['title'] = ''; - } - if (empty($options['artist'])) { - $options['artist'] = ''; - } - if (empty($options['album'])) { - $options['album'] = ''; - } - if (!isset($options['digitised'])) { - $options['digitised'] = true; - } - if (empty($options['itonesplaylistid'])) { - $options['itonesplaylistid'] = null; - } - if (!isset($options['limit'])) { - $options['limit'] = Config::$ajax_limit_default; - } - if (empty($options['trackid'])) { - $options['trackid'] = null; - } - if (empty($options['lastfmverified'])) { - $options['lastfmverified'] = null; - } - if (empty($options['random'])) { - $options['random'] = null; - } - if (empty($options['idsort'])) { - $options['idsort'] = null; - } - if (empty($options['custom'])) { - $options['custom'] = null; - } - if (empty($options['precise'])) { - $options['precise'] = false; - } - if (empty($options['nocorrectionproposed'])) { - $options['nocorrectionproposed'] = false; - } - if (empty($options['clean'])) { - $options['clean'] = false; - } - - //Prepare paramaters - $sql_params = array($options['title'], $options['artist'], $options['album'], $options['precise'] ? '' : '%'); - $count = 4; - if ($options['limit'] != 0) { - $sql_params[] = $options['limit']; - $count++; - $limit_param = $count; - } - if ($options['clean']) { - $sql_params[] = $options['clean']; - $count++; - $clean_param = $count; - } - - //Do the bulk of the sorting with SQL - $result = self::$db->fetch_all('SELECT DISTINCT rec_track.artist - FROM rec_track - INNER JOIN rec_record ON ( rec_track.recordid = rec_record.recordid ) - WHERE rec_track.title ILIKE $4 || $1 || $4 - AND rec_track.artist ILIKE $4 || $2 || $4 - AND rec_record.title ILIKE $4 || $3 || $4 - ' . ($options['digitised'] ? ' AND digitised=\'t\'' : '') . ' - ' . ($options['lastfmverified'] === true ? ' AND lastfm_verified=\'t\'' : '') - . ($options['lastfmverified'] === false ? ' AND lastfm_verified=\'f\'' : '') - . ($options['nocorrectionproposed'] === true ? ' AND trackid NOT IN ( - SELECT trackid FROM public.rec_trackcorrection WHERE state=\'p\' - )' : '') - . ($options['clean'] != null ? ' AND clean=$'.$clean_param : '') - . ($options['custom'] !== null ? ' AND ' . $options['custom'] : '') - . ($options['random'] ? ' ORDER BY RANDOM()' : '') - . ($options['idsort'] ? ' ORDER BY trackid' : '') - . ($options['limit'] == 0 ? '' : ' LIMIT $'.$limit_param), $sql_params); - - return $result; - } -} diff --git a/src/Classes/ServiceAPI/MyRadio_APICaller_Common.php b/src/Classes/ServiceAPI/MyRadio_APICaller_Common.php new file mode 100644 index 000000000..95c3f31b7 --- /dev/null +++ b/src/Classes/ServiceAPI/MyRadio_APICaller_Common.php @@ -0,0 +1,113 @@ +permissions at startup. + * + * @uses \Database + */ +trait MyRadio_APICaller_Common +{ + protected $permissions; + + /** + * Returns the API key's active permission flags. + * + * @return array + */ + public function getPermissions() + { + return $this->permissions; + } + + /** + * Returns if the user has the given permission. + * + * Always use AuthUtils::hasAuth when working with the current user. + * + * @param null|int $authid The permission to test for. Null is "no permission required" + * + * @return bool Whether this user has the requested permission + */ + public function hasAuth($authid) + { + // I am become superuser, doer of API calls + if (in_array(AUTH_APISUDO, $this->getPermissions())) { + return true; + } + return $authid === null || in_array((int) $authid, $this->getPermissions()); + } + + /** + * Returns if the user can call this method via the REST API. + */ + public function canCall($class, $method) + { + // I am become superuser, doer of API calls + if ($this->hasAuth(AUTH_APISUDO)) { + return true; + } + + $result = MyRadio_Swagger::getCallRequirements($class, $method); + if ($result === null) { + return false; //No permissions means the method is not accessible + } + + if (empty($result)) { + return true; //An empty array means no permissions needed + } + + foreach ($result as $type) { + if ($this->hasAuth($type)) { + return true; //The Key has that permission + } + } + + return false; //Didn't match anything... + } + + /** + * Tells you whether this APICaller can use the given mixins. + * + * @param string $class The class the method belongs to (actual, not API Alias) + * @param string[] $mixins The mixins being called + * + * @return bool Whether or not the user can call this + */ + public function canMixin($class, $mixins) + { + // I am become superuser, doer of API calls + if ($this->hasAuth(AUTH_APISUDO)) { + return true; + } + + foreach ($mixins as $mixin) { + $result = MyRadio_Swagger::getMixinRequirements($class, $mixin); + if ($result === null) { + return false; //No permissions means the method is not accessible + } + + $ok = false; + if (empty($result)) { + $ok = true; //An empty array means no permissions needed + } else { + foreach ($result as $type) { + if ($this->hasAuth($type)) { + $ok = true; //The Key has that permission + break; + } + } + } + + if (!$ok) { + return false; + } + } + + return true; + } +} diff --git a/src/Classes/ServiceAPI/MyRadio_APIKey.php b/src/Classes/ServiceAPI/MyRadio_APIKey.php index fc749172e..83c7a467a 100644 --- a/src/Classes/ServiceAPI/MyRadio_APIKey.php +++ b/src/Classes/ServiceAPI/MyRadio_APIKey.php @@ -1,112 +1,69 @@ - * @package MyRadio_API - * @uses \Database + * + * @uses \Database */ -class MyRadio_APIKey extends ServiceAPI { - - /** - * The API Key - * @var String - */ - private $key; - - /** - * The Permission flags this API key has. - * @var int[] - */ - private $permissions; - - /** - * Construct the API Key Object - * @param String $key - */ - protected function __construct($key) { - $this->key = $key; - $this->permissions = self::$db->fetch_column('SELECT typeid FROM myury.api_key_auth WHERE key_string=$1', array($key)); - } - - /** - * Check if this API Key can call the given Method. - * - * @param String $class The class the method belongs to (actual, not API Alias) - * @param String $method The method being called - * @return boolean - */ - public function canCall($class, $method) { - if (in_array(AUTH_APISUDO, $this->permissions)) { - return true; - } - - $result = self::getCallRequirements($class, $method); - - if ($result === null) { - return false; //No permissions means the method is not accessible +class MyRadio_APIKey extends ServiceAPI implements APICaller +{ + use MyRadio_APICaller_Common; + + /** + * The API Key. + * + * @var string + */ + private $key; + + /** + * Whether the API key has been revoked. + * + * @var bool + */ + private $revoked; + + /** + * Construct the API Key Object. + * + * @param string $key + */ + protected function __construct($key) + { + $this->key = $key; + $revoked = self::$db->fetchColumn('SELECT revoked from myury.api_key WHERE key_string=$1', [$key]); + $this->revoked = ($revoked[0] == 't'); + $this->permissions = array_map( + 'intval', + self::$db->fetchColumn( + 'SELECT typeid FROM myury.api_key_auth WHERE key_string=$1', + [$key] + ) + ); } - if (empty($result)) { - return true; //An empty array means no permissions needed + /** + * Get the key for this apikey. + */ + public function getID() + { + return $this->key; } - foreach ($result as $type) { - if (in_array($type, $this->permissions)) { - return true; //The Key has that permission - } - } - - return false; //Didn't match anything... - } - - /** - * Logs that this API Key has called something. Used for auditing. - * - * @param String $uri - * @param Array $args - * @deprecated - * @todo A better way of doing this - */ - public function logCall($uri, $args) { - return; - self::$db->query('INSERT INTO myury.api_key_log (key_string, remote_ip, request_path, request_params) - VALUES ($1, $2, $3, $4)', array($this->key, $_SERVER['REMOTE_ADDR'], $uri, json_encode($args))); - } - - /** - * Get the permissions that are needed to access this API Call. - * - * If the return values is null, this method cannot be called. - * If the return value is an empty array, no permissions are needed. - * - * @param String $class The class the method belongs to (actual, not API Alias) - * @param String $method The method being called - * @return int[] - */ - public static function getCallRequirements($class, $method) { - $result = self::$db->fetch_column('SELECT typeid FROM myury.api_method_auth WHERE class_name=$1 AND - (method_name=$2 OR method_name IS NULL)', array($class, $method)); - if (empty($result)) { - return null; + /** + * Get if the key has been revoked. + */ + public function isRevoked() + { + return $this->revoked; } - - foreach ($result as $row) { - if (empty($row)) { - return array(); //There's a global auth option - } - } - - return $result; - } - -} \ No newline at end of file +} diff --git a/src/Classes/ServiceAPI/MyRadio_Album.php b/src/Classes/ServiceAPI/MyRadio_Album.php index 10a3dc376..3a09986a5 100644 --- a/src/Classes/ServiceAPI/MyRadio_Album.php +++ b/src/Classes/ServiceAPI/MyRadio_Album.php @@ -1,378 +1,438 @@ - * @author Lloyd Wallis - * @package MyRadio_Core + * The Album class fetches information about albums in the Central Database. + * * @uses \Database - * */ - -class MyRadio_Album extends ServiceAPI { - - /** - * The Title of the release - * @var String - */ - private $title; - - private $artist; - - private $status; - - private $media; - - private $format; - - private $record_label; - - private $date_added; - - private $date_released; - - private $shelf_number; - - private $shelf_letter; - - private $albumid; - - private $member_add; - - private $member_edit; - - private $last_modified; - - private $cdid; - - private $location; - - private $label; - - private $tracks = array(); - - protected function __construct($recordid) { - $this->albumid = $recordid; - - $result = self::$db->fetch_one('SELECT * FROM (SELECT * FROM public.rec_record WHERE recordid=$1 LIMIT 1) AS t1 - LEFT JOIN public.rec_statuslookup ON t1.status = rec_statuslookup.status_code - LEFT JOIN public.rec_medialookup ON t1.media = rec_medialookup.media_code - LEFT JOIN public.rec_formatlookup ON t1.format = rec_formatlookup.format_code - LEFT JOIN public.rec_locationlookup ON t1.location = rec_locationlookup.location_code', array($recordid)); - - if (empty($result)) { - throw new MyRadioException('The specified Record/Album does not seem to exist'); - return; - } - - $this->title = $result['title']; - $this->artist = $result['artist']; - $this->status = $result['status_descr']; - $this->media = $result['media_descr']; - $this->format = $result['format_descr']; - $this->record_label = $result['recordlabel']; - $this->date_added = strtotime($result['dateadded']); - $this->date_released = strtotime($result['datereleased']); - $this->shelf_number = (int)$result['shelfnumber']; - $this->shelf_letter = $result['shelfletter']; - $this->member_add = empty($result['memberid_add']) ? null : (int)$result['memberid_add']; - $this->member_edit = empty($result['memberid_edit']) ? null : (int)$result['memberid_edit']; - $this->last_modified = strtotime($result['datetime_lastedit']); - $this->cdid = $result['cdid']; - $this->location = $result['location_descr']; - - $this->tracks = self::$db->fetch_column('SELECT trackid FROM rec_track WHERE recordid=$1', array($this->albumid)); - } - - public function getID() { - return $this->albumid; - } - - public function getTracks() { - return MyRadio_Track::resultSetToObjArray($this->tracks); - } - - public function getTitle() { - return $this->title; - } - - public function getFolder() { - $dir = Config::$music_central_db_path.'/records/'.$this->getID(); - if (!is_dir($dir)) { - mkdir($dir); - } - return $dir; - } - - /** - * - * @param String $paramName The key to update, e.g. title. - * Don't be silly and try to set recordid. Bad things will happen. - * @param mixed $value The value to set the param to. Type depends on $paramName. - */ - private function setCommonParam($paramName, $value) { +class MyRadio_Album extends ServiceAPI +{ /** - * You won't believe how annoying psql can be about '' already being used on a unique key. + * The Title of the release. + * + * @var string */ - if ($value == '') $value = null; - //Maps Class variable names to their database values, if they mismatch. - $param_maps = ['albumid' => 'recordid']; - - if (!property_exists($this, $paramName)) - throw new MyRadioException('paramName invalid', 500); - - if ($this->$paramName == $value) return false; - - $this->$paramName = $value; - - if (isset($param_maps[$paramName])) - $paramName = $param_maps[$paramName]; - - self::$db->query('UPDATE public.rec_record SET ' . $paramName . '=$1 WHERE recordid=$2', array($value, $this->getID())); - - return true; - } - - public function setTitle($title) { - if (empty($title)) { - throw new MyRadioException('Title must not be empty.', 400); - } - - $this->setCommonParam('title', $title); - - return $this; - } - - /** - * Update the Artist for this Album - * @param String $artist The Artist name - * @param bool $applyToTracks If true, this will update the Artist for each individual Track in the Album. - * Default false. - * @return \MyRadio_Album - * @throws MyRadioException - */ - public function setArtist($artist, $applyToTracks = false) { - if (empty($artist)) { - throw new MyRadioException('Artist must not be empty.', 400); - } - - $this->setCommonParam('artist', $artist); - - if ($applyToTracks) { - foreach ($this->getTracks() as $track) { - $track->setArtist($artist); - } - } - - return $this; - } - - public static function findByName($title, $limit) { - $title = trim($title); - $result = self::$db->fetch_column('SELECT DISTINCT rec_record.recordid AS recordid FROM rec_record - WHERE rec_record.title ILIKE \'%\' || $1 || \'%\' LIMIT $2;', array($title, $limit)); - - $response = array(); - foreach ($result as $album) { - $response[] = MyRadio_Album::getInstance($album); - } - - return $response; - } - - /** - * - * @param Array $options One or more of the following: - * title: String title of the track - * artist: String artist name of the track - * digitised: If true, only return digitised tracks. If false, return any. - * limit: Maximum number of items to return. 0 = No Limit - * trackid: int Track id - * lastfmverified: Boolean whether or not verified with Last.fm Fingerprinter. Default any. - * random: If true, sort randomly - * idsort: If true, sort by trackid - * custom: A custom SQL WHERE clause - * precise: If true, will only return exact matches for artist/title - * nocorrectionproposed: If true, will only return items with no correction proposed. - * clean: Default any. 'y' for clean tracks, 'n' for dirty, 'u' for unknown. - * - */ - public static function findByOptions($options) { - self::wakeup(); - - if (empty($options['title'])) { - $options['title'] = ''; - } - if (empty($options['artist'])) { - $options['artist'] = ''; - } - if (empty($options['album'])) { - $options['album'] = ''; - } - if (!isset($options['digitised'])) { - $options['digitised'] = true; - } - if (empty($options['itonesplaylistid'])) { - $options['itonesplaylistid'] = null; - } - if (!isset($options['limit'])) { - $options['limit'] = Config::$ajax_limit_default; + private $title; + + private $artist; + + private $status; + + private $media; + + private $format; + + private $record_label; + + private $date_added; + + private $date_released; + + private $shelf_number; + + private $shelf_letter; + + private $albumid; + + private $member_add; + + private $member_edit; + + private $last_modified; + + private $cdid; + + private $location; + + private $label; + + private $tracks = []; + + protected function __construct($recordid) + { + $this->albumid = (int) $recordid; + + $result = self::$db->fetchOne( + 'SELECT * FROM (SELECT * FROM public.rec_record WHERE recordid=$1 LIMIT 1) AS t1 + LEFT JOIN public.rec_statuslookup ON t1.status = rec_statuslookup.status_code + LEFT JOIN public.rec_medialookup ON t1.media = rec_medialookup.media_code + LEFT JOIN public.rec_formatlookup ON t1.format = rec_formatlookup.format_code + LEFT JOIN public.rec_locationlookup ON t1.location = rec_locationlookup.location_code', + [$recordid] + ); + + if (empty($result)) { + throw new MyRadioException('The specified Record/Album does not seem to exist', 404); + + return; + } + + $this->title = $result['title']; + $this->artist = $result['artist']; + $this->status = $result['status_descr']; + $this->media = $result['media_descr']; + $this->format = $result['format_descr']; + $this->record_label = $result['recordlabel']; + $this->date_added = strtotime($result['dateadded']); + $this->date_released = strtotime($result['datereleased']); + $this->shelf_number = (int) $result['shelfnumber']; + $this->shelf_letter = $result['shelfletter']; + $this->member_add = empty($result['memberid_add']) ? null : (int) $result['memberid_add']; + $this->member_edit = empty($result['memberid_edit']) ? null : (int) $result['memberid_edit']; + $this->last_modified = strtotime($result['datetime_lastedit']); + $this->cdid = $result['cdid']; + $this->location = $result['location_descr']; + + $this->tracks = self::$db->fetchColumn('SELECT trackid FROM rec_track WHERE recordid=$1', [$this->albumid]); } - if (empty($options['trackid'])) { - $options['trackid'] = null; + + public function getID() + { + return $this->albumid; } - if (empty($options['lastfmverified'])) { - $options['lastfmverified'] = null; + + public function getTracks() + { + return MyRadio_Track::resultSetToObjArray($this->tracks); } - if (empty($options['random'])) { - $options['random'] = null; + + public function getTitle() + { + return $this->title; } - if (empty($options['idsort'])) { - $options['idsort'] = null; + + public function getArtist() + { + return $this->artist; } - if (empty($options['custom'])) { - $options['custom'] = null; + + public function getFolder() + { + $dir = Config::$music_central_db_path.'/records/'.$this->getID(); + if (!is_dir($dir)) { + if (!mkdir($dir, 0777, true)) { + throw new MyRadioException('Failed to create directory '.$dir, 500); + } + } + + return $dir; } - if (empty($options['precise'])) { - $options['precise'] = false; + + /** + * @param string $paramName The key to update, e.g. title. + * Don't be silly and try to set recordid. Bad things will happen. + * @param mixed $value The value to set the param to. Type depends on $paramName. + */ + private function setCommonParam($paramName, $value) + { + /* + * You won't believe how annoying psql can be about '' already being used on a unique key. + */ + if ($value == '') { + $value = null; + } + //Maps Class variable names to their database values, if they mismatch. + $param_maps = ['albumid' => 'recordid']; + + if (!property_exists($this, $paramName)) { + throw new MyRadioException('paramName invalid', 500); + } + + if ($this->$paramName == $value) { + return false; + } + + $this->$paramName = $value; + + if (isset($param_maps[$paramName])) { + $paramName = $param_maps[$paramName]; + } + + self::$db->query('UPDATE public.rec_record SET '.$paramName.'=$1 WHERE recordid=$2', [$value, $this->getID()]); + + return true; } - if (empty($options['nocorrectionproposed'])) { - $options['nocorrectionproposed'] = false; + + public function setTitle($title) + { + if (empty($title)) { + throw new MyRadioException('Title must not be empty.', 400); + } + + $this->setCommonParam('title', $title); + + return $this; } - if (empty($options['clean'])) { - $options['clean'] = false; + + /** + * Update the Artist for this Album. + * + * @param string $artist The Artist name + * @param bool $applyToTracks If true, this will update the Artist for each individual Track in the Album. + * Default false. + * + * @return \MyRadio_Album + * + * @throws MyRadioException + */ + public function setArtist($artist, $applyToTracks = false) + { + if (empty($artist)) { + throw new MyRadioException('Artist must not be empty.', 400); + } + + $this->setCommonParam('artist', $artist); + + if ($applyToTracks) { + foreach ($this->getTracks() as $track) { + $track->setArtist($artist); + } + } + + return $this; } - //Prepare paramaters - $sql_params = array($options['title'], $options['artist'], $options['album'], $options['precise'] ? '' : '%'); - $count = 4; - if ($options['limit'] != 0) { - $sql_params[] = $options['limit']; - $count++; - $limit_param = $count; + public static function findByName($title, $limit) + { + $title = trim($title); + $result = self::$db->fetchColumn( + 'SELECT DISTINCT rec_record.recordid AS recordid FROM rec_record + WHERE rec_record.title ILIKE \'%\' || $1 || \'%\' LIMIT $2;', + [$title, $limit] + ); + + $response = []; + foreach ($result as $album) { + $response[] = self::getInstance($album); + } + + return $response; } - if ($options['clean']) { - $sql_params[] = $options['clean']; - $count++; - $clean_param = $count; + + /** + * @param array $options One or more of the following: + * title: String title of the track + * artist: String artist name of the track + * digitised: If true, only return digitised tracks. If false, return any. + * limit: Maximum number of items to return. 0 = No Limit + * trackid: int Track id + * lastfmverified: Boolean whether or not verified with Last.fm Fingerprinter. Default any. + * random: If true, sort randomly + * idsort: If true, sort by trackid + * custom: A custom SQL WHERE clause + * precise: If true, will only return exact matches for artist/title + * nocorrectionproposed: If true, will only return items with no correction proposed. + * clean: Default any. 'y' for clean tracks, 'n' for dirty, 'u' for unknown. + */ + public static function findByOptions($options) + { + self::wakeup(); + + if (empty($options['title'])) { + $options['title'] = ''; + } + if (empty($options['artist'])) { + $options['artist'] = ''; + } + if (empty($options['album'])) { + $options['album'] = ''; + } + if (!isset($options['digitised'])) { + $options['digitised'] = true; + } + if (empty($options['itonesplaylistid'])) { + $options['itonesplaylistid'] = null; + } + if (!isset($options['limit'])) { + $options['limit'] = Config::$ajax_limit_default; + } + if (empty($options['trackid'])) { + $options['trackid'] = null; + } + if (empty($options['lastfmverified'])) { + $options['lastfmverified'] = null; + } + if (empty($options['random'])) { + $options['random'] = null; + } + if (empty($options['idsort'])) { + $options['idsort'] = null; + } + if (empty($options['custom'])) { + $options['custom'] = null; + } + if (empty($options['precise'])) { + $options['precise'] = false; + } + if (empty($options['nocorrectionproposed'])) { + $options['nocorrectionproposed'] = false; + } + if (empty($options['clean'])) { + $options['clean'] = false; + } + + //Prepare paramaters + $sql_params = [$options['title'], $options['artist'], $options['album'], $options['precise'] ? '' : '%']; + $count = 4; + if ($options['limit'] != 0) { + $sql_params[] = $options['limit']; + ++$count; + $limit_param = $count; + } + if ($options['clean']) { + $sql_params[] = $options['clean']; + ++$count; + $clean_param = $count; + } + + //Do the bulk of the sorting with SQL + $result = self::$db->fetchAll( + 'SELECT DISTINCT rec_record.recordid + FROM rec_record + INNER JOIN rec_track ON ( rec_record.recordid = rec_track.recordid ) + WHERE rec_track.title ILIKE $4 || $1 || $4 + AND rec_track.artist ILIKE $4 || $2 || $4 + AND rec_record.title ILIKE $4 || $3 || $4' + .($options['digitised'] ? ' AND digitised=\'t\'' : '') + .($options['lastfmverified'] === true ? ' AND lastfm_verified=\'t\'' : '') + .($options['lastfmverified'] === false ? ' AND lastfm_verified=\'f\'' : '') + .($options['nocorrectionproposed'] === true ? ' AND trackid NOT IN ( + SELECT trackid FROM public.rec_trackcorrection WHERE state=\'p\' + )' : '') + .($options['clean'] != null ? ' AND clean=$'.$clean_param : '') + .($options['custom'] !== null ? ' AND '.$options['custom'] : '') + .($options['random'] ? ' ORDER BY RANDOM()' : '') + .($options['idsort'] ? ' ORDER BY trackid' : '') + .($options['limit'] == 0 ? '' : ' LIMIT $'.$limit_param), + $sql_params + ); + + $response = []; + foreach ($result as $recordid) { + if ($options['trackid'] !== null && $recordid['trackid'] != $options['trackid']) { + continue; + } + $response[] = new self($recordid['recordid']); + } + + return $response; } - //Do the bulk of the sorting with SQL - $result = self::$db->fetch_all('SELECT DISTINCT rec_record.recordid - FROM rec_record - INNER JOIN rec_track ON ( rec_record.recordid = rec_track.recordid ) - WHERE rec_track.title ILIKE $4 || $1 || $4 - AND rec_track.artist ILIKE $4 || $2 || $4 - AND rec_record.title ILIKE $4 || $3 || $4 - ' . ($options['digitised'] ? ' AND digitised=\'t\'' : '') . ' - ' . ($options['lastfmverified'] === true ? ' AND lastfm_verified=\'t\'' : '') - . ($options['lastfmverified'] === false ? ' AND lastfm_verified=\'f\'' : '') - . ($options['nocorrectionproposed'] === true ? ' AND trackid NOT IN ( - SELECT trackid FROM public.rec_trackcorrection WHERE state=\'p\' - )' : '') - . ($options['clean'] != null ? ' AND clean=$'.$clean_param : '') - . ($options['custom'] !== null ? ' AND ' . $options['custom'] : '') - . ($options['random'] ? ' ORDER BY RANDOM()' : '') - . ($options['idsort'] ? ' ORDER BY trackid' : '') - . ($options['limit'] == 0 ? '' : ' LIMIT $'.$limit_param), $sql_params); - - $response = array(); - foreach ($result as $recordid) { - if ($options['trackid'] !== null && $recordid['trackid'] != $options['trackid']) { - continue; - } - $response[] = new MyRadio_Album($recordid['recordid']); + public static function findOrCreate($title, $artist) + { + $title = trim($title); + $artist = trim($artist); + + $result = self::$db->fetchOne( + 'SELECT recordid FROM rec_record WHERE title=$1 AND artist=$2 LIMIT 1', + [$title, $artist] + ); + + if (empty($result)) { + //Create Album + return self::create(['title' => $title, 'artist' => $artist]); + } else { + //Load Album + return self::getInstance($result['recordid']); + } } - return $response; - } - - public static function findOrCreate($title, $artist) { - $title = trim($title); - $artist = trim($artist); - - $result = self::$db->fetch_one('SELECT recordid FROM rec_record WHERE title=$1 AND artist=$2 LIMIT 1', - array($title, $artist)); - - if (empty($result)) { - //Create Album - return self::create(array('title' => $title, 'artist' => $artist)); - } else { - //Load Album - return self::getInstance($result['recordid']); + + public static function create($options) + { + if (empty($options['title']) or empty($options['artist'])) { + throw new MyRadioException('TITLE and ARTIST are required options to create an Album.', 400); + + return; + } + //Digitial Only + if (!isset($options['status'])) { + $options['status'] = 'd'; + } + //NIPSWeb Upload + if (!isset($options['media'])) { + $options['media'] = 'n'; + } + //Album + if (!isset($options['format'])) { + $options['format'] = 'a'; + } + //Blank + if (!isset($options['recordlabel'])) { + $options['recordlabel'] = ''; + } + //Shelf 0 + if (!isset($options['shelfnumber'])) { + $options['shelfnumber'] = 0; + } + //Shelf a + if (!isset($options['shelfletter'])) { + $options['shelfletter'] = 'a'; + } + //NULL CDID + if (!isset($options['cdid'])) { + $options['cdid'] = null; + } + //NULL location + if (!isset($options['location'])) { + $options['location'] = null; + } + //NULL promoter + if (!isset($options['promoterid'])) { + $options['promoterid'] = null; + } + + $q = 'INSERT INTO rec_record (title, artist, status, media, format, recordlabel, shelfnumber, + shelfletter, memberid_add, cdid, location, promoterid) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) + RETURNING recordid'; + $p = [ + trim($options['title']), + trim($options['artist']), + $options['status'], + $options['media'], + $options['format'], + $options['recordlabel'], + $options['shelfnumber'], + $options['shelfletter'], + $_SESSION['memberid'], + $options['cdid'], + $options['location'], + $options['promoterid'], + ]; + + $id = self::$db->fetchAll($q, $p); + + return self::getInstance($id[0]['recordid']); } - } - - public static function create($options) { - if (empty($options['title']) or empty($options['artist'])) { - throw new MyRadioException('TITLE and ARTIST are required options to create an Album.', 400); - return; + + public function toDataSource($mixins = []) + { + return [ + 'title' => $this->getTitle(), + 'recordid' => $this->getID(), + 'artist' => $this->artist, + 'cdid' => $this->cdid, + 'date_added' => CoreUtils::happyTime($this->date_added), + 'date_released' => CoreUtils::happyTime($this->date_released, false), + 'format' => $this->format, + 'last_modified' => CoreUtils::happyTime($this->last_modified), + 'location' => $this->location, + 'media' => $this->media, + 'member_add' => $this->member_add, + 'member_edit' => $this->member_edit, + 'record_label' => $this->record_label, + 'shelf_letter' => $this->shelf_letter, + 'shelf_number' => $this->shelf_number, + 'status' => $this->status, + 'label' => $this->record_label, + ]; } - //Digitial Only - if (!isset($options['status'])) $options['status'] = 'd'; - //NIPSWeb Upload - if (!isset($options['media'])) $options['media'] = 'n'; - //Album - if (!isset($options['format'])) $options['format'] = 'a'; - //Blank - if (!isset($options['recordlabel'])) $options['recordlabel'] = ''; - //Shelf 0 - if (!isset($options['shelfnumber'])) $options['shelfnumber'] = 0; - //Shelf a - if (!isset($options['shelfletter'])) $options['shelfletter'] = 'a'; - //NULL CDID - if (!isset($options['cdid'])) $options['cdid'] = null; - //NULL location - if (!isset($options['location'])) $options['location'] = null; - //NULL promoter - if (!isset($options['promoterid'])) $options['promoterid'] = null; - - $r = self::$db->query('INSERT INTO rec_record (title, artist, status, media, format, recordlabel, shelfnumber, - shelfletter, memberid_add, cdid, location, promoterid) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) - RETURNING recordid', array( - trim($options['title']), - trim($options['artist']), - $options['status'], - $options['media'], - $options['format'], - $options['recordlabel'], - $options['shelfnumber'], - $options['shelfletter'], - $_SESSION['memberid'], - $options['cdid'], - $options['location'], - $options['promoterid'] - )); - - $id = self::$db->fetch_all($r); - - return self::getInstance($id[0]['recordid']); - } - - public function toDataSource() { - return array( - 'title' => $this->getTitle(), - 'recordid' => $this->getID(), - 'artist' => $this->artist, - 'cdid' => $this->cdid, - 'date_added' => CoreUtils::happyTime($this->date_added), - 'date_released' => CoreUtils::happyTime($this->date_released, false), - 'format' => $this->format, - 'last_modified' => CoreUtils::happyTime($this->last_modified), - 'location' => $this->location, - 'media' => $this->media, - 'member_add' => $this->member_add, - 'member_edit' => $this->member_edit, - 'record_label' => $this->record_label, - 'shelf_letter' => $this->shelf_letter, - 'shelf_number' => $this->shelf_number, - 'status' => $this->status, - 'label' => $this->record_label - ); - } - -} \ No newline at end of file +} diff --git a/src/Classes/ServiceAPI/MyRadio_Alias.php b/src/Classes/ServiceAPI/MyRadio_Alias.php index 86284a042..e36ff3121 100644 --- a/src/Classes/ServiceAPI/MyRadio_Alias.php +++ b/src/Classes/ServiceAPI/MyRadio_Alias.php @@ -1,142 +1,164 @@ - * @package MyRadio_Mail - * @uses \Database - * + * + * @uses \Database */ -class MyRadio_Alias extends ServiceAPI { - /** - * The ID of the Alias - * @var int - */ - private $alias_id; - /** - * The source of the alias - * If this is an alias from foo@ury.org.uk to bar@ury.org.uk, this value is - * 'foo' - * @var String - */ - private $source; - /** - * An array of Lists, Users, Officers and text destinations for the Alias. - * - * Format:
    - * {{type: 'text', value: 'dave.tracz'}, ...} - * - * @var mixed[] - */ - private $destinations = array(); +class MyRadio_Alias extends ServiceAPI +{ + /** + * The ID of the Alias. + * + * @var int + */ + private $alias_id; - - protected function __construct($id) { - $result = self::$db->fetch_one(' SELECT source, ' - . '(SELECT array(SELECT destination FROM mail.alias_text ' - . ' WHERE alias_id=$1)) AS dtext, ' - . '(SELECT array(SELECT destination FROM mail.alias_officer ' - . ' WHERE alias_id=$1)) AS dofficer, ' - . '(SELECT array(SELECT destination FROM mail.alias_member ' - . ' WHERE alias_id=$1)) AS dmember, ' - . '(SELECT array(SELECT destination FROM mail.alias_list ' - . ' WHERE alias_id=$1)) AS dlist ' - . 'FROM mail.alias WHERE alias_id=$1', - [$id]); - if (empty($result)) { - throw new MyRadioException('Alias '.$id.' does not exist!', 404); - } else { - $this->alias_id = (int)$id; - $this->source = $result['source']; - - foreach (self::$db->decodeArray($result['dtext']) as $text) { - $this->destinations[] = [ - 'type' => 'text', - 'value' => $text - ]; - } - - foreach (self::$db->decodeArray($result['dofficer']) as $officer) { - $this->destinations[] = [ - 'type' => 'officer', - 'value' => MyRadio_Officer::getInstance($officer) - ]; - } - - foreach (self::$db->decodeArray($result['dmember']) as $member) { - $this->destinations[] = [ - 'type' => 'member', - 'value' => MyRadio_User::getInstance($member) - ]; - } - - foreach (self::$db->decodeArray($result['dlist']) as $list) { - $this->destinations[] = [ - 'type' => 'list', - 'value' => MyRadio_List::getInstance($list) - ]; - } + /** + * The source of the alias + * If this is an alias from foo@ury.org.uk to bar@ury.org.uk, this value is + * 'foo'. + * + * @var string + */ + private $source; + + /** + * An array of Lists, Users, Officers and text destinations for the Alias. + * + * Format:
    + * {{type: 'text', value: 'dave.tracz'}, ...} + * + * @var mixed[] + */ + private $destinations = []; + + protected function __construct($id) + { + $result = self::$db->fetchOne( + 'SELECT source, ( + SELECT array_to_json(array( + SELECT destination FROM mail.alias_text WHERE alias_id=$1 + )) + ) AS dtext, ( + SELECT array_to_json(array( + SELECT destination FROM mail.alias_officer WHERE alias_id=$1 + )) + ) AS dofficer, ( + SELECT array_to_json(array( + SELECT destination FROM mail.alias_member WHERE alias_id=$1 + )) + ) AS dmember, ( + SELECT array_to_json(array( + SELECT destination FROM mail.alias_list WHERE alias_id=$1 + )) + ) AS dlist + FROM mail.alias WHERE alias_id=$1', + [$id] + ); + if (empty($result)) { + throw new MyRadioException('Alias '.$id.' does not exist!', 404); + } else { + $this->alias_id = (int) $id; + $this->source = $result['source']; + + foreach (json_decode($result['dtext']) as $text) { + $this->destinations[] = [ + 'type' => 'text', + 'value' => $text, + ]; + } + + foreach (json_decode($result['dofficer']) as $officer) { + $this->destinations[] = [ + 'type' => 'officer', + 'value' => MyRadio_Officer::getInstance($officer), + ]; + } + + foreach (json_decode($result['dmember']) as $member) { + $this->destinations[] = [ + 'type' => 'member', + 'value' => MyRadio_User::getInstance($member), + ]; + } + + foreach (json_decode($result['dlist']) as $list) { + $this->destinations[] = [ + 'type' => 'list', + 'value' => MyRadio_List::getInstance($list), + ]; + } + } } - } - - /** - * Returns all the Aliases available. - * @return array - */ - public static function getAllAliases() { - return self::resultSetToObjArray(self::$db->fetch_column( - 'SELECT alias_id FROM mail.alias')); - } - - /** - * Get the ID fo this Alias - * @return int - */ - public function getID() { - return $this->alias_id; - } - - /** - * Returns the string prefix of the Alias. - * - * @return String - */ - public function getSource() { - return $this->source; - } - - /** - * Returns what the Alias maps to. - * - * Format:
    - * {{type: 'text', value: 'dave.tracz'}, ...} - * - * @return mixed[] - */ - public function getDestinations() { - return $this->destinations; - } - - /** - * Returns data about the Alias for the API. - * - * @param bool $full - * @return Array - */ - public function toDataSource($full = true) { - $data = [ - 'alias_id' => $this->getID(), - 'source' => $this->getSource(), - 'destinations' => CoreUtils::dataSourceParser($this->getDestinations(), false) - ]; - - return $data; - } + /** + * Returns all the Aliases available. + * + * @return array + */ + public static function getAllAliases() + { + return self::resultSetToObjArray( + self::$db->fetchColumn( + 'SELECT alias_id FROM mail.alias' + ) + ); + } + + /** + * Get the ID fo this Alias. + * + * @return int + */ + public function getID() + { + return $this->alias_id; + } + + /** + * Returns the string prefix of the Alias. + * + * @return string + */ + public function getSource() + { + return $this->source; + } + + /** + * Returns what the Alias maps to. + * + * Format:
    + * {{type: 'text', value: string/Officer/Member}, ...} + * + * @return mixed[] + */ + public function getDestinations() + { + return $this->destinations; + } + + /** + * Returns data about the Alias for the API. + * @param array $mixins + * @return array + */ + public function toDataSource($mixins = []) + { + $data = [ + 'alias_id' => $this->getID(), + 'source' => $this->getSource(), + 'destinations' => CoreUtils::dataSourceParser($this->getDestinations()), + ]; + + return $data; + } } diff --git a/src/Classes/ServiceAPI/MyRadio_Artist.php b/src/Classes/ServiceAPI/MyRadio_Artist.php new file mode 100644 index 000000000..2471e87f8 --- /dev/null +++ b/src/Classes/ServiceAPI/MyRadio_Artist.php @@ -0,0 +1,166 @@ +artistid = (int) $artistid; + throw new MyRadioException('Not implemented Artist::__construct'); + } + + /** + * Returns an Array of Artists matching the given partial name. + * + * @param string $title A partial or total title to search for + * @param int $limit The maximum number of tracks to return + * + * @return array 2D with each first dimension an Array as follows:
    + * title: The name of the artist
    + * artistid: Always 0 until Artist support is implemented + */ + public static function findByName($title, $limit) + { + $title = trim($title); + + return self::$db->fetchAll( + 'SELECT title, artistid FROM ( + SELECT DISTINCT title, artistid, priority FROM + ( + ( + SELECT rec_track.artist AS title, 0 AS artistid, 1 AS priority + FROM rec_track WHERE rec_track.artist=$1 + ) UNION ( + SELECT rec_track.artist AS title, 0 AS artistid, 2 AS priority + FROM rec_track WHERE rec_track.artist ILIKE $1 || \'%\' + ) UNION ( + SELECT rec_track.artist AS title, 0 AS artistid, 3 AS priority + FROM rec_track WHERE rec_track.artist ILIKE \'%\' || $1 || \'%\' + ) + ) AS t1 + ) As t2 + ORDER BY priority LIMIT $2', + [$title, $limit] + ); + } + + /** + * @param array $options One or more of the following: + * title: String title of the track + * artist: String artist name of the track + * digitised: If true, only return digitised tracks. If false, return any. + * limit: Maximum number of items to return. 0 = No Limit + * trackid: int Track id + * lastfmverified: Boolean whether or not verified with Last.fm Fingerprinter. Default any. + * random: If true, sort randomly + * idsort: If true, sort by trackid + * custom: A custom SQL WHERE clause + * precise: If true, will only return exact matches for artist/title + * nocorrectionproposed: If true, will only return items with no correction proposed. + * clean: Default any. 'y' for clean tracks, 'n' for dirty, 'u' for unknown. + */ + public static function findByOptions($options) + { + self::wakeup(); + + if (empty($options['title'])) { + $options['title'] = ''; + } + if (empty($options['artist'])) { + $options['artist'] = ''; + } + if (empty($options['album'])) { + $options['album'] = ''; + } + if (!isset($options['digitised'])) { + $options['digitised'] = true; + } + if (empty($options['itonesplaylistid'])) { + $options['itonesplaylistid'] = null; + } + if (!isset($options['limit'])) { + $options['limit'] = Config::$ajax_limit_default; + } + if (empty($options['trackid'])) { + $options['trackid'] = null; + } + if (empty($options['lastfmverified'])) { + $options['lastfmverified'] = null; + } + if (empty($options['random'])) { + $options['random'] = null; + } + if (empty($options['idsort'])) { + $options['idsort'] = null; + } + if (empty($options['custom'])) { + $options['custom'] = null; + } + if (empty($options['precise'])) { + $options['precise'] = false; + } + if (empty($options['nocorrectionproposed'])) { + $options['nocorrectionproposed'] = false; + } + if (empty($options['clean'])) { + $options['clean'] = false; + } + + //Prepare paramaters + $sql_params = [$options['title'], $options['artist'], $options['album'], $options['precise'] ? '' : '%']; + $count = 4; + if ($options['limit'] != 0) { + $sql_params[] = $options['limit']; + ++$count; + $limit_param = $count; + } + if ($options['clean']) { + $sql_params[] = $options['clean']; + ++$count; + $clean_param = $count; + } + + //Do the bulk of the sorting with SQL + $result = self::$db->fetchAll( + 'SELECT DISTINCT rec_track.artist + FROM rec_track + INNER JOIN rec_record ON ( rec_track.recordid = rec_record.recordid ) + WHERE rec_track.title ILIKE $4 || $1 || $4 + AND rec_track.artist ILIKE $4 || $2 || $4 + AND rec_record.title ILIKE $4 || $3 || $4' + .($options['digitised'] ? ' AND digitised=\'t\'' : '') + .($options['lastfmverified'] === true ? ' AND lastfm_verified=\'t\'' : '') + .($options['lastfmverified'] === false ? ' AND lastfm_verified=\'f\'' : '') + .($options['nocorrectionproposed'] === true ? ' AND trackid NOT IN ( + SELECT trackid FROM public.rec_trackcorrection WHERE state=\'p\' + )' : '') + .($options['clean'] != null ? ' AND clean=$'.$clean_param : '') + .($options['custom'] !== null ? ' AND '.$options['custom'] : '') + .($options['random'] ? ' ORDER BY RANDOM()' : '') + .($options['idsort'] ? ' ORDER BY trackid' : '') + .($options['limit'] == 0 ? '' : ' LIMIT $'.$limit_param), + $sql_params + ); + + return $result; + } +} diff --git a/src/Classes/ServiceAPI/MyRadio_AutoVizClip.php b/src/Classes/ServiceAPI/MyRadio_AutoVizClip.php new file mode 100644 index 000000000..cf352222f --- /dev/null +++ b/src/Classes/ServiceAPI/MyRadio_AutoVizClip.php @@ -0,0 +1,111 @@ +type; + } + + public function getStartTime(): int + { + return $this->start_time; + } + + public function getEndTime(): int + { + return $this->end_time; + } + + public function getTimeslotID(): int + { + return $this->timeslot_id; + } + + public function getPublicURL(): string + { + return Config::$autoviz_public_clips_base . '/' . $this->timeslot_id . '/' . $this->filename; + } + + /** + * @return {AutoVizClip[]} + */ + public static function getClipsForTimeslot(int $timeslot_id): array + { + if (!is_int($timeslot_id)) { + // Path traversal would be very bad indeed! + throw new MyRadioException('Timeslot ID must be an int!'); + } + $path = Config::$autoviz_clips_path . '/' . $timeslot_id; + if (!is_dir($path)) { + return []; + } + + $paths = scandir($path); + if ($paths === false) { + throw new MyRadioException("Failed to list clips for timeslot $timeslot_id"); + } + $result = []; + foreach ($paths as $clipPath) { + if ($clipPath[0] === '.') { + continue; + } + if (str_ends_with($clipPath, '.mkv')) { + // OBS complete show recording, ignore it + continue; + } + $clip = new self(); + $parts = explode('-', $clipPath, 3); + switch ($parts[0]) { + case 'full_show': + case 'clip': + $clip->type = $parts[0]; + break; + default: + throw new MyRadioException("Unrecognised clip type for file $clipPath"); + } + $clip->start_time = intval($parts[1]); + $clip->end_time = intval($parts[2]); + $clip->timeslot_id = $timeslot_id; + $clip->filename = $clipPath; + $result[] = $clip; + } + return $result; + } + + public function toDataSource($mixins = []) + { + return [ + 'type' => $this->type, + 'startTime' => CoreUtils::happyTime($this->start_time), + 'endTime' => CoreUtils::happyTime($this->end_time), + 'timeslot' => MyRadio_Timeslot::getInstance($this->timeslot_id)->toDataSource($mixins) + ]; + } +} diff --git a/src/Classes/ServiceAPI/MyRadio_AutoVizConfiguration.php b/src/Classes/ServiceAPI/MyRadio_AutoVizConfiguration.php new file mode 100644 index 000000000..87b243e46 --- /dev/null +++ b/src/Classes/ServiceAPI/MyRadio_AutoVizConfiguration.php @@ -0,0 +1,161 @@ +autoviz_config_id = (int) $data['autoviz_config_id']; + $this->show_season_timeslot_id = (int) $data['show_season_timeslot_id']; + $this->record = $data['record']; + $this->stream_url = $data['stream_url']; + $this->stream_key = $data['stream_key']; + } + + protected static function factory($itemid) + { + $data = self::$db->fetchOne( + 'SELECT * FROM schedule.autoviz_configuration + WHERE autoviz_config_id = $1', + [$itemid] + ); + if (empty($data)) { + throw new MyRadioException("AutoViz Configuration $itemid not found", 404); + } + return new self($data); + } + + + public static function getConfigForTimeslot($timeslot_id): ?MyRadio_AutoVizConfiguration + { + $data = self::$db->fetchOne( + 'SELECT * FROM schedule.autoviz_configuration + WHERE show_season_timeslot_id = $1 + LIMIT 1', + [$timeslot_id] + ); + if (empty($data)) { + return null; + } + return new self($data); + } + + /** + * @return int + */ + public function getID(): int + { + return $this->autoviz_config_id; + } + + public function getTimeslot(): MyRadio_Timeslot + { + return MyRadio_Timeslot::getInstance($this->show_season_timeslot_id); + } + + /** + * @return bool + */ + public function getRecord(): bool + { + return $this->record; + } + + /** + * @return string|null + */ + public function getStreamUrl() + { + return $this->stream_url; + } + + /** + * @return string|null + */ + public function getStreamKey() + { + return $this->stream_key; + } + + public static function create(int $timeslotID, bool $record, ?string $streamURL, ?string $streamKey): MyRadio_AutoVizConfiguration + { + if (($streamURL !== null && $streamKey === null) || ($streamURL === null && $streamKey !== null)) { + throw new MyRadioException('Must specify both stream URL and key', 400); + } + $result = self::$db->fetchColumn( + 'INSERT INTO schedule.autoviz_configuration (show_season_timeslot_id, record, stream_url, stream_key) + VALUES ($1, $2, $3, $4) + RETURNING autoviz_config_id', + [$timeslotID, $record, $streamURL, $streamKey] + ); + return self::getInstance((int)$result[0]); + } + + public function update(bool $record, ?string $streamURL, ?string $streamKey) + { + if (($streamURL !== null && $streamKey === null) || ($streamURL === null && $streamKey !== null)) { + throw new MyRadioException('Must specify both stream URL and key', 400); + } + if (!$record && $streamURL === null && $streamKey === null) { + // Just delete the config + self::$db->query('DELETE FROM schedule.autoviz_configuration WHERE autoviz_config_id = $1', [$this->autoviz_config_id]); + self::$cache->delete(self::getCacheKey($this->getID())); + } else { + self::$db->query( + 'UPDATE schedule.autoviz_configuration + SET record = $2, stream_url = $3, stream_key = $4 + WHERE autoviz_config_id = $1', + [$this->autoviz_config_id, $record, $streamURL, $streamKey] + ); + $this->updateCacheObject(); + } + } + + public function toDataSource($mixins = []) + { + return [ + 'autoviz_config_id' => $this->autoviz_config_id, + 'timeslot' => $this->getTimeslot()->toDataSource($mixins), + 'record' => $this->record, + 'stream_url' => $this->stream_url, + 'stream_key' => $this->stream_key, + ]; + } + + /** + * Returns an array representing this configuration in the format expected by the autoviz software. + * @return array + */ + public function toTask(): array + { + $ts = $this->getTimeslot(); + $task = [ + 'name' => $ts->getMeta('title') . ' - ' . CoreUtils::happyTime($ts->getStartTime()), + 'timeslotid' => $ts->getID(), + 'startTime' => CoreUtils::getIso8601Timestamp($ts->getStartTime()), + 'endTime' => CoreUtils::getIso8601Timestamp($ts->getEndTime()), + 'record' => $this->record + ]; + if (!empty($this->stream_url) && !empty($this->stream_key)) { + $task['stream'] = [ + 'url' => $this->stream_url, + 'key' => $this->stream_key + ]; + } else { + $task['stream'] = false; + } + return $task; + } +} diff --git a/src/Classes/ServiceAPI/MyRadio_Banner.php b/src/Classes/ServiceAPI/MyRadio_Banner.php index 0c9632947..99a0c527e 100644 --- a/src/Classes/ServiceAPI/MyRadio_Banner.php +++ b/src/Classes/ServiceAPI/MyRadio_Banner.php @@ -1,279 +1,402 @@ - * @package MyRadio_Website - * @uses \Database + * The Banner class stores and manages information about a Banner on the front website. + * + * @uses \Database */ -class MyRadio_Banner extends MyRadio_Photo { - - /** - * The ID of the banner - * @var int - */ - private $banner_id; - - /** - * A short text description of the banner - * @var String - */ - private $alt; - - /** - * URL target of the banner. Activated when banner is clicked. - * @var String - */ - private $target; - - /** - * Banner type. No idea what this is, and there's only one type. - * @var int - */ - private $type = 2; - - /** - * IDs of Campaigns that use this Banner - * @var int[] - */ - private $campaigns = array(); - - /** - * Initiates the MyRadio_Banner object - * @param int $banner_id The ID of the Banner to initialise - */ - protected function __construct($banner_id) { - $this->banner_id = (int) $banner_id; - - $result = self::$db->fetch_one('SELECT * FROM website.banner WHERE banner_id=$1', array($banner_id)); - if (empty($result)) { - throw new MyRadioException('Banner ' . $banner_id . ' does not exist!'); +class MyRadio_Banner extends MyRadio_Photo +{ + /** + * The ID of the banner. + * + * @var int + */ + private $banner_id; + + /** + * A short text description of the banner. + * + * @var string + */ + private $alt; + + /** + * URL target of the banner. Activated when banner is clicked. + * + * @var string + */ + private $target; + + /** + * Banner type. No idea what this is, and there's only one type. + * + * @var int + */ + private $type = 2; + + /** + * IDs of Campaigns that use this Banner. + * + * @var int[] + */ + private $campaigns = []; + + /** + * Initiates the MyRadio_Banner object. + * + * @param int $banner_id The ID of the Banner to initialise + */ + protected function __construct($banner_id) + { + $this->banner_id = (int) $banner_id; + + $result = self::$db->fetchOne('SELECT * FROM website.banner WHERE banner_id=$1', [$banner_id]); + if (empty($result)) { + throw new MyRadioException('Banner '.$banner_id.' does not exist!'); + } + + $this->alt = $result['alt']; + $this->target = $result['target']; + $this->type = (int) $result['banner_type_id']; + + if (is_numeric($result['photoid'])) { + parent::__construct($result['photoid']); + } else { + parent::__construct(Config::$photo_joined); + } + + $this->campaigns = self::$db->fetchColumn( + 'SELECT banner_campaign_id FROM website.banner_campaign + WHERE banner_id=$1', + [$this->banner_id] + ); + } + + public function toDataSource($mixins = []) + { + $data = [ + 'banner_id' => $this->getBannerID(), + 'alt' => $this->getAlt(), + 'target' => $this->getTarget(), + 'num_campaigns' => sizeof($this->getCampaigns()), + 'is_active' => $this->isActive(), + 'edit_link' => [ + 'display' => 'icon', + 'value' => 'pencil', + 'title' => 'Click here to edit this Banner', + 'url' => URLUtils::makeURL('Website', 'editBanner', ['bannerid' => $this->getBannerID()]), + ], + 'campaigns_link' => [ + 'display' => 'icon', + 'value' => 'calendar', + 'title' => 'Click here to view the Campaigns for this Banner', + 'url' => URLUtils::makeURL('Website', 'campaigns', ['bannerid' => $this->getBannerID()]), + ], + ]; + + return array_merge(parent::toDataSource($mixins), $data); + } + + /** + * Get the ID of the Banner. + * + * @return int + */ + public function getBannerID() + { + return $this->banner_id; + } + + /** + * Get a description of the Banner. + * + * @return string + */ + public function getAlt() + { + return $this->alt; + } + + /** + * Get the link action of the Banner. + * + * @return string + */ + public function getTarget() + { + return $this->target; + } + + /** + * Get the type of the Banner. + * + * @return int + */ + public function getType() + { + return $this->type; + } + + /** + * Get all the campaigns that this Banner has. + * + * @return MyRadio_BannerCampaign[] + */ + public function getCampaigns() + { + return MyRadio_BannerCampaign::resultSetToObjArray($this->campaigns); } - $this->alt = $result['alt']; - $this->target = $result['target']; - $this->type = (int) $result['banner_type_id']; + /** + * Get if any Campaigns linked to this Banner are active. + * + * @return bool + */ + public function isActive() + { + foreach ($this->getCampaigns() as $campaign) { + if ($campaign->isActive()) { + return true; + } + } - if (is_numeric($result['photoid'])) { - parent::__construct($result['photoid']); - } else { - parent::__construct(Config::$photo_joined); + return false; } - $this->campaigns = self::$db->fetch_column('SELECT banner_campaign_id FROM website.banner_campaign - WHERE banner_id=$1', [$this->banner_id]); - } - - public function toDataSource() { - $data = [ - 'banner_id' => $this->getBannerID(), - 'alt' => $this->getAlt(), - 'target' => $this->getTarget(), - 'num_campaigns' => sizeof($this->getCampaigns()), - 'is_active' => $this->isActive(), - 'edit_link' => [ - 'display' => 'icon', - 'value' => 'pencil', - 'title' => 'Click here to edit this Banner', - 'url' => CoreUtils::makeURL('Website', 'editBanner', array('bannerid' => $this->getBannerID())) - ], - 'campaigns_link' => [ - 'display' => 'icon', - 'value' => 'calendar', - 'title' => 'Click here to view the Campaigns for this Banner', - 'url' => CoreUtils::makeURL('Website', 'campaigns', array('bannerid' => $this->getBannerID())) - ] - ]; - - return array_merge(parent::toDataSource(), $data); - } - - /** - * Get the ID of the Banner - * @return int - */ - public function getBannerID() { - return $this->banner_id; - } - - /** - * Get a description of the Banner - * @return String - */ - public function getAlt() { - return $this->alt; - } - - /** - * Get the link action of the Banner - * @return String - */ - public function getTarget() { - return $this->target; - } - - /** - * Get the type of the Banner - * @return int - */ - public function getType() { - return $this->type; - } - - /** - * Get all the campaigns that this Banner has - * @return MyRadio_BannerCampaign[] - */ - public function getCampaigns() { - return MyRadio_BannerCampaign::resultSetToObjArray($this->campaigns); - } - - /** - * Get if any Campaigns linked to this Banner are active - * @return boolean - */ - public function isActive() { - foreach ($this->getCampaigns() as $campaign) { - if ($campaign->isActive()) { - return true; - } + public function getEditForm() + { + return self::getForm() + ->editMode( + $this->getBannerID(), + [ + 'alt' => $this->getAlt(), + 'target' => $this->getTarget(), + 'type' => $this->getType(), + ] + ); } - return false; - } - - public function getEditForm() { - return self::getBannerForm() - ->editMode($this->getBannerID(), - [ - 'alt' => $this->getAlt(), - 'target' => $this->getTarget(), - 'type' => $this->getType() - ], 'doEditBanner'); - } - - /** - * Set the Alt text - * @param String $alt - * @return \MyRadio_Banner - * @throws MyRadioException - */ - public function setAlt($alt) { - if (empty($alt)) { - throw new MyRadioException('Banner Alt cannot be empty!', 400); + /** + * Set the Alt text. + * + * @param string $alt + * + * @return \MyRadio_Banner + * + * @throws MyRadioException + */ + public function setAlt($alt) + { + if (empty($alt)) { + throw new MyRadioException('Banner Alt cannot be empty!', 400); + } + $this->alt = $alt; + self::$db->query('UPDATE website.banner SET alt=$1 WHERE banner_id=$2', [$alt, $this->getBannerID()]); + + return $this; + } + + /** + * Set the Target URL. + * + * @param string $target + * + * @return \MyRadio_Banner + */ + public function setTarget($target) + { + $this->target = $target; + self::$db->query('UPDATE website.banner SET target=$1 WHERE banner_id=$2', [$target, $this->getBannerID()]); + + return $this; + } + + /** + * Set the Banner Type. + * + * @param int $type + * + * @return \MyRadio_Banner + * + * @throws MyRadioException + */ + public function setType($type) + { + if (empty($type) or !is_int($type)) { + throw new MyRadioException('Banner Type must be a number!', 400); + } + + $this->type = $type; + self::$db->query( + 'UPDATE website.banner SET banner_type_id=$1 WHERE banner_id=$2', + [$type, $this->getBannerID()] + ); + return $this; + } + + /** + * Set the Banner Photo. + * + * @param MyRadio_Photo $photo + * + * @return \MyRadio_Banner + */ + public function setPhoto(MyRadio_Photo $photo) + { + parent::__construct($photo->getID()); + self::$db->query( + 'UPDATE website.banner SET image=$1, photoid=$2 WHERE banner_id=$3', + [str_replace(Config::$public_media_uri.'/', '', $photo->getURL()), $this->getID(), $this->getBannerID()] + ); + + return $this; } - $this->alt = $alt; - self::$db->query('UPDATE website.banner SET alt=$1 WHERE banner_id=$2', [$alt, $this->getBannerID()]); - - return $this; - } - - /** - * Set the Target URL - * @param String $target - * @return \MyRadio_Banner - */ - public function setTarget($target) { - $this->target = $target; - self::$db->query('UPDATE website.banner SET target=$1 WHERE banner_id=$2', [$target, $this->getBannerID()]); - - return $this; - } - - /** - * Set the Banner Type - * @param int $type - * @return \MyRadio_Banner - * @throws MyRadioException - */ - public function setType($type) { - if (empty($type) or !is_int($type)) { - throw new MyRadioException('Banner Type must be a number!', 400); + + /** + * Creates a banner. + * + * @param MyRadio_Photo $photo The Photo this banner will use. Must be 640x212px. + * @param string $alt Friendly name. Used on backend and as 'alt' text. + * @param string $target URL clicking the banner takes you to. Should be absolute. + * @param int $type The type of banner. Currently, there's only one type, intuitively called 2. + * + * @return MyRadio_Banner The new Banner, of course! + * + * @throws MyRadioException + */ + public static function create($photo, $alt = 'Unnamed Banner', $target = null, $type = 2) + { + $result = self::$db->fetchColumn( + 'INSERT INTO website.banner (alt, image, target, banner_type_id, photoid) + VALUES ($1, $2, $3, $4, $5) RETURNING banner_id', + [$alt, $photo->getURL(), $target, $type, $photo->getID()] + ); + + return self::getInstance($result[0]); } - - $this->type = $type; - self::$db->query('UPDATE website.banner SET banner_type_id=$1 WHERE banner_id=$2', [$type, $this->getBannerID()]); - - return $this; - } - - /** - * Set the Banner Photo - * @param MyRadio_Photo $photo - * @return \MyRadio_Banner - */ - public function setPhoto(MyRadio_Photo $photo) { - parent::__construct($photo->getID()); - self::$db->query('UPDATE website.banner SET image=$1, photoid=$2 WHERE banner_id=$2', - [str_replace(Config::$public_media_uri.'/','',$photo->getURL()), $this->getID(), $this->getBannerID()]); - - return $this; - } - - /** - * Creates a banner - * @param MyRadio_Photo $photo The Photo this banner will use. Must be 640x212px. - * @param String $alt Friendly name. Used on backend and as 'alt' text. - * @param String $target URL clicking the banner takes you to. Should be absolute. - * @param int $type The type of banner. Currently, there's only one type, intuitively called 2. - * @return MyRadio_Banner The new Banner, of course! - * @throws MyRadioException - */ - public static function create(MyRadio_Photo $photo, $alt = 'Unnamed Banner', $target = null, $type = 2) { - $result = self::$db->fetch_column('INSERT INTO website.banner (alt, image, target, banner_type_id, photoid) - VALUES ($1, $2, $3, $4, $5) RETURNING banner_id', array($alt, $photo->getURL(), $target, $type, $photo->getID())); - - return self::getInstance($result[0]); - } - - /** - * Get ALL the Banners - * @return MyRadio_Banner[] - */ - public static function getAllBanners() { - return self::resultSetToObjArray(self::$db->fetch_column('SELECT banner_id FROM website.banner')); - } - - public static function getBannerTypes() { - return self::$db->fetch_all('SELECT banner_type_id, description FROM website.banner_type'); - } - - /** - * Generates the form used to Create and Edit Banners - * @return MyRadio_Form - */ - public static function getBannerForm() { - return (new MyRadioForm('bannerfrm', 'Website', 'doCreateBanner', [ - 'title' => 'Edit Banner', - 'template' => 'Website/bannerfrm.twig' - ])) - ->addField(new MyRadioFormField('alt', MyRadioFormField::TYPE_TEXT, [ - 'label' => 'Title', - 'explanation' => 'This is used on the backpages to identify the Banner, and also on the main website as mouseover text.' - ])) - ->addField(new MyRadioFormField('target', MyRadioFormField::TYPE_TEXT, [ - 'label' => 'Action', - 'explanation' => 'This is the URL that the User will be taken to if they click the Banner. You can leave this blank for there to not be a link.', - 'required' => false - ])) - ->addField(new MyRadioFormField('type', MyRadioFormField::TYPE_SELECT, [ - 'label' => 'Type', - 'explanation' => 'TODO: Ask Matt what this is even supposed to do.', - 'options' => array_map(function($x) { - return ['value' => $x['banner_type_id'], 'text' => $x['description']]; - }, self::getBannerTypes()) - ])) - ->addField(new MyRadioFormField('photo', MyRadioFormField::TYPE_FILE, [ - 'label' => 'Image', - 'explanation' => 'Please upload a 640x212px image file to use as the Banner.' - ])); - } + /** + * Get ALL the Banners. + * + * @return MyRadio_Banner[] + */ + public static function getAllBanners() + { + return self::resultSetToObjArray(self::$db->fetchColumn('SELECT banner_id FROM website.banner')); + } + + /** + * Gets all Banners that are currently active. That is, they have started and have not expired. + * It returns them even when there isn't currently a Banner Timeslot for the Campaign running. + * + * @return MyRadio_Banner[] + */ + public static function getActiveBanners() + { + $result = []; + foreach (MyRadio_BannerCampaign::getActiveBannerCampaigns() as $campaign) { + $result[] = $campaign->getBanner(); + } + + return $result; + } + + /** + * Gets all Banners that are currently live. That is they are active and have timeslots at the current time. + * + * @return MyRadio_Banner[] + */ + public static function getLiveBanners() + { + $result = []; + foreach (MyRadio_BannerCampaign::getLiveBannerCampaigns() as $campaign) { + $result[] = $campaign->getBanner(); + } + + return $result; + } + + public static function getBannerTypes() + { + return self::$db->fetchAll('SELECT banner_type_id, description FROM website.banner_type'); + } + + /** + * Generates the form used to Create and Edit Banners. + * + * @return MyRadio_Form + */ + public static function getForm() + { + return (new MyRadioForm( + 'bannerfrm', + 'Website', + 'editBanner', + [ + 'title' => 'Edit Banner', + 'template' => 'Website/bannerfrm.twig', + ] + )) + ->addField( + new MyRadioFormField( + 'alt', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Title', + 'explanation' => 'This is used on the backpages to identify the Banner, ' + . 'and also on the main website as mouseover text.', + ] + ) + ) + ->addField( + new MyRadioFormField( + 'target', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Action', + 'explanation' => 'This is the URL that the User will be taken to if they click the Banner. ' + . 'You can leave this blank for there to not be a link.', + 'required' => false, + ] + ) + ) + ->addField( + new MyRadioFormField( + 'type', + MyRadioFormField::TYPE_SELECT, + [ + 'label' => 'Type', + 'explanation' => 'TODO: Ask Matt what this is even supposed to do.', + 'options' => array_map( + function ($x) { + return ['value' => $x['banner_type_id'], 'text' => $x['description']]; + }, + self::getBannerTypes() + ), + ] + ) + ) + ->addField( + new MyRadioFormField( + 'photo', + MyRadioFormField::TYPE_FILE, + [ + 'label' => 'Image', + 'explanation' => 'Please upload a 680x230px image file to use as the Banner.', + ] + ) + ); + } } diff --git a/src/Classes/ServiceAPI/MyRadio_BannerCampaign.php b/src/Classes/ServiceAPI/MyRadio_BannerCampaign.php index 86cb41b5c..09e5629b8 100644 --- a/src/Classes/ServiceAPI/MyRadio_BannerCampaign.php +++ b/src/Classes/ServiceAPI/MyRadio_BannerCampaign.php @@ -1,369 +1,571 @@ - * @package MyRadio_Website - * @uses \Database + * The BannerCampaign class stores and manages information about a Banner Campaign on the front website. + * + * @uses \Database */ -class MyRadio_BannerCampaign extends ServiceAPI { - - /** - * The ID of the BannerCampaign - * @var int - */ - private $banner_campaign_id; - - /** - * The Banner this is a Campaign for - * @var MyRadio_Banner - */ - private $banner; - - /** - * The User that created this Banner Campaign - * @var MyRadio_User - */ - private $created_by; - - /** - * The User that approved this Banner Campaign - * @var MyRadio_User - */ - private $approved_by; - - /** - * The time this Banner Campaign is active from - * @var int - */ - private $effective_from; - - /** - * The time this Banner Campaign is active to - * @var int - */ - private $effective_to; - - /** - * The ID of the location of the Banner (e.g. index) - * @var int - */ - private $banner_location_id; - - /** - * A 2D array of timeslots where this Banner Campaign is visible, - * with day of week and timeslots. This is repeated every week during an active campaign. - * - * Format:
    - * [[id: 69, day: 1, starttime: "00:00:00", endtime: "00:00:00", order: 5]]
    - * Where day is [Matt doesn't even know.], and order is the order the banner appears in on the scrolling - * slideshow. A higher number appears first. - * - * @var Array[] - */ - private $timeslots; - - /** - * Initiates the MyRadio_BannerCampaign object - * @param int $banner_campaign_id The ID of the Banner Campaign to initialise - */ - protected function __construct($banner_campaign_id) { - $this->banner_campaign_id = (int) $banner_campaign_id; - - $result = self::$db->fetch_one('SELECT * FROM website.banner_campaign WHERE banner_campaign_id=$1', array($banner_campaign_id)); - if (empty($result)) { - throw new MyRadioException('Banner Campaign ' . $banner_campaign_id . ' does not exist!'); +class MyRadio_BannerCampaign extends ServiceAPI +{ + /** + * The ID of the BannerCampaign. + * + * @var int + */ + private $banner_campaign_id; + + /** + * The Banner this is a Campaign for. + * + * @var MyRadio_Banner + */ + private $banner; + + /** + * The User that created this Banner Campaign. + * + * @var MyRadio_User + */ + private $created_by; + + /** + * The User that approved this Banner Campaign. + * + * @var MyRadio_User + */ + private $approved_by; + + /** + * The time this Banner Campaign is active from. + * + * @var int + */ + private $effective_from; + + /** + * The time this Banner Campaign is active to. + * + * @var int + */ + private $effective_to; + + /** + * The ID of the location of the Banner (e.g. index). + * + * @var int + */ + private $banner_location_id; + + /** + * A 2D array of timeslots where this Banner Campaign is visible, + * with day of week and timeslots. This is repeated every week during an active campaign. + * + * Format:
    + * [[id: 69, day: 1, starttime: "00:00:00", endtime: "00:00:00", order: 5]]
    + * Where day is [Matt doesn't even know.], and order is the order the banner appears in on the scrolling + * slideshow. A higher number appears first. + * + * @var array[] + */ + private $timeslots; + + /** + * Initiates the MyRadio_BannerCampaign object. + * + * @param int $banner_campaign_id The ID of the Banner Campaign to initialise + */ + protected function __construct($banner_campaign_id) + { + $this->banner_campaign_id = (int) $banner_campaign_id; + + $result = self::$db->fetchOne( + 'SELECT * FROM website.banner_campaign WHERE banner_campaign_id=$1', + [$banner_campaign_id] + ); + if (empty($result)) { + throw new MyRadioException('Banner Campaign '.$banner_campaign_id.' does not exist!'); + } + + $this->banner = MyRadio_Banner::getInstance($result['banner_id']); + $this->created_by = MyRadio_User::getInstance($result['memberid']); + $this->approved_by = empty($result['approvedid']) ? null : MyRadio_User::getInstance($result['approvedid']); + $this->effective_from = strtotime($result['effective_from']); + $this->effective_to = empty($result['effective_to']) ? null : strtotime($result['effective_to']); + $this->banner_location_id = (int) $result['banner_location_id']; + + //Make times be in seconds since midnight + $this->timeslots = array_map( + function ($x) { + return [ + 'id' => $x['id'], 'day' => $x['day'], + 'start_time' => strtotime($x['start_time'], 0), + 'end_time' => strtotime($x['end_time'], 0), + ]; + }, + self::$db->fetchAll( + 'SELECT id, day, start_time, end_time, \'order\' FROM website.banner_timeslot + WHERE banner_campaign_id=$1', + [$this->banner_campaign_id] + ) + ); + } + + /** + * Returns data about the Campaign. + * @param array $mixins Mixins. + * @mixin timeslots Provides data about the timeslots in this campaign + * @return array + */ + public function toDataSource($mixins = []) + { + $mixin_funcs = [ + 'timeslots' => function (&$data) { + $data['timeslots'] = $this->getTimeslots(); + } + ]; + + $data = [ + 'banner_campaign_id' => $this->getID(), + 'created_by' => $this->getCreatedBy()->getID(), + 'approved_by' => ($this->getApprovedBy() == null) ? null : $this->getApprovedBy()->getID(), + 'effective_from' => CoreUtils::happyTime($this->getEffectiveFrom()), + 'effective_to' => ($this->getEffectiveTo() === null) ? + 'Never' : CoreUtils::happyTime($this->getEffectiveTo()), + 'banner_location_id' => $this->getLocation(), + 'num_timeslots' => sizeof($this->getTimeslots()), + 'edit_link' => [ + 'display' => 'icon', + 'value' => 'pencil', + 'title' => 'Click here to edit this Campaign', + 'url' => URLUtils::makeURL('Website', 'editCampaign', ['campaignid' => $this->getID()]), + ], + ]; + + $this->addMixins($data, $mixins, $mixin_funcs); + + return $data; + } + + /** + * Get the ID of the BannerCampaign. + * + * @return int + */ + public function getID() + { + return $this->banner_campaign_id; + } + + /** + * Get the User that created this Campaign. + * + * @return MyRadio_User + */ + public function getCreatedBy() + { + return $this->created_by; + } + + /** + * Get the User that approved this Campaign. + * + * @return MyRadio_User + */ + public function getApprovedBy() + { + return $this->approved_by; + } + + /** + * Get the time (as epoch int) that this Campaign starts. + * + * @return int + */ + public function getEffectiveFrom() + { + return $this->effective_from; + } + + /** + * Get the time (as epoch int) that this campaign ends. + * Returns null if the Campaign does not end. + * + * @return int + */ + public function getEffectiveTo() + { + return $this->effective_to; + } + + /** + * Get the ID of the Banner Location. + * + * @return int + */ + public function getLocation() + { + return $this->banner_location_id; + } + + /** + * Get an array of times during the Active period that the Campaign is visible on the Website. + * + * @return array [[day: 1, start_time: 0, end_time: 86399], ...] + */ + public function getTimeslots() + { + return $this->timeslots; + } + + /** + * Get the Banner this is a Campaign for. + * + * @return MyRadio_Banner + */ + public function getBanner() + { + return $this->banner; } - $this->banner = MyRadio_Banner::getInstance($result['banner_id']); - $this->created_by = MyRadio_User::getInstance($result['memberid']); - $this->approved_by = empty($result['approvedid']) ? null : MyRadio_User::getInstance($result['approvedid']); - $this->effective_from = strtotime($result['effective_from']); - $this->effective_to = empty($result['effective_to']) ? null : strtotime($result['effective_to']); - $this->banner_location_id = (int) $result['banner_location_id']; - - //Make times be in seconds since midnight - $this->timeslots = array_map(function($x) { - return ['id' => $x['id'], 'day' => $x['day'], - 'start_time' => strtotime($x['start_time'], 0), - 'end_time' => strtotime($x['end_time'], 0)]; - }, self::$db->fetch_all('SELECT id, day, start_time, end_time, \'order\' FROM website.banner_timeslot - WHERE banner_campaign_id=$1', [$this->banner_campaign_id])); - } - - /** - * Returns data about the Campaign - * @param bool $full If true, returns full, detailed data about the timeslots in this campaign - * @return Array - */ - public function toDataSource($full = false) { - $data = [ - 'banner_campaign_id' => $this->getID(), - 'created_by' => $this->getCreatedBy()->getID(), - 'approved_by' => ($this->getApprovedBy() == null) ? null : $this->getApprovedBy()->getID(), - 'effective_from' => CoreUtils::happyTime($this->getEffectiveFrom()), - 'effective_to' => ($this->getEffectiveTo() === null) ? 'Never' : CoreUtils::happyTime($this->getEffectiveTo()), - 'banner_location_id' => $this->getLocation(), - 'num_timeslots' => sizeof($this->getTimeslots()), - 'edit_link' => [ - 'display' => 'icon', - 'value' => 'pencil', - 'title' => 'Click here to edit this Campaign', - 'url' => CoreUtils::makeURL('Website', 'editCampaign', ['campaignid' => $this->getID()]) - ] - ]; - - if ($full) { - $data['timeslots'] = $this->getTimeslots(); + /** + * Returns a MyRadioForm filled in and ripe for being used to edit this Campaign. + * + * @return MyRadioForm + */ + public function getEditForm() + { + return $this->getForm($this->banner->getID()) + ->editMode( + $this->getID(), + [ + 'timeslots' => $this->getTimeslots(), + 'effective_from' => CoreUtils::happyTime($this->getEffectiveFrom()), + 'effective_to' => $this->getEffectiveTo() === null ? null : + CoreUtils::happyTime($this->getEffectiveTo()), + 'location' => $this->getLocation(), + ] + ); } - return $data; - } - - /** - * Get the ID of the BannerCampaign - * @return int - */ - public function getID() { - return $this->banner_campaign_id; - } - - /** - * Get the User that created this Campaign - * @return MyRadio_User - */ - public function getCreatedBy() { - return $this->created_by; - } - - /** - * Get the User that approved this Campaign - * @return MyRadio_User - */ - public function getApprovedBy() { - return $this->approved_by; - } - - /** - * Get the time (as epoch int) that this Campaign starts. - * @return int - */ - public function getEffectiveFrom() { - return $this->effective_from; - } - - /** - * Get the time (as epoch int) that this campaign ends. - * Returns null if the Campaign does not end. - * @return int - */ - public function getEffectiveTo() { - return $this->effective_to; - } - - /** - * Get the ID of the Banner Location - * @return int - */ - public function getLocation() { - return $this->banner_location_id; - } - - /** - * Get an array of times during the Active period that the Campaign is visible on the Website. - * @return Array [[day: 1, start_time: 0, end_time: 86399], ...] - */ - public function getTimeslots() { - return $this->timeslots; - } - - /** - * Get the Banner this is a Campaign for - * @return MyRadio_Banner - */ - public function getBanner() { - return $this->banner; - } - - /** - * Returns a MyRadioForm filled in and ripe for being used to edit this Campaign. - * @return MyRadioForm - */ - public function getEditForm() { - return $this->getBannerCampaignForm($this->banner->getID()) - ->editMode($this->getID(), [ - 'timeslots' => $this->getTimeslots(), - 'effective_from' => CoreUtils::happyTime($this->getEffectiveFrom()), - 'effective_to' => $this->getEffectiveTo() === null ? null : - CoreUtils::happyTime($this->getEffectiveTo()), - 'location' => $this->getLocation() - ], 'doEditCampaign'); - } - - /** - * Return if this Banner Campaign is currently active. That is, it has started and has not expired. - * It returns true even when there isn't currently a Banner Timeslot for the Campaign running. - * @return boolean - */ - public function isActive() { - return $this->effective_from <= time() && ($this->effective_to == null or $this->effective_to > time()); - } - - /** - * Removes all timeslots associated with a Banner Campaign. - * - * Used when editing, as they are then immediately added again. - */ - public function clearTimeslots() { - self::$db->query('DELETE FROM website.banner_timeslot WHERE banner_campaign_id=$1', [$this->getID()]); - } - - /** - * Sets the start time of the Campaign - * @param int $time - * @return MyRadio_BannerCampaign - */ - public function setEffectiveFrom($time) { - $this->effective_from = $time; - self::$db->query('UPDATE website.banner_campaign SET effective_from=$1 WHERE banner_campaign_id=$2', - [CoreUtils::getTimestamp($time), $this->getID()]); - - return $this; - } - - /** - * Sets the end time of the Campaign - * @param int $time - * @return MyRadio_BannerCampaign - */ - public function setEffectiveTo($time) { - $this->effective_to = $time; - self::$db->query('UPDATE website.banner_campaign SET effective_to=$1 WHERE banner_campaign_id=$2', - [CoreUtils::getTimestamp($time), $this->getID()]); - - return $this; - } - - /** - * Sets the location of the Campaign - * @param int $location - * @return MyRadio_BannerCampaign - */ - public function setLocation($location) { - $this->banner_location_id = $location; - self::$db->query('UPDATE website.banner_campaign SET banner_location_id=$1 WHERE banner_campaign_id=$2', - [$location, $this->getID()]); - - return $this; - } - - /** - * Adds an Active Timeslot to the Campaign - * - * @param int $day Day the timeslot is on. 1 = Monday, 7 = Sunday. Timeslots cannot span days. - * @param int $start Seconds since midnight that the Timeslot starts. - * @param int $end Seconds since midnight that the Timeslot ends. - * @todo Input validation. - */ - public function addTimeslot($day, $start, $end) { - $start = gmdate('H:i:s', $start).'+00'; - $end = gmdate('H:i:s', $end).'+00'; - - self::$db->query('INSERT INTO website.banner_timeslot' - . ' (banner_campaign_id, memberid, approvedid, "order", day, start_time, end_time) VALUES' - . ' ($1, $2, $2, $1, $3, $4, $5)', [$this->getID(), MyRadio_User::getInstance()->getID(), $day, $start, $end]); - } - - /** - * Creates a new Banner Campaign - * @param MyRadio_Banner $banner The Banner that is being Campaigned - * @param int $banner_location_id The location of the Banner Campaign. Default 1 (index page) - * @param int $effective_from Epoch time that the campaign is starts at. Default now. - * @param int $effective_to Epoch time that the campaign ends at. Default never. - * @param Array $timeslots An array of Timeslots the Campaign is active during. - * @return MyRadio_BannerCampaign The new BannerCampaign - */ - public static function create(MyRadio_Banner $banner, $banner_location_id = 1, $effective_from = null, - $effective_to = null, $timeslots = array()) { - if ($effective_from == null) { - $effective_from = time(); + /** + * Return if this Banner Campaign is currently active. That is, it has started and has not expired. + * It returns true even when there isn't currently a Banner Timeslot for the Campaign running. + * + * @return bool + */ + public function isActive() + { + return $this->effective_from <= time() && ($this->effective_to == null or $this->effective_to > time()); } - $result = self::$db->fetch_column('INSERT INTO website.banner_campaign - (banner_id, banner_location_id, effective_from, effective_to, memberid, approvedid) - VALUES ($1, $2, $3, $4, $5, $5) RETURNING banner_campaign_id', array($banner->getBannerID(), $banner_location_id, - CoreUtils::getTimestamp($effective_from), - CoreUtils::getTimestamp($effective_to), MyRadio_User::getInstance()->getID())); - - $campaign = self::getInstance($result[0]); - - foreach ($timeslots as $timeslot) { - $campaign->addTimeslot($timeslot['day'], $timeslot['start_time'], $timeslot['end_time']); + /** + * Removes all timeslots associated with a Banner Campaign. + * + * Used when editing, as they are then immediately added again. + */ + public function clearTimeslots() + { + $this->timeslots = []; + self::$db->query('DELETE FROM website.banner_timeslot WHERE banner_campaign_id=$1', [$this->getID()]); + $this->updateCacheObject(); } - - return $campaign; - } - - /** - * Get all Banner Campaigns - * @return MyRadio_BannerCampaign[] - */ - public static function getAllBannerCampaigns() { - return self::resultSetToObjArray(self::$db->fetch_column('SELECT banner_campaign_id FROM website.banner_campaign')); - } - - /** - * Get all the possible Banner Campaign Locations. - * @return Array - */ - public static function getCampaignLocations() { - return self::$db->fetch_all('SELECT banner_location_id AS value, description AS text FROM website.banner_location'); - } - - /** - * Returns the form needed to create or edit Banner Campaigns. - * - * @param int $bannerid The ID of the Banner that this Campaign will be/is linked to - * @return MyRadioForm - */ - public static function getBannerCampaignForm($bannerid = null) { - return (new MyRadioForm('bannercampaignfrm', 'Website', 'doCreateCampaign', [ - 'template' => 'Website/campaignfrm.twig', - 'title' => 'Edit Banner Campaign' - ])) - ->addField(new MyRadioFormField('effective_from', MyRadioFormField::TYPE_DATETIME, [ - 'required' => true, - 'value' => CoreUtils::happyTime(time()), - 'label' => 'Start Time', - 'explanation' => 'The time from which this Campaign becomes active.' - ])) - ->addField(new MyRadioFormField('effective_to', MyRadioFormField::TYPE_DATETIME, [ - 'required' => false, - 'label' => 'End Time', - 'explanation' => 'The time at which this Campaign becomes inactive. Leaving this blank means' - . ' the Campaign will continue indefinitely.' - ])) - ->addField(new MyRadioFormField('location', MyRadioFormField::TYPE_SELECT, [ - 'label' => 'Location', - 'explanation' => 'Choose where on the website this Campaign is run.', - 'options' => self::getCampaignLocations() - ])) - ->addField(new MyRadioFormField('timeslots', MyRadioFormField::TYPE_WEEKSELECT, [ - 'label' => 'Timeslots', - 'explanation' => 'All times filled in on this schedule (i.e. are purple) are times during the' - . ' week that this Campaign is considered active, and therefore appears on the website.' - . ' Click a square to toggle it. Click and drag to select lots at once!', - ])) - ->addField(new MyRadioFormField('bannerid', MyRadioFormField::TYPE_HIDDEN, [ - 'value' => $bannerid - ])); - } + /** + * Sets the start time of the Campaign. + * + * @param int $time + * + * @return MyRadio_BannerCampaign + */ + public function setEffectiveFrom($time) + { + $this->effective_from = $time; + self::$db->query( + 'UPDATE website.banner_campaign SET effective_from=$1 WHERE banner_campaign_id=$2', + [CoreUtils::getTimestamp($time), $this->getID()] + ); + $this->updateCacheObject(); + + return $this; + } + + /** + * Sets the end time of the Campaign. + * + * @param int $time + * + * @return MyRadio_BannerCampaign + */ + public function setEffectiveTo($time) + { + if ($time === null) { + $this->effective_to = $time; + self::$db->query( + 'UPDATE website.banner_campaign SET effective_to=NULL WHERE banner_campaign_id=$1', + [$this->getID()] + ); + } else { + self::$db->query( + 'UPDATE website.banner_campaign SET effective_to=$1 WHERE banner_campaign_id=$2', + [CoreUtils::getTimestamp($time), $this->getID()] + ); + } + + $this->updateCacheObject(); + + return $this; + } + + /** + * Sets the location of the Campaign. + * + * @param int $location + * + * @return MyRadio_BannerCampaign + */ + public function setLocation($location) + { + $this->banner_location_id = $location; + self::$db->query( + 'UPDATE website.banner_campaign SET banner_location_id=$1 WHERE banner_campaign_id=$2', + [$location, $this->getID()] + ); + $this->updateCacheObject(); + + return $this; + } + + /** + * Adds an Active Timeslot to the Campaign. + * + * @param int $day Day the timeslot is on. 1 = Monday, 7 = Sunday. Timeslots cannot span days. + * @param int $start Seconds since midnight that the Timeslot starts. + * @param int $end Seconds since midnight that the Timeslot ends. + * + * @todo Input validation. + */ + public function addTimeslot($day, $start, $end) + { + $start = gmdate('H:i:s', $start).'+00'; + $end = gmdate('H:i:s', $end).'+00'; + + $id = self::$db->fetchColumn( + 'INSERT INTO website.banner_timeslot + (banner_campaign_id, memberid, approvedid, "order", day, start_time, end_time) + VALUES ($1, $2, $2, $1, $3, $4, $5) RETURNING id', + [ + $this->getID(), + MyRadio_User::getInstance()->getID(), + $day, + $start, + $end, + ] + )[0]; + + $this->timeslots[] = [ + 'id' => $id, + 'day' => $day, + 'start_time' => strtotime($start, 0), + 'end_time' => strtotime($end, 0), + ]; + + $this->updateCacheObject(); + } + + /** + * Creates a new Banner Campaign. + * + * @param MyRadio_Banner $banner The Banner that is being Campaigned + * @param int $banner_location_id The location of the Banner Campaign. Default 1 (index page) + * @param int $effective_from Epoch time that the campaign is starts at. Default now. + * @param int $effective_to Epoch time that the campaign ends at. Default never. + * @param array $timeslots An array of Timeslots the Campaign is active during. + * + * @return MyRadio_BannerCampaign The new BannerCampaign + */ + public static function create( + MyRadio_Banner $banner, + $banner_location_id = 1, + $effective_from = null, + $effective_to = null, + $timeslots = [] + ) { + if ($effective_from == null) { + $effective_from = time(); + } + + $result = self::$db->fetchColumn( + 'INSERT INTO website.banner_campaign + (banner_id, banner_location_id, effective_from, effective_to, memberid, approvedid) + VALUES ($1, $2, $3, $4, $5, $5) RETURNING banner_campaign_id', + [ + $banner->getBannerID(), + $banner_location_id, + CoreUtils::getTimestamp($effective_from), + CoreUtils::getTimestamp($effective_to), + MyRadio_User::getInstance()->getID(), + ] + ); + + $campaign = self::getInstance($result[0]); + + foreach ($timeslots as $timeslot) { + $campaign->addTimeslot($timeslot['day'], $timeslot['start_time'], $timeslot['end_time']); + } + + return $campaign; + } + + /** + * Get all Banner Campaigns. + * + * @return MyRadio_BannerCampaign[] + */ + public static function getAllBannerCampaigns() + { + return self::resultSetToObjArray( + self::$db->fetchColumn('SELECT banner_campaign_id FROM website.banner_campaign') + ); + } + + /** + * Gets all Banner Campaigns that are currently active. That is, they have started and have not expired. + * It returns them even when there isn't currently a Banner Timeslot for the Campaign running. + * + * @return MyRadio_BannerCampaign[] + */ + public static function getActiveBannerCampaigns() + { + return self::resultSetToObjArray( + self::$db->fetchColumn( + 'SELECT banner_campaign_id FROM website.banner_campaign + WHERE effective_from < now() + AND (effective_to IS NULL + OR effective_to > now())' + ) + ); + } + + /** + * Gets all currently live Banner Campaigns. That is they are active and have timeslots at the current time. + * + * @return MyRadio_BannerCampaign[] + */ + public static function getLiveBannerCampaigns() + { + return self::resultSetToObjArray( + self::$db->fetchColumn( + 'SELECT website.banner_campaign.banner_campaign_id FROM website.banner_campaign, website.banner_timeslot + WHERE website.banner_campaign.banner_campaign_id = website.banner_timeslot.banner_campaign_id + AND effective_from < now() + AND (effective_to IS NULL + OR effective_to > now()) + AND day = EXTRACT(ISODOW FROM now()) + AND start_time < localtime + AND end_time > localtime + ORDER BY "order" ASC' + ) + ); + } + + /** + * Get all the possible Banner Campaign Locations. + * + * @return array + */ + public static function getCampaignLocations() + { + return self::$db->fetchAll( + 'SELECT banner_location_id AS value, description AS text FROM website.banner_location' + ); + } + + /** + * Returns the form needed to create or edit Banner Campaigns. + * + * @param int $bannerid The ID of the Banner that this Campaign will be/is linked to + * + * @return MyRadioForm + */ + public static function getForm($bannerid = null) + { + return ( + new MyRadioForm( + 'bannercampaignfrm', + 'Website', + 'editCampaign', + [ + 'template' => 'Website/campaignfrm.twig', + 'title' => 'Edit Banner Campaign', + ] + ) + ) + ->addField( + new MyRadioFormField( + 'effective_from', + MyRadioFormField::TYPE_DATETIME, + [ + 'required' => true, + 'value' => CoreUtils::happyTime(time()), + 'label' => 'Start Time', + 'explanation' => 'The time from which this Campaign becomes active.', + ] + ) + ) + ->addField( + new MyRadioFormField( + 'effective_to', + MyRadioFormField::TYPE_DATETIME, + [ + 'required' => false, + 'label' => 'End Time', + 'explanation' => 'The time at which this Campaign becomes inactive. Leaving this blank means' + .' the Campaign will continue indefinitely.', + ] + ) + ) + ->addField( + new MyRadioFormField( + 'location', + MyRadioFormField::TYPE_SELECT, + [ + 'label' => 'Location', + 'explanation' => 'Choose where on the website this Campaign is run.', + 'options' => self::getCampaignLocations(), + ] + ) + ) + ->addField( + new MyRadioFormField( + 'timeslots', + MyRadioFormField::TYPE_WEEKSELECT, + [ + 'label' => 'Timeslots', + 'explanation' => 'All times filled in on this schedule (i.e. are purple) are times during the' + .' week that this Campaign is considered active, and therefore appears on the website.' + .' Click a square to toggle it. Click and drag to select lots at once!', + ] + ) + ) + ->addField( + new MyRadioFormField( + 'bannerid', + MyRadioFormField::TYPE_HIDDEN, + [ + 'value' => $bannerid, + ] + ) + ); + } } diff --git a/src/Classes/ServiceAPI/MyRadio_ChartRelease.php b/src/Classes/ServiceAPI/MyRadio_ChartRelease.php index d1efff037..1d6e2afd9 100644 --- a/src/Classes/ServiceAPI/MyRadio_ChartRelease.php +++ b/src/Classes/ServiceAPI/MyRadio_ChartRelease.php @@ -1,8 +1,14 @@ - * @package MyRadio_Charts * @uses \Database */ -class MyRadio_ChartRelease extends ServiceAPI { +class MyRadio_ChartRelease extends ServiceAPI +{ const GET_INSTANCE_SQL = ' SELECT * @@ -53,6 +57,8 @@ class MyRadio_ChartRelease extends ServiceAPI { music.chart_release(chart_type_id, submitted) VALUES ($1, $2) + RETURNING + chart_release_id ;'; const SET_RELEASE_TIME_SQL = ' @@ -73,40 +79,45 @@ class MyRadio_ChartRelease extends ServiceAPI { chart_release_id = $2 ;'; - /** - * The singleton store for ChartRelease objects + * The singleton store for ChartRelease objects. + * * @var MyRadio_ChartRelease[] */ private static $chart_releases = []; /** * The chart type this chart release was released under. + * * @var MyRadio_ChartType */ private $chart_type; /** * The numeric ID of the chart type. - * @var Int + * + * @var int */ private $chart_type_id; /** * The numeric ID of the chart release. - * @var Int + * + * @var int */ private $chart_release_id; /** * The UNIX timestamp, if any, on which this chart release was released. - * @var Int + * + * @var int */ private $release_time; /** * The list of IDs of MyRadio_ChartRows for this chart release. - * @var Int[] + * + * @var int[] */ private $chart_row_ids; @@ -118,25 +129,27 @@ class MyRadio_ChartRelease extends ServiceAPI { * @param $chart_release_id The numeric ID of the chart release. * @param $chart_type The parent chart type, if any. * - * @return The chart type with the given ID. + * @return The chart release with the given ID. */ - protected function __construct($chart_release_id, $chart_type=null) { - $this->chart_release_id = $chart_release_id; + protected function __construct($chart_release_id, $chart_type = null) + { + $this->chart_release_id = (int) $chart_release_id; $this->chart_type = $chart_type; - $chart_release_data = self::$db->fetch_one( + $chart_release_data = self::$db->fetchOne( self::GET_INSTANCE_SQL, [$chart_release_id] ); if (empty($chart_release_data)) { throw new MyRadioException('The specified Chart Release does not seem to exist.'); + return; } $this->release_time = strtotime($chart_release_data['submitted']); $this->chart_type_id = $chart_release_data['chart_type_id']; - $this->chart_row_ids = self::$db->fetch_column( + $this->chart_row_ids = self::$db->fetchColumn( self::GET_CHART_ROWS_SQL, [$chart_release_id] ); @@ -150,8 +163,9 @@ protected function __construct($chart_release_id, $chart_type=null) { * * @return The chart release with the given ID. */ - public static function getInstance($chart_release_id=-1, $chart_type=null) { - self::__wakeup(); + public static function getInstance($chart_release_id = -1, $chart_type = null) + { + self::wakeup(); if (!is_numeric($chart_release_id)) { throw new MyRadioException( @@ -166,6 +180,7 @@ public static function getInstance($chart_release_id=-1, $chart_type=null) { $chart_type ); } + return self::$chart_releases[$chart_release_id]; } @@ -175,150 +190,275 @@ public static function getInstance($chart_release_id=-1, $chart_type=null) { * * This is mainly useful for finding a newly created chart type's ID. * - * @param int $release_time The release time, as a UNIX timestamp. - * @param int $chart_type_id The ID of the chart type to search in. - * @return int The first chart released on the given time for - * the given type. + * @param int $release_time The release time, as a UNIX timestamp. + * @param int $chart_type_id The ID of the chart type to search in. + * + * @return int The first chart released on the given time for + * the given type. */ - public function findReleaseIDOn($release_time, $chart_type_id) { + public function findReleaseIDOn($release_time, $chart_type_id) + { return array_pop( - self::$db->fetch_column( + self::$db->fetchColumn( self::FIND_RELEASE_ID_ON_SQL, [ $chart_type_id, - date('c', $release_time) + date('c', $release_time), ] ) ); } - + /** * Retrieves the time of release of this chart release. * - * @return int the submission time as a UNIX timestamp. + * @return int the submission time as a UNIX timestamp. */ - public function getReleaseTime() { + public function getReleaseTime() + { return $this->release_time; } /** * Retrieves the unique ID of this chart release. * - * @return int The chart release ID. + * @return int The chart release ID. */ - public function getID() { + public function getID() + { return $this->chart_release_id; } /** * Retrieves the unique ID of this chart release's type. * - * @return int The chart type ID. + * @return int The chart type ID. */ - public function getChartTypeID() { + public function getChartTypeID() + { return $this->chart_type_id; } /** * Retrieves the type this chart release falls under. * - * @return MyRadio_ChartType The chart type object. + * @return MyRadio_ChartType The chart type object. */ - public function getChartType() { + public function getChartType() + { if ($this->chart_type === null) { $this->chart_type = MyRadio_ChartType::getInstance($this->chart_type_id); } + return $this->chart_type; } /** * Retrieves the rows that make up this chart release. * - * @return array The chart rows. + * @return array The chart rows. */ - public function getChartRows() { + public function getChartRows() + { $chart_rows = []; foreach ($this->chart_row_ids as $chart_row_id) { $chart_rows[] = MyRadio_ChartRow::getInstance($chart_row_id, $this); } + return $chart_rows; } + /** + * Sets the chart rows that make up this chart release. + * + * @param $chart_rows array An array of trackids in position order. + * + * @return none. + */ + public function setChartRows($chart_rows) + { + $old_rows = $this->getChartRows(); + if (empty($old_rows)) { + foreach ($chart_rows as $i => $row) { + MyRadio_ChartRow::create( + [ + 'chart_release_id' => $this->getID(), + 'position' => $i + 1, + 'trackid' => $row, + ] + ); + } + } else { + foreach ($chart_rows as $i => $row) { + if ($old_rows[$i]->getTrackID() !== $row) { + $old_rows[$i]->setTrackID($row); + } + } + } + } + /** * Creates a new chart release in the database. * - * @param $data array An array of data to populate the row with. + * @param $data array An array of data to populate the row with. * Must contain 'chart_type_id' and 'submitted_time'. - * @return null nothing. + * + * @return The chart release with the given ID. */ - public static function create($data) { - self::$db->query( + public static function create($data) + { + $r = self::$db->fetchColumn( self::INSERT_SQL, [ - intval($data['chart_type_id']), - date('%c', intval($data['submitted_time'])) // Expecting UNIX timestamp + intval($data['chart_type_id']), + date('%c', intval($data['submitted_time'])), // Expecting UNIX timestamp ], true ); + + return self::getInstance($r[0]); } /** * Sets this chart release's release time. * - * @param int $release_time The new time, as a UNIX timestamp. + * @param int $release_time The new time, as a UNIX timestamp. * - * @return MyRadio_ChartRelease This object, for method chaining. + * @return MyRadio_ChartRelease This object, for method chaining. */ - public function setReleaseTime($release_time) { + public function setReleaseTime($release_time) + { $this->release_time = strtotime($release_time); - return $this->set_db(SET_RELEASE_TIME_SQL, date('c', $release_time)); + + return $this->setDB(self::SET_RELEASE_TIME_SQL, date('c', $release_time)); } /** * Sets this chart release's type ID. * - * @param int $chart_type_id The new ID. + * @param int $chart_type_id The new ID. * * @return MyRadio_ChartRelease This object, for method chaining. */ - public function setChartTypeID($chart_type_id) { + public function setChartTypeID($chart_type_id) + { $this->chart_type_id = intval($chart_type_id); - return $this->set_db(SET_CHART_TYPE_ID_SQL, intval($chart_type_id)); + + return $this->setDB(self::SET_CHART_TYPE_ID_SQL, intval($chart_type_id)); } /** * Sets a property on the database representation of this chart release. * - * @param string $sql The SQL to use for setting this property. + * @param string $sql The SQL to use for setting this property. * @param $value The value of the property to set on this chart release. * - * @return MyRadio_ChartRelease This object, for method chaining. + * @return MyRadio_ChartRelease This object, for method chaining. */ - private function set_db($sql, $value) { + private function setDB($sql, $value) + { self::$db->query($sql, [$value, $this->getID()]); + return $this; } + public static function getForm() + { + $types = MyRadio_ChartType::getAll(); + $type_select = [['text' => 'Please select...', 'disabled' => true]]; + foreach ($types as $type) { + $type_select[] = [ + 'value' => $type->getID(), + 'text' => $type->getDescription(), + ]; + } + + $form = ( + new MyRadioForm( + 'charts_editchartrelease', + 'Charts', + 'editChartRelease', + ['title' => 'Create Chart Release'] + ) + )->addField( + new MyRadioFormField( + 'chart_type_id', + MyRadioFormField::TYPE_SELECT, + [ + 'label' => 'Chart Type', + 'explanation' => 'The type of chart.', + 'options' => $type_select, + ] + ) + )->addField( + new MyRadioFormField( + 'submitted_time', + MyRadioFormField::TYPE_DATE, + [ + 'label' => 'Release Date', + 'explanation' => 'The date on which the chart is released.', + ] + ) + )->addField( + new MyRadioFormField( + 'tracks', + MyRadioFormField::TYPE_TABULARSET, + array( + 'options' => array( + new MyRadioFormField( + 'track', + MyRadioFormField::TYPE_TRACK, + [ + 'label' => 'Tracks', + ] + ), + ), + ) + ) + ); + + return $form; + } + + public function getEditForm() + { + return self::getForm() + ->setTitle('Edit Chart Release') + ->editMode( + $this->getID(), + [ + 'chart_type_id' => $this->getChartTypeID(), + 'submitted_time' => CoreUtils::happyTime($this->getReleaseTime(), false), + 'tracks.track' => array_map( + function ($chartRow) { + return $chartRow->getTrack(); + }, + $this->getChartRows() + ), + ] + ); + } + /** * Converts this chart release to a table data source. - * - * @return array The object as a data source. + * @param array $mixins Mixins. + * @return array The object as a data source. */ - public function toDataSource() { + public function toDataSource($mixins = []) + { return [ 'type' => $this->getChartType()->getDescription(), - 'date' => strftime('%c', $this->getReleaseTime()), + 'date' => date('d/m/Y', $this->getReleaseTime()), 'editlink' => [ 'display' => 'icon', - 'value' => 'script', + 'value' => 'pencil', 'title' => 'Edit Chart Release', - 'url' => CoreUtils::makeURL( + 'url' => URLUtils::makeURL( 'Charts', 'editChartRelease', ['chart_release_id' => $this->getID()] - ) + ), ], ]; } } -?> diff --git a/src/Classes/ServiceAPI/MyRadio_ChartRow.php b/src/Classes/ServiceAPI/MyRadio_ChartRow.php index 87cc95c60..6a12aa0da 100644 --- a/src/Classes/ServiceAPI/MyRadio_ChartRow.php +++ b/src/Classes/ServiceAPI/MyRadio_ChartRow.php @@ -1,166 +1,183 @@ - * @package MyRadio_Charts + * * @uses \Database */ -class MyRadio_ChartRow extends ServiceAPI { - /** - * The singleton store for ChartRow objects - * @var MyRadio_ChartRow[] - */ - private static $chart_rows = []; - - /** - * The numeric ID of the chart row. - * @var Int - */ - private $chart_row_id; - - /** - * The position on the chart release this row occupies. - * @var Int - */ - private $position; - - /** - * The ID of the track at this position. - * @var String - */ - private $trackid; - - /** - * Constructs a new MyRadio_ChartRow from the database. - * - * You should generally use MyRadio_ChartRow::getInstance instead. - * - * @param $chart_row_id The numeric ID of the chart row. - * @param $chart_release The parent chart release, if any. - * - * @return The chart row with the given ID. - */ - protected function __construct($chart_row_id, $chart_release=null) { - $this->chart_row_id = $chart_row_id; - $this->chart_release = $chart_release; - - $chart_row_data = self::$db->fetch_one( - 'SELECT * - FROM music.chart_row - WHERE chart_row_id = $1;', - [$chart_row_id] - ); - if (empty($chart_row_data)) { - throw new MyRadioException('The specified Chart Row does not seem to exist.'); - return; +class MyRadio_ChartRow extends ServiceAPI +{ + /** + * The singleton store for ChartRow objects. + * + * @var MyRadio_ChartRow[] + */ + private static $chart_rows = []; + + /** + * The numeric ID of the chart row. + * + * @var int + */ + private $chart_row_id; + + /** + * The position on the chart release this row occupies. + * + * @var int + */ + private $position; + + /** + * The ID of the track at this position. + * + * @var string + */ + private $trackid; + + /** + * Constructs a new MyRadio_ChartRow from the database. + * + * You should generally use MyRadio_ChartRow::getInstance instead. + * + * @param $chart_row_id The numeric ID of the chart row. + * @param $chart_release The parent chart release, if any. + * + * @return The chart row with the given ID. + */ + protected function __construct($chart_row_id, $chart_release = null) + { + $this->chart_row_id = $chart_row_id; + $this->chart_release = $chart_release; + + $chart_row_data = self::$db->fetchOne( + 'SELECT * + FROM music.chart_row + WHERE chart_row_id = $1;', + [$chart_row_id] + ); + if (empty($chart_row_data)) { + throw new MyRadioException('The specified Chart Row does not seem to exist.'); + + return; + } + + $this->position = intval($chart_row_data['position']); + $this->trackid = intval($chart_row_data['trackid']); + } + + /** + * Retrieves the MyRadio_ChartRow with the given numeric ID. + * + * @param $chart_row_id The numeric ID of the chart row. + */ + public static function getInstance($chart_row_id = -1) + { + self::wakeup(); + + if (!is_numeric($chart_row_id)) { + throw new MyRadioException( + 'Invalid Chart Row ID!', + MyRadioException::FATAL + ); + } + + if (!isset(self::$chart_rows[$chart_row_id])) { + self::$chart_rows[$chart_row_id] = new self($chart_row_id); + } + + return self::$chart_rows[$chart_row_id]; + } + + /** + * Retrieves the unique ID of this chart row. + * + * @return The chart row ID. + */ + public function getID() + { + return $this->chart_row_id; } - $this->position = intval($chart_row_data['position']); - $this->trackid = intval($chart_row_data['trackid']); - } - - /** - * Retrieves the MyRadio_ChartRow with the given numeric ID. - * - * @param $chart_row_id The numeric ID of the chart row. - */ - public static function getInstance($chart_row_id=-1) { - self::__wakeup(); - - if (!is_numeric($chart_row_id)) { - throw new MyRadioException( - 'Invalid Chart Row ID!', - MyRadioException::FATAL - ); + /** + * Returns the chart row's track ID. + * + * @return int The unique integral ID of the chart row track. + */ + public function getTrackID() + { + return $this->trackid; } - if (!isset(self::$chart_rows[$chart_row_id])) { - self::$chart_rows[$chart_row_id] = new self($chart_row_id); + /** + * Returns the chart row's track. + * + * Will perform one database query, most likely. + * + * @return MyRadio_Track The track this chart row represents. + */ + public function getTrack() + { + return MyRadio_Track::getInstance($this->getTrackID()); } - return self::$chart_rows[$chart_row_id]; - } - - /** - * Retrieves the unique ID of this chart row. - * - * @return The chart row ID. - */ - public function getID() { - return $this->chart_row_id; - } - - /** - * Returns the chart row's track ID. - * @return int The unique integral ID of the chart row track. - */ - public function getTrackID() { - return $this->trackid; - } - - /** - * Returns the chart row's track. - * - * Will perform one database query, most likely. - * - * @return MyRadio_Track The track this chart row represents. - */ - public function getTrack() { - return MyRadio_Track::getInstance($this->getTrackID()); - } - - /** - * Returns the chart row's position. - * @return The position on the chart release this row occupies. - */ - public function getPosition() { - return $this->position; - } - - /** - * Creates a new chart row in the database. - * - * @param $data array An array of data to populate the row with. - * Must contain 'position', 'chart_release_id' and - * 'trackid'. - * @return MyRadio_ChartRow The newly created track. - */ - public function create($data) { - self::$db->query( - 'INSERT INTO music.chart_row(chart_release_id, position, trackid) - VALUES ($1, $2, $3);', - [ - $data['chart_release_id'], - $data['position'], - $data['trackid'] - ], - true - ); - } - - /** - * Sets this chart row's track ID. - * - * @param int $trackid The new track ID. - * - * @return This object, for method chaining. - */ - public function setTrackID($trackid) { - $this->trackid = intval($trackid); - - self::$db->query( - 'UPDATE music.chart_row - SET trackid = $1 - WHERE chart_row_id = $2;', - [$trackid, $this->getID()] - ); - - return $this; - } + /** + * Returns the chart row's position. + * + * @return The position on the chart release this row occupies. + */ + public function getPosition() + { + return $this->position; + } + + /** + * Creates a new chart row in the database. + * + * @param $data array An array of data to populate the row with. + * Must contain 'position', 'chart_release_id' and + * 'trackid'. + * + * @return MyRadio_ChartRow The newly created track. + */ + public function create($data) + { + self::$db->query( + 'INSERT INTO music.chart_row(chart_release_id, position, trackid) + VALUES ($1, $2, $3);', + [ + $data['chart_release_id'], + $data['position'], + $data['trackid'], + ], + true + ); + } + + /** + * Sets this chart row's track ID. + * + * @param int $trackid The new track ID. + * + * @return This object, for method chaining. + */ + public function setTrackID($trackid) + { + $this->trackid = intval($trackid); + + self::$db->query( + 'UPDATE music.chart_row + SET trackid = $1 + WHERE chart_row_id = $2;', + [$trackid, $this->getID()] + ); + + return $this; + } } diff --git a/src/Classes/ServiceAPI/MyRadio_ChartType.php b/src/Classes/ServiceAPI/MyRadio_ChartType.php index 6ad76fa93..5f26c4e89 100644 --- a/src/Classes/ServiceAPI/MyRadio_ChartType.php +++ b/src/Classes/ServiceAPI/MyRadio_ChartType.php @@ -1,218 +1,281 @@ - * @package MyRadio_Charts + * * @uses \Database */ -class MyRadio_ChartType extends MyRadio_Type { - /** - * The singleton store for ChartType objects - * @var MyRadio_ChartType[] - */ - private static $chart_types = []; - - /** - * The numeric ID of the chart type. - * @var Int - */ - private $chart_type_id; - - - /** - * The list of IDs of MyRadio_ChartReleases for this chart type. - * @var Int - */ - private $chart_release_ids; - - /** - * Constructs a new MyRadio_ChartType from the database. - * - * You should generally use MyRadio_ChartType::getInstance instead. - * - * @param $chart_type_id The numeric ID of the chart type. - * - * @return The chart type with the given ID. - */ - protected function __construct($chart_type_id) { - $this->chart_type_id = $chart_type_id; - - $chart_type_data = self::$db->fetch_one( - 'SELECT * - FROM music.chart_type - WHERE chart_type_id = $1;', - [$chart_type_id] - ); - if (empty($chart_type_data)) { - throw new MyRadioException('The specified Chart Type does not seem to exist.'); - return; +class MyRadio_ChartType extends MyRadio_Type +{ + /** + * The singleton store for ChartType objects. + * + * @var MyRadio_ChartType[] + */ + private static $chart_types = []; + + /** + * The numeric ID of the chart type. + * + * @var int + */ + private $chart_type_id; + + /** + * The list of IDs of MyRadio_ChartReleases for this chart type. + * + * @var int + */ + private $chart_release_ids; + + /** + * Constructs a new MyRadio_ChartType from the database. + * + * You should generally use MyRadio_ChartType::getInstance instead. + * + * @param $chart_type_id The numeric ID of the chart type. + * + * @return The chart type with the given ID. + */ + protected function __construct($chart_type_id) + { + $this->chart_type_id = $chart_type_id; + + $chart_type_data = self::$db->fetchOne( + 'SELECT * + FROM music.chart_type + WHERE chart_type_id = $1;', + [$chart_type_id] + ); + if (empty($chart_type_data)) { + throw new MyRadioException('The specified Chart Type does not seem to exist.'); + + return; + } + + parent::constructType($chart_type_data['name'], $chart_type_data['description']); + + $this->chart_release_ids = self::$db->fetchColumn( + 'SELECT chart_release_id + FROM music.chart_release + WHERE chart_type_id = $1 + ORDER BY submitted DESC;', + [$chart_type_id] + ); + } + + /** + * Retrieves the MyRadio_ChartType with the given numeric ID. + * + * @param $chart_type_id The numeric ID of the chart type. + * + * @return The chart type with the given ID. + */ + public static function getInstance($chart_type_id = -1) + { + self::wakeup(); + + if (!is_numeric($chart_type_id)) { + throw new MyRadioException( + 'Invalid Chart Type ID!', + MyRadioException::FATAL + ); + } + + if (!isset(self::$chart_types[$chart_type_id])) { + self::$chart_types[$chart_type_id] = new self($chart_type_id); + } + + return self::$chart_types[$chart_type_id]; + } + + /** + * Retrieves all current chart types. + * + * @return array An array of all active chart types. + */ + public static function getAll() + { + $chart_type_ids = self::$db->fetchColumn( + 'SELECT chart_type_id + FROM music.chart_type + ORDER BY chart_type_id ASC;', + [] + ); + $chart_types = []; + foreach ($chart_type_ids as $chart_type_id) { + $chart_types[] = self::getInstance($chart_type_id); + } + + return $chart_types; } - parent::construct_type($chart_type_data['name'], $chart_type_data['description']); - - $this->chart_release_ids = self::$db->fetch_column( - 'SELECT chart_release_id - FROM music.chart_release - WHERE chart_type_id = $1 - ORDER BY submitted DESC;', - [$chart_type_id] - ); - } - - /** - * Retrieves the MyRadio_ChartType with the given numeric ID. - * - * @param $chart_type_id The numeric ID of the chart type. - * - * @return The chart type with the given ID. - */ - public static function getInstance($chart_type_id=-1) { - self::__wakeup(); - - if (!is_numeric($chart_type_id)) { - throw new MyRadioException( - 'Invalid Chart Type ID!', - MyRadioException::FATAL - ); + /** + * Retrieves the unique ID of this chart type. + * + * @return The chart type ID. + */ + public function getID() + { + return $this->chart_type_id; } - if (!isset(self::$chart_types[$chart_type_id])) { - self::$chart_types[$chart_type_id] = new self($chart_type_id); + /** + * Retrieves the number of releases made under this chart type. + * + * @return int The release count. + */ + public function getNumberOfReleases() + { + return sizeof($this->chart_release_ids); } - return self::$chart_types[$chart_type_id]; - } - - /** - * Retrieves all current chart types. - * - * @return array An array of all active chart types. - */ - public function getAll() { - $chart_type_ids = self::$db->fetch_column( - 'SELECT chart_type_id - FROM music.chart_type - ORDER BY chart_type_id ASC;', - [] - ); - $chart_types = []; - foreach ($chart_type_ids as $chart_type_id) { - $chart_types[] = self::getInstance($chart_type_id); + + /** + * Retrieves the releases made under this chart type. + * + * @return array The chart releases. + */ + public function getReleases() + { + $chart_releases = []; + foreach ($this->chart_release_ids as $chart_release_id) { + $chart_releases[] = MyRadio_ChartRelease::getInstance($chart_release_id, $this); + } + + return $chart_releases; + } + + /** + * Sets the name of this chart type. + * + * @param string $name The new name of the chart type. + * + * @return This object, for method chaining. + */ + public function setName($name) + { + if (empty($name)) { + throw new MyRadioException('Chart type name must not be empty!'); + } + + $this->name = $name; + self::$db->query( + 'UPDATE music.chart_type + SET name = $1 + WHERE chart_type_id = $2;', + [$name, $this->getID()] + ); + + return $this; } - return $chart_types; - } - - /** - * Retrieves the unique ID of this chart type. - * - * @return The chart type ID. - */ - public function getID() { - return $this->chart_type_id; - } - - /** - * Retrieves the number of releases made under this chart type. - * - * @return int The release count. - */ - public function getNumberOfReleases() { - return sizeof($this->chart_release_ids); - } - - /** - * Retrieves the releases made under this chart type. - * - * @return array The chart releases. - */ - public function getReleases() { - $chart_releases = []; - foreach ($this->chart_release_ids as $chart_release_id) { - $chart_releases[] = MyRadio_ChartRelease::getInstance($chart_release_id, $this); + + /** + * Sets the description of this chart type. + * + * @param string $description The new description of the chart type. + * + * @return This object, for method chaining. + */ + public function setDescription($description) + { + if (empty($description)) { + throw new MyRadioException('Chart type description must not be empty!'); + } + + $this->description = $description; + self::$db->query( + 'UPDATE music.chart_type + SET description = $1 + WHERE chart_type_id = $2;', + [$description, $this->getID()] + ); + + return $this; } - return $chart_releases; - } - - /** - * Sets the name of this chart type. - * - * @param string $name The new name of the chart type. - * - * @return This object, for method chaining. - */ - public function setName($name) { - if (empty($name)) { - throw new MyRadioException('Chart type name must not be empty!'); + + public static function getForm() + { + $form = ( + new MyRadioForm( + 'charts_editcharttype', + 'Charts', + 'editChartType', + ['title' => 'Edit Chart Type'] + ) + )->addField( + new MyRadioFormField( + 'name', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Identifier', + 'explanation' => 'What the chart will be referred to in the website code.', + ] + ) + )->addField( + new MyRadioFormField( + 'description', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Name', + 'explanation' => 'What the chart will be called on the website itself.', + ] + ) + ); + + return $form; } - $this->name = $name; - self::$db->query( - 'UPDATE music.chart_type - SET name = $1 - WHERE chart_type_id = $2;', - [$name, $this->getID()] - ); - - return $this; - } - - /** - * Sets the description of this chart type. - * - * @param string $description The new description of the chart type. - * - * @return This object, for method chaining. - */ - public function setDescription($description) { - if (empty($description)) { - throw new MyRadioException('Chart type description must not be empty!'); + public function getEditForm() + { + return self::getForm() + ->editMode( + $this->getID(), + [ + 'name' => $this->getName(), + 'description' => $this->getDescription(), + ] + ); } - $this->description = $description; - self::$db->query( - 'UPDATE music.chart_type - SET description = $1 - WHERE chart_type_id = $2;', - [$description, $this->getID()] - ); - - return $this; - } - - /** - * Converts this chart type to a table data source. - * - * @return array The object as a data source. - */ - public function toDataSource() { - return [ - 'name' => $this->getName(), - 'description' => $this->getDescription(), - 'releases' => [ - 'display' => 'text', - 'value' => $this->getNumberOfReleases(), - 'title' => 'Click to see releases for this chart type.', - 'url' => CoreUtils::makeURL( - 'Charts', - 'listChartReleases', - ['chart_type_id' => $this->getID()] - ) - ], - 'editlink' => [ - 'display' => 'icon', - 'value' => 'script', - 'title' => 'Edit Chart Type', - 'url' => CoreUtils::makeURL( - 'Charts', - 'editChartType', - ['chart_type_id' => $this->getID()] - ) - ], - ]; - } + /** + * Converts this chart type to a table data source. + * @param array $mixins Mixins. + * @return array The object as a data source. + */ + public function toDataSource($mixins = []) + { + return [ + 'name' => $this->getName(), + 'description' => $this->getDescription(), + 'releases' => [ + 'display' => 'text', + 'value' => $this->getNumberOfReleases(), + 'title' => 'Click to see releases for this chart type.', + 'url' => URLUtils::makeURL( + 'Charts', + 'listChartReleases', + ['chart_type_id' => $this->getID()] + ), + ], + 'editlink' => [ + 'display' => 'icon', + 'value' => 'pencil', + 'title' => 'Edit Chart Type', + 'url' => URLUtils::makeURL( + 'Charts', + 'editChartType', + ['chart_type_id' => $this->getID()] + ), + ], + ]; + } } diff --git a/src/Classes/ServiceAPI/MyRadio_Creditable.php b/src/Classes/ServiceAPI/MyRadio_Creditable.php index 2f5e2cf68..79352150d 100644 --- a/src/Classes/ServiceAPI/MyRadio_Creditable.php +++ b/src/Classes/ServiceAPI/MyRadio_Creditable.php @@ -1,8 +1,11 @@ - * @author Matt Windsor - * @package MyRadio_Core - * @uses \Database + * @uses \Database */ -trait MyRadio_Creditable { - protected $credits = array(); - protected static $credit_names; - - /** - * Get all credits - * @param MyRadio_Metadata_Common $parent Used when there is inheritance enabled - * for this object. In this case credits are merged. - * @return type - */ - public function getCredits($parent = null) { - $parent = $parent === null ? [] : $parent->getCredits(); - $current = empty($this->credits) ? [] : $this->credits; - return array_unique(array_merge($current, $parent), SORT_REGULAR); - } - - /** - * Returns an Array of Arrays containing Credit names and roles, or just - * name. - * - * @param boolean $types If true return an array with the role as well. - * Otherwise just return the credit. - * - * @return type - */ - public function getCreditsNames($types = true) { - $return = array(); - foreach ($this->credits as $credit) { - if ($types) { - $credit['name'] = MyRadio_User::getInstance($credit['memberid'])->getName(); - $credit['type_name'] = self::getCreditName($credit['type']); - } else { - $credit = MyRadio_User::getInstance($credit['memberid'])->getName(); - } - $return[] = $credit; +trait MyRadio_Creditable +{ + protected $owner; + protected $credits = []; + protected static $credit_names; + + /** + * Get all credits. + * + * @param MyRadio_Metadata_Common $parent Used when there is inheritance enabled + * for this object. In this case credits are merged. + * + * @return type + */ + public function getCredits(\MyRadio\ServiceAPI\MyRadio_Metadata_Common $parent = null) + { + $parent = empty($parent) ? [] : $parent->getCredits(); + $current = empty($this->credits) ? [] : $this->credits; + + return array_values(array_unique(array_merge($current, $parent), SORT_REGULAR)); } - return $return; - } - - /** - * Similar to getCredits, but only returns the User objects. This means the - * loss of the credit type in the result. - */ - public function getCreditObjects($parent = null) { - $r = array(); - foreach ($this->getCredits($parent) as $credit) { - $r[] = $credit['User']; + + /** + * Returns an Array of Arrays containing Credit names and roles, or just + * name. + * + * @param bool $types If true return an array with the role as well. + * Otherwise just return the credit. + * + * @return type + */ + public function getCreditsNames($types = true) + { + $return = []; + foreach ($this->credits as $credits) { + if ($types) { + $credit['name'] = MyRadio_User::getInstance($credits['memberid'])->getName(); + $credit['type_name'] = self::getCreditName($credits['type']); + } else { + $credit = MyRadio_User::getInstance($credits['memberid'])->getName(); + } + $return[] = $credit; + } + + return $return; } - return $r; - } - - /** - * Gets the presenter credits for as a comma-delimited string. - * - * @return String - */ - public function getPresenterString() { - $str = ''; - foreach ($this->getCredits() as $credit) { - if ($credit['type'] !== 1) { - continue; - } else { - $str .= $credit['User']->getName().', '; - } + + /** + * Similar to getCredits, but only returns the User objects. This means the + * loss of the credit type in the result. + */ + public function getCreditObjects($parent = null) + { + $r = []; + foreach ($this->getCredits($parent) as $credit) { + $r[] = $credit['User']; + } + + return $r; } - return empty($str) ? '' : substr($str, 0, -2); - } - - /** - * Updates the list of Credits. - * - * Existing credits are kept active, ones that are not in the new list are - * set to effective_to now, and ones that are in the new list but not exist - * are created with effective_from now. - * - * @param User[] $users An array of Users associated. - * @param int[] $credittypes The relevant credittypeid for each User. - */ - public function setCredits($users, $credittypes, $table, $pkey) { - //Start a transaction, atomic-like. - self::$db->query('BEGIN'); - - $newcredits = $this->mergeCreditArrays($users, $credittypes); - $oldcredits = $this->getCredits(); - - $this->removeOldCredits($oldcredits, $newcredits, $table, $pkey); - $this->addNewCredits($oldcredits, $newcredits, $table, $pkey); - $this->updateLocalCredits($newcredits); - - //Oh, and commit the transaction. I always forget this. - self::$db->query('COMMIT'); - - return $this; - } - - /* - * Merges two parallel credit arrays into one array of credits. - * - * @param array $users The array of incoming credit users. - * @param array $types The array of incoming credit types. - * - * @return array The merged credit array. - */ - private function mergeCreditArrays($users, $types) { - return array_filter( - array_map( - function($user, $type) { - return (empty($user) || empty($type)) - ? null - : [ 'User' => $user, 'type' => $type, 'memberid' => $user->getID() ]; - }, - $users, - $types - ), - function($credit) { return !empty($credit); } - ); - } - - /** - * De-activates any credits that are not in the incoming credits set. - * - * @param array $old The array of existing credits. - * @param array $new The array of incoming credits. - * @param string $table The database table to update. - * @param string $pkey The primary key of the object to update. - * - * @return null Nothing. - */ - private function removeOldCredits($old, $new, $table, $pkey) { - foreach ($old as $credit) { - if (!in_array($credit, $new)) { - self::$db->query( - 'UPDATE '.$table.' SET effective_to=NOW()' - . 'WHERE '.$pkey.'=$1 AND creditid=$2 AND credit_type_id=$3', - [$this->getID(), $credit['User']->getID(), $credit['type']], - true - ); - } + /** + * Checks the current user is in the credits for the creditable item + * @return boolean The user is an owner + */ + public function isCurrentUserAnOwner() + { + if ($this->owner === $_SESSION['memberid']) { + return true; + } + foreach ($this->getCreditObjects() as $user) { + if ($user->getID() === $_SESSION['memberid']) { + return true; + } + } + + return false; } - } - - /** - * Creates any new credits that are not in the existing credits set. - * - * @param array $old The array of existing credits. - * @param array $new The array of incoming credits. - * @param string $table The database table to update. - * @param string $pkey The primary key of the object to update. - * - * @return null Nothing. - */ - private function addNewCredits($old, $new, $table, $pkey) { - foreach ($new as $credit) { - //Look for an existing credit - if (!in_array($credit, $old)) { - //Doesn't seem to exist. - self::$db->query( - 'INSERT INTO '.$table.' ('.$pkey.', credit_type_id, creditid, effective_from,' - . 'memberid, approvedid) VALUES ($1, $2, $3, NOW(), $4, $4)', - [ - $this->getID(), - $credit['type'], - $credit['memberid'], - MyRadio_User::getCurrentOrSystemUser()->getID() - ], - true + + /** + * Gets the presenter credits for as a comma-delimited string. + * + * @return string + */ + public function getPresenterString() + { + $credit_types = MyRadio_Scheduler::getCreditTypes(); + $credit_types_in_byline = []; + foreach ($credit_types as $type) { + if ($type["is_in_byline"] == "t") { + $credit_types_in_byline[] = $type["value"]; + } + } + $str = ''; + foreach ($this->getCredits() as $credit) { + if (in_array($credit['type'], $credit_types_in_byline)) { + $str .= $credit['User']->getName().', '; + } else { + continue; + } + } + + return empty($str) ? '' : substr($str, 0, -2); + } + + /** + * Updates the list of Credits. + * + * Existing credits are kept active, ones that are not in the new list are + * set to effective_to now, and ones that are in the new list but not exist + * are created with effective_from now. + * + * @param User[] $users An array of Users associated. + * @param int[] $credittypes The relevant credittypeid for each User. + */ + public function setCredits($users, $credittypes, $table, $pkey) + { + //Start a transaction, atomic-like. + self::$db->query('BEGIN'); + + $newcredits = $this->mergeCreditArrays($users, $credittypes); + $oldcredits = $this->getCredits(); + + $this->removeOldCredits($oldcredits, $newcredits, $table, $pkey); + $this->addNewCredits($oldcredits, $newcredits, $table, $pkey); + $this->updateLocalCredits($newcredits); + + //Oh, and commit the transaction. I always forget this. + self::$db->query('COMMIT'); + + return $this; + } + + /* + * Merges two parallel credit arrays into one array of credits. + * + * @param array $users The array of incoming credit users. + * @param array $types The array of incoming credit types. + * + * @return array The merged credit array. + */ + private function mergeCreditArrays($users, $types) + { + return array_filter( + array_map( + function ($user, $type) { + return (empty($user) || empty($type)) + ? null + : ['User' => $user, 'type' => $type, 'memberid' => $user->getID()]; + }, + $users, + $types + ), + function ($credit) { + return !empty($credit); + } ); - } } - } - - /** - * Updates the local credits cache for this object. - * - * @param array $new The array of incoming credits - * @param array $types The array of incoming credit types. - * - * @return null Nothing. - */ - private function updateLocalCredits($new) { - $this->credits = $new; - } - - protected static function getCreditName($credit_id) { - if (empty(self::$credit_names)) { - $r = self::$db->fetch_all('SELECT credit_type_id, name FROM people.credit_type'); - - foreach ($r as $v) { - self::$credit_names[$v['credit_type_id']] = $v['name']; - } + + /** + * De-activates any credits that are not in the incoming credits set. + * + * @param array $old The array of existing credits. + * @param array $new The array of incoming credits. + * @param string $table The database table to update. + * @param string $pkey The primary key of the object to update. + */ + private function removeOldCredits($old, $new, $table, $pkey) + { + foreach ($old as $credit) { + if (!in_array($credit, $new)) { + self::$db->query( + 'UPDATE '.$table.' SET effective_to=NOW()' + .' WHERE '.$pkey.'=$1 AND creditid=$2 AND credit_type_id=$3' + .' AND effective_to IS NULL', + [$this->getID(), $credit['User']->getID(), $credit['type']], + true + ); + } + } + } + + /** + * Creates any new credits that are not in the existing credits set. + * + * @param array $old The array of existing credits. + * @param array $new The array of incoming credits. + * @param string $table The database table to update. + * @param string $pkey The primary key of the object to update. + */ + private function addNewCredits($old, $new, $table, $pkey) + { + foreach ($new as $credit) { + //Look for an existing credit + if (!in_array($credit, $old)) { + //Doesn't seem to exist. + self::$db->query( + 'INSERT INTO '.$table.' ('.$pkey.', credit_type_id, creditid, effective_from,' + .'memberid, approvedid) VALUES ($1, $2, $3, NOW(), $4, $4)', + [ + $this->getID(), + $credit['type'], + $credit['memberid'], + MyRadio_User::getCurrentOrSystemUser()->getID(), + ], + true + ); + } + } + } + + /** + * Updates the local credits cache for this object. + * + * @param array $new The array of incoming credits + * @param array $types The array of incoming credit types. + */ + private function updateLocalCredits($new) + { + $this->credits = $new; } - return empty(self::$credit_names[$credit_id]) ? 'Contrib' : self::$credit_names[$credit_id]; - } + protected static function getCreditName($credit_id) + { + if (empty(self::$credit_names)) { + $r = self::$db->fetchAll('SELECT credit_type_id, name FROM people.credit_type'); + + foreach ($r as $v) { + self::$credit_names[$v['credit_type_id']] = $v['name']; + } + } + + return empty(self::$credit_names[$credit_id]) ? 'Contrib' : self::$credit_names[$credit_id]; + } } -?> diff --git a/src/Classes/ServiceAPI/MyRadio_Demo.php b/src/Classes/ServiceAPI/MyRadio_Demo.php index 23df5e872..d49e23c30 100644 --- a/src/Classes/ServiceAPI/MyRadio_Demo.php +++ b/src/Classes/ServiceAPI/MyRadio_Demo.php @@ -1,123 +1,642 @@ - * @version 20130607 - * @package MyRadio_Demo - * @uses \Database + * @uses \Database */ -class MyRadio_Demo extends MyRadio_Metadata_Common { +class MyRadio_Demo extends ServiceAPI +{ + + private $demo_id; + private $demo_time; + private $demo_link; + private $presenterstatusid; + private $memberid; + private $signup_cutoff_hours; + private $demo_max_participants; + + protected function __construct($demoid) + { + $this->demo_id = (int) $demoid; + + self::initDB(); + + $result = self::$db->fetchOne( + "SELECT * FROM schedule.demo WHERE demo_id = $1", + [$demoid] + ); + + if (empty($result)) { + throw new MyRadioException("The specified demo " . $demoid . "doesn't exist."); + return; + } + + $this->demo_time = $result['demo_time']; + $this->demo_link = $result['demo_link']; + $this->demo_max_participants = $result['max_participants']; + $this->presenterstatusid = $result['presenterstatusid']; + $this->memberid = $result["memberid"]; + $this->signup_cutoff_hours = (int) $result['signup_cutoff_hours']; + } + + public function getID() + { + return $this->demo_id; + } + + public static function registerDemo($time, $training_type, $max_participants, $link = null, $signup_cutoff_hours = 0) + { + if ($time == null || $training_type == null || !is_numeric($time)) { + throw new MyRadioException("A training demo must have a time and training date.", 400); + } else { + try { + $training = MyRadio_TrainingStatus::getInstance($training_type); + + self::initDB(); + + self::$db->query( + "INSERT INTO schedule.demo (presenterstatusid, demo_time, demo_link, max_participants, memberid, signup_cutoff_hours) + VALUES ($1, $2, $3, $4, $5, $6)", + [$training_type, CoreUtils::getTimestamp($time), $link, $max_participants, $_SESSION["memberid"], $signup_cutoff_hours] + ); + date_default_timezone_set(Config::$timezone); + + // Let people waiting for this know + $waiters = self::trainingWaitingList($training_type); + foreach ($waiters as $waiter) { + $user = MyRadio_User::getInstance($waiter['memberid']); + MyRadioEmail::sendEmailToUser( + $user, + "Available Training Session", + "Hi " . $user->getFName() . "," + . "\r\n\r\n A session to get you " . $training->getTitle() + . " has opened up. Check it out on MyRadio.\r\n\r\n" + . URLUtils::makeURL('Training', 'listDemos') . "\r\n\r\n" + . Config::$long_name . " Training" + ); + } + } catch (MyRadioException $e) { + throw $e; + } + } + return true; + } + + public function editDemo($time, $training_type, $max_participants, $link = null, $signup_cutoff_hours = 0) + { + + // TODO, Only edit your training demos, or any if you have perms + // TODO: Allow changing demoer + + if ($time == null || $training_type == null || $max_participants == null) { + throw new MyRadioException( + "A training demo must have a time, training date and maximum number of participants.", + 400); + } else { + if ($time != $this->demo_time || $link != $this->demo_link || $training_type != $this->presenterstatusid) { + // Do the Update + self::$db->query( + "UPDATE schedule.demo SET demo_time = $1, + demo_link = $2, + presenterstatusid = $3, + signup_cutoff_hours = $4, + max_participants = $5 + WHERE demo_id = $6", + [CoreUtils::getTimestamp($time), $link, $training_type, $signup_cutoff_hours, $max_participants, $this->getID()] + ); + // Email People + $attendees = $this->myRadioUsersAttendingDemo(); + $attendees[] = $this->getDemoer(); + + // Work out what to tell people re. where their training is + if ($link == null && $this->demo_link != null) { + $demo_location = "It is now in person at our studios in Vanbrugh College."; + } elseif ($link != null && $this->demo_link == null) { + $demo_location = "It is now online and will be hosted at " . $link; + } elseif ($link != null) { + $demo_location = "It will now be at " . $link; + } else { + $demo_location = ""; + } + + foreach ($attendees as $attendee) { + MyRadioEmail::sendEmailToUser( + $attendee, + "Updated Training Session", + "Hi " . $attendee->getFName() + . "\r\n\r\n There's been a change to your training session on " . $this->demo_time + . ".\r\n\r\n" + . ($time != $this->demo_time ? "It is now at " + . CoreUtils::happyTime($time) . ".\r\n\r\n" : "") + . $demo_location . "\r\n\r\n" + . Config::$long_name . " Training Team" + ); + } + $this->demo_link = $link; + $this->demo_time = CoreUtils::getTimestamp($time); + $this->presenterstatusid = $training_type; + $this->signup_cutoff_hours = $signup_cutoff_hours; + $this->max_participants = $max_participants; + $this->updateCacheObject(); + } + } + } + + public static function getForm() + { + return (new MyRadioForm( + 'sched_demo', + 'Training', + 'createDemo', + [ + 'title' => 'Training', + 'subtitle' => 'Create Training Session', + ] + ))->addField( + new MyRadioFormField( + 'demo_training_type', + MyRadioFormField::TYPE_SELECT, + [ + "label" => "Training Type", + "options" => MyRadio_TrainingStatus::getOptionsToTrain(MyRadio_User::getCurrentUser()) + ] + ) + )->addField( + new MyRadioFormField( + 'demo_datetime', + MyRadioFormField::TYPE_DATETIME, + ['label' => 'Date and Time of the session'] + ) + )->addField( + new MyRadioFormField( + 'demo_link', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => "Zoom/Google Meets Link (Optional)", + "required" => false + ] + ) + )->addField( + new MyRadioFormField( + 'signup_cutoff_hours', + MyRadioFormField::TYPE_NUMBER, + [ + 'label' => "Signup Cutoff (Hours)", + 'explanation' => 'If set, people will only be able to join this many hours before your session starts. Set to 0 to disable.', + "required" => false, + 'options' => [ + 'min' => 0, + ], + 'value' => 0, + 'required' => true + ] + ) + )->addField( + new MyRadioFormField( + 'demo_max_participants', + MyRadioFormField::TYPE_NUMBER, + [ + 'label' => "Attendee Limit", + 'explanation' => "Maximum number of people who can sign up to training session", + 'options' => [ + 'min' => 1, + 'max' => 4, + ], + 'value' => 2, + 'required' => true + ] + ) + ); + } + + public function getEditForm() + { + return self::getForm() + ->setSubtitle("Edit Training Session") + ->editMode( + $this->getID(), + [ + "demo_training_type" => $this->getTrainingType()->getID(), + "demo_datetime" => CoreUtils::happyTime($this->getDemoTime()), + "demo_link" => $this->getLink(), + "demo_max_participants" => $this->demo_max_participants, + 'signup_cutoff_hours' => $this->signup_cutoff_hours, + ] + ); + } + + public function isUserAttendingDemo($userid) + { + $r = self::$db->fetchColumn( + "SELECT (1) FROM schedule.demo_attendee + WHERE demo_id = $1 AND memberid = $2", + [$this->demo_id, $userid] + ); + return count($r) > 0; + } + + public function isSpaceOnDemo(): bool + { + return $this->attendingDemoCount() < $this->demo_max_participants; + } + + public function tooCloseToStart() + { + if ($this->signup_cutoff_hours === 0) { + return false; + } + return time() > (strtotime($this->demo_time) - ($this->signup_cutoff_hours * 60 * 60)); + } + + // Grrr...this returns names, not users. + public function usersAttendingDemo() + { + // First, retrieve all the memberids attending this demo + + $r = self::$db->fetchColumn( + "SELECT memberid FROM schedule.demo_attendee + WHERE demo_id = $1", + [$this->demo_id] + ); + + if (empty($r)) { + return 'Nobody'; + } + $str = MyRadio_User::getInstance($r[0])->getName(); + if (isset($r[1])) { + $str .= ', ' . MyRadio_User::getInstance($r[1])->getName(); + } + + return $str; + } + + /** + * @return MyRadio_User[] + */ + public function myRadioUsersAttendingDemo() + { + $r = self::$db->fetchColumn( + "SELECT memberid FROM schedule.demo_attendee + WHERE demo_id = $1", + [$this->demo_id] + ); + $attendees = []; + foreach ($r as $attendee) { + $attendees[] = MyRadio_User::getInstance($attendee); + } + return $attendees; + } + + public function attendingDemoCount() + { + return count(self::$db->fetchColumn( + "SELECT memberid FROM schedule.demo_attendee WHERE demo_id = $1", + [$this->demo_id] + )); + } - public static function registerDemo($time) { - self::initDB(); - date_default_timezone_set('UTC'); - /** - * Check for conflicts + * Gets a list of available demo slots in the future. */ - $r = MyRadio_Scheduler::getScheduleConflict($time, $time+3600); - if (!empty($r)) { - //There's a conflict - throw new MyRadioException('There is already something scheduled at that time', MyRadioException::FATAL); - return false; + public static function listDemos() + { + self::initDB(); + + $result = self::$db->fetchAll( + "SELECT demo_id, demo_link, presenterstatusid, demo_time, memberid FROM schedule.demo + WHERE demo_time > NOW() ORDER BY demo_time ASC" + ); + + $demos = []; + foreach ($result as $demo) { + $demo['demo_time'] = date('D d M H:i', strtotime($demo['demo_time'])); + $demo['member'] = MyRadio_User::getInstance($demo['memberid'])->getName(); + $demo['memberid'] = (int)$demo['memberid']; + $demo['presenterstatusid'] = MyRadio_TrainingStatus::getInstance($demo['presenterstatusid'])->getTitle(); + + $demos[] = $demo; + } + + return $demos; } - + /** - * Demos use the timeslot member as the credit for simplicity + * Gets a list of available demo slots in the future, including more details about the slot, suitable for the new signups form */ - self::$db->query('INSERT INTO schedule.show_season_timeslot (show_season_id, start_time, memberid, approvedid, duration) - VALUES (0, $1, $2, $2, \'01:00:00\')', array(CoreUtils::getTimestamp($time), $_SESSION['memberid'])); - date_default_timezone_set(Config::$timezone); - return true; - } - - public static function attendingDemo($demoid) { - if (MyRadio_User::getInstance()->hasAuth(AUTH_ADDDEMOS)) { - $r = self::$db->fetch_column('SELECT creditid FROM schedule.show_credit WHERE show_id = 0 AND effective_from=$1 AND credit_type_id=7', array(self::getDemoTime($demoid))); - if (empty($r)) return 'Nobody'; - $str = MyRadio_User::getInstance($r[0])->getName(); - if (isset($r[1])) { - $str .= ', '.MyRadio_User::getInstance($r[1])->getName(); - } - return $str; - } else { - if (self::attendingDemoCount($demoid) < 2) { - return 'Space Available!'; - } else { - return 'Full'; - } - } - } - - public static function attendingDemoCount($demoid) { - return self::$db->num_rows(self::$db->query('SELECT creditid FROM schedule.show_credit WHERE show_id = 0 AND effective_from=$1 AND credit_type_id=7', array(self::getDemoTime($demoid)))); - } - - /** - * Gets a list of available demo slots in the future - */ - public static function listDemos() { - self::initDB(); - $result = self::$db->fetch_all('SELECT show_season_timeslot_id, start_time, memberid FROM schedule.show_season_timeslot - WHERE show_season_id = 0 AND start_time > NOW() ORDER BY start_time ASC'); - - //Add the credits for each member - $demos = array(); - foreach ($result as $demo) { - $demo['start_time'] = date('d M H:i', strtotime($demo['start_time'])); - $demo['memberid'] = MyRadio_User::getInstance($demo['memberid'])->getName(); - $demos[] = array_merge($demo, array('attending' => self::attendingDemo($demo['show_season_timeslot_id']))); - } - - return $demos; - } - - /** - * The current user is marked as attending a demo - * Return 0: Success - * Return 1: Demo Full - * Return 2: Already Attending a Demo - */ - public static function attend($demoid) { - self::initDB(); - //Get # of attendees - if (self::attendingDemoCount($demoid) >= 2) return 1; - - //Check they aren't already attending one in the next week - if (self::$db->num_rows(self::$db->query('SELECT creditid FROM schedule.show_credit WHERE show_id=0 AND creditid=$1 - AND effective_from >= NOW() AND effective_from <= (NOW() + INTERVAL \'1 week\') LIMIT 1', array($_SESSION['memberid']))) === 1) { - return 2; - } - - self::$db->query('INSERT INTO schedule.show_credit (show_id, credit_type_id, creditid, effective_from, effective_to, memberid, approvedid) VALUES - (0, 7, $1, $2, $2, $1, $1)', array($_SESSION['memberid'], self::getDemoTime($demoid))); - $time = self::getDemoTime($demoid); - $user = self::getDemoer($demoid); - $attendee = MyRadio_User::getInstance(); - MyRadioEmail::sendEmailToUser($user, 'New Demo Attendee', $attendee->getName().' has joined your demo at '.$time.'.'); - MyRadioEmail::sendEmailToUser($attendee, 'Attending Demo', 'Hi '.$attendee->getFName(). - ",\r\n\r\nThanks for joining a demo at $time. You will be demoed by ".$user->getName(). - '. Just head over to the station in Vanbrugh College just before your demo and the trainer will be waiting for you.' - ."\r\n\r\nSee you on air soon!\r\n".Config::$long_name." Training"); - return 0; - } - - public static function getDemoTime($demoid) { - self::initDB(); - $r = self::$db->fetch_column('SELECT start_time FROM schedule.show_season_timeslot WHERE show_season_timeslot_id=$1', array($demoid)); - return $r[0]; - } - - public static function getDemoer($demoid) { - self::initDB(); - $r = self::$db->fetch_column('SELECT memberid FROM schedule.show_season_timeslot WHERE show_season_timeslot_id=$1', array($demoid)); - return MyRadio_User::getInstance($r[0]); - } + public static function listDemosForSignup() + { + self::initDB(); + + $result = self::$db->fetchAll( + "SELECT schedule.demo.demo_id, demo_link, presenterstatusid, demo_time, schedule.demo.memberid, signup_cutoff_hours, max_participants, COUNT(schedule.demo_attendee.memberid) AS attendee_count FROM schedule.demo + LEFT JOIN schedule.demo_attendee ON schedule.demo.demo_id = schedule.demo_attendee.demo_id + WHERE demo_time > NOW() GROUP BY schedule.demo.demo_id ORDER BY demo_time ASC" + ); + $demos = []; + foreach ($result as $demo) { + $demo_time = strtotime($demo['demo_time']); + $demo['demo_time'] = date('D d M H:i', $demo_time); + $demo['demo_time_'] = $demo_time; + $demo['member'] = MyRadio_User::getInstance($demo['memberid'])->getName(); + $demo['memberid'] = (int)$demo['memberid']; + $demo['presenterstatusid'] = MyRadio_TrainingStatus::getInstance($demo['presenterstatusid'])->getTitle(); + $demo['signup_cutoff_hours'] = (int)$demo['signup_cutoff_hours']; + $demo['max_participants'] = (int)$demo['max_participants']; + $demo['attendee_count'] = (int)$demo['attendee_count']; + + $demos[] = $demo; + } + + return $demos; + } + + /** + * Deletes this demo and all attendees (if any). + * @param bool $deleteWithAttendees if true, will remove all attendees (and email them). + * @return void + * @throws MyRadioException with code 409 if the demo has attendees and $deleteWithAttendees is false + */ + public function delete(bool $deleteWithAttendees = false) + { + self::$db->query('BEGIN'); + $attendees = $this->attendingDemoCount(); + if ($attendees > 0) { + if (!$deleteWithAttendees) { + self::$db->query('ROLLBACK'); + throw new MyRadioException( + 'This demo has attendees.', + 409 + ); + } + foreach ($this->myRadioUsersAttendingDemo() as $user) { + $time = CoreUtils::happyTime($this->getDemoTime()); + $name = $user->getFName(); + $listLink = URLUtils::makeURL('Training', 'listDemos'); + $stationName = Config::$long_name; + MyRadioEmail::sendEmailToUser($user, 'Training Session Cancelled', + "Hi $name,
    " . + "The training session you were due to attend at $time has been cancelled.
    " . + "Please check the
    training schedule to see if there are any other sessions you can attend.
    " . + "Thanks,
    ". + "$stationName Training" + ); + } + self::$db->query( + 'DELETE FROM schedule.demo_attendee WHERE demo_id = $1', + [$this->demo_id] + ); + } + self::$db->query( + 'DELETE FROM schedule.demo WHERE demo_id = $1', + [$this->demo_id] + ); + self::$db->query('COMMIT'); + self::$cache->delete(self::getCacheKey($this->demo_id)); + } + + /** + * The current user is marked as attending a demo + * Return 0: Success + * Return 1: Demo Full + * Return 2: Already Attending a Demo. + * Return 3: Too Late. + */ + public function attend() + { + return $this->addAttendee($_SESSION['memberid']); + } + + /** + * The passed user is marked as attending a demo + * Return 0: Success + * Return 1: Demo Full + * Return 2: Already Attending a Demo. + * Return 3: Too Late. + */ + public function addAttendee(int $userid) + { + if (!$this->isSpaceOnDemo()) { + return 1; + } + + //Check they aren't already attending one in the next week + if (count(self::$db->fetchColumn( + "SELECT demo_id FROM schedule.demo_attendee + INNER JOIN schedule.demo USING (demo_id) + WHERE schedule.demo_attendee.memberid = $1 + AND demo_time <= (NOW() + INTERVAL '1 week') + AND presenterstatusid = $2", + [$userid, $this->presenterstatusid] + )) !== 0) { + return 2; + } + + if ($this->tooCloseToStart()) { + return 3; + } + + self::$db->query( + "INSERT INTO schedule.demo_attendee + (demo_id, memberid) + VALUES ($1, $2)", + [$this->demo_id, $userid] + ); + + $user = $this->getDemoer(); + $attendee = MyRadio_User::getInstance($userid); + MyRadioEmail::sendEmailToUser( + $user, + 'New Training Attendee', + $attendee->getName() . ' has joined your session at ' . $this->getDemoTime() . '.' + . ($this->getLink() ? " The training session is at " . $this->getLink() : "") + ); + MyRadioEmail::sendEmailToUser( + $attendee, + 'Attending Training', + 'Hi ' + . $attendee->getFName() . ",\r\n\r\n" + . "Thanks for joining a training session at " . $this->getDemoTime() . ". You will be trained by " + . $user->getName() + . ($this->getLink() ? '. The training session will be available at ' + . $this->getLink() : '. Just head over to the station in Vanbrugh College just before your slot ' + . 'and the trainer will be waiting for you.') + . "\r\n\r\nSee you on air soon!\r\n" + . Config::$long_name + . ' Training' + ); + + // Take off waiting list + if (self::onWaitingList($this->presenterstatusid, $userid)) { + self::leaveWaitingList($this->presenterstatusid, $userid); + } + + return 0; + } + + /** + * The current user is unmarked as attending a demo. + * Returns 0: successful + * Returns 3: too late, need to contact trainer + */ + public function leave() + { + if ($this->tooCloseToStart()) { + return 3; + } + + self::$db->query( + "DELETE FROM schedule.demo_attendee + WHERE demo_id = $1 AND memberid = $2", + [$this->demo_id, $_SESSION['memberid']] + ); + + $attendee = MyRadio_User::getInstance(); + MyRadioEmail::sendEmailToUser( + $this->getDemoer(), + 'Training Attendee Left', + $attendee->getName() . ' has left your session at ' . $this->getDemoTime() . '.' + ); + MyRadioEmail::sendEmailToUser( + $attendee, + 'Training Cancellation', + 'Hi ' . $attendee->getFName() . ",\r\n\r\n" + . "Just to confirm that you have left the training session at " . $this->getDemoTime() + . ". If this was accidental, simply rejoin. " + . "Meanwhile, you can join the waiting list, and we'll let you know if a session becomes available. " + . URLUtils::makeURL("Training", "listWaitingLists") + . "\r\n\r\nThanks!\r\n" + . Config::$long_name + . ' Training' + ); + + + return 0; + } + + public function getDemoTime() + { + return $this->demo_time; + } + + public function getDemoer() + { + return MyRadio_User::getInstance($this->memberid); + } + + public function getLink() + { + return $this->demo_link; + } + + public function getTrainingType() + { + return MyRadio_TrainingStatus::getInstance($this->presenterstatusid); + } + + public function markTrained() + { + $attendees = $this->myRadioUsersAttendingDemo(); + foreach ($attendees as $attendee) { + MyRadio_UserTrainingStatus::create( + $this->getTrainingType(), + $attendee, + MyRadio_User::getInstance($_SESSION['memberid']) + ); + } + } + + public static function joinWaitingList($presenterstatusid) + { + self::addToWaitingList($presenterstatusid, $_SESSION['memberid']); + } + + public static function addToWaitingList($presenterstatusid, int $userid) + { + self::initDB(); + + $r = self::$db->fetchColumn( + "SELECT memberid FROM schedule.demo_waiting_list + WHERE memberid = $1 AND presenterstatusid = $2", + [$userid, $presenterstatusid] + ); + + if (count($r) != 0) { + // Already waiting for this training status + return 1; + } else { + self::$db->query( + "INSERT INTO schedule.demo_waiting_list (memberid, presenterstatusid, date_added) + VALUES ($1, $2, NOW())", + [$userid, $presenterstatusid] + ); + } + + return 0; + } + + public static function leaveWaitingList($presenterstatusid, int $userid = -1) + { + if (isset($_SESSION['memberid'])) { + $userid = $_SESSION['memberid']; + } + self::initDB(); + self::$db->query( + "DELETE FROM schedule.demo_waiting_list + WHERE memberid = $1 + AND presenterstatusid = $2", + [$userid, $presenterstatusid] + ); + } + + public static function userWaitingList(int $userid = -1) + { + if (isset($_SESSION['memberid'])) { + $userid = $_SESSION['memberid']; + } + self::initDB(); + return self::$db->fetchAll( + "SELECT presenterstatusid, date_added FROM schedule.demo_waiting_list + WHERE memberid = $1 + ORDER BY date_added ASC", + [$userid] + ); + } + + public static function trainingWaitingList($presenterstatusid) + { + self::initDB(); + return self::$db->fetchAll( + "SELECT memberid, date_added FROM schedule.demo_waiting_list + WHERE presenterstatusid = $1 + ORDER BY date_added ASC", + [$presenterstatusid] + ); + } + + public static function onWaitingList($presenterstatusid, int $userid = -1) + { + $list = self::userWaitingList($userid); + foreach ($list as $entry) { + if ($entry['presenterstatusid'] == $presenterstatusid) { + return true; + } + } + return false; + } } diff --git a/src/Classes/ServiceAPI/MyRadio_EmailDestination.php b/src/Classes/ServiceAPI/MyRadio_EmailDestination.php new file mode 100644 index 000000000..787a50187 --- /dev/null +++ b/src/Classes/ServiceAPI/MyRadio_EmailDestination.php @@ -0,0 +1,194 @@ +alias_id = $data['alias_id']; + $this->source = $data['source']; + $this->reason = $data['reason']; + $this->destination = $data['destination']; + } + + /** + * @return int|null + */ + public function getAliasId() + { + return $this->alias_id; + } + + public function getAlias() + { + if ($this->alias_id !== null) { + return MyRadio_Alias::getInstance($this->alias_id); + } + return null; + } + + /** + * @return string + */ + public function getSource() + { + return $this->source; + } + + /** + * "member", "officer", "personal", "team", "list_optin" or "list_auto" + * @return string + */ + public function getReason() + { + return $this->reason; + } + + /** + * @return mixed + */ + public function getDestination() + { + switch ($this->reason) { + case 'member': + case 'personal': + return MyRadio_User::getInstance($this->destination); + case 'officer': + return MyRadio_Officer::getInstance($this->destination); + case 'team': + return MyRadio_Team::getInstance($this->destination); + case 'list_optin': + case 'list_auto': + return MyRadio_List::getInstance($this->destination); + default: + $r = $this->reason; + throw new MyRadioException("Don't know how to get destination for reason $r"); + } + } + + + public static function getAllSourcesForUser(Database $database, int $memberid) + { + // Oh dear. + $rows = $database->fetchAll( + <<= NOW() + '28 days'::INTERVAL) +) +UNION ALL +SELECT alias_id, source || '@' || $2 AS source, 'list_optin' AS reason, destination FROM mail.alias +INNER JOIN mail.alias_list USING (alias_id) +WHERE destination IN ( + SELECT listid FROM public.mail_subscription WHERE memberid = $1 +) +UNION +SELECT NULL AS alias_id, listaddress || '@' || $2 AS source, 'list_optin' AS reason, listid AS destination +FROM mail_list +WHERE subscribable = 't' +AND $1 IN ( + SELECT memberid FROM mail_subscription WHERE listid = mail_list.listid +) +UNION +SELECT NULL as alias_id, + COALESCE(listaddress || '@' || $2, listname) AS source, + 'list_auto' AS reason, + listid AS destination +FROM mail_list +WHERE defn IS NOT NULL +AND $1 IN ( + SELECT memberid FROM mail.eval_list_sql( + REPLACE( + REPLACE( + mail_list.defn, + '%Y', + COALESCE( + ( + SELECT EXTRACT(year FROM start) + FROM public.terms + WHERE descr = 'Autumn' + AND EXTRACT(year FROM start) = EXTRACT(year FROM NOW()) + ), + ( + SELECT + EXTRACT(year FROM start) + FROM public.terms + WHERE descr = 'Autumn' + AND EXTRACT(year FROM start) = EXTRACT(year FROM NOW()) - 1 + ) + )::TEXT + ), + '%LISTID', + mail_list.listid::text + ) + ) +) +AND $1 NOT IN ( + SELECT memberid FROM mail_subscription WHERE listid = mail_list.listid +) +AND COALESCE(listaddress, listname) NOT IN (SELECT local_alias FROM public.team) +UNION +SELECT NULL AS alias_id, officer_alias || '@' || $2 AS listname, 'officer' AS reason, officerid AS source +FROM public.officer +WHERE (status = 'c' OR status = 'h') +AND $1 IN ( + SELECT memberid FROM member_officer + WHERE officerid = officer.officerid + AND from_date <= NOW() + AND (till_date IS NULL OR till_date >= NOW() + '28 days'::INTERVAL) +) +UNION +SELECT null AS alias_id, local_alias || '@' || $2 AS listname, 'team' AS reason, teamid AS source +FROM public.team +WHERE team.local_alias IS NOT NULL +AND $1 IN ( + SELECT memberid FROM member_officer + INNER JOIN officer o on member_officer.officerid = o.officerid + WHERE o.teamid = team.teamid +) +AND (SELECT COUNT(*) FROM mail_list WHERE listaddress = local_alias) > 0 +UNION +SELECT NULL AS alias_id, local_alias || '@' || $2 AS listname, 'personal' AS reason, $1 AS source +FROM public.member +WHERE local_alias IS NOT NULL +AND memberid = $1 +UNION +SELECT NULL AS alias_id, local_name || '@' || $2 AS listname, 'personal' AS reason, $1 AS source +FROM public.member +WHERE local_name IS NOT NULL +AND memberid = $1 +SQL + // What maniac wrote this? + , + [$memberid, Config::$email_domain] + ); + $result = []; + foreach ($rows as $row) { + $result[] = new self($row); + } + return $result; + } +} diff --git a/src/Classes/ServiceAPI/MyRadio_Event.php b/src/Classes/ServiceAPI/MyRadio_Event.php new file mode 100644 index 000000000..3c293b050 --- /dev/null +++ b/src/Classes/ServiceAPI/MyRadio_Event.php @@ -0,0 +1,501 @@ +eventid = (int)$data['eventid']; + $this->title = $data['title']; + $this->descriptionHtml = $data['description_html']; + + $this->startTime = strtotime($data['start_time']); + $this->endTime = strtotime($data['end_time']); + + $this->hostId = (int)$data['hostid']; + } + + /** + * @return int + */ + public function getID(): int + { + return $this->eventid; + } + + /** + * @return string + */ + public function getTitle(): string + { + return $this->title; + } + + /** + * @param string $title + */ + public function setTitle(string $title): void + { + self::$db->query('UPDATE public.events + SET title = $2 + WHERE eventid = $1', [ + $this->getID(), + $title + ]); + $this->title = $title; + $this->updateCacheObject(); + } + + /** + * @param string $descriptionHtml + */ + public function setDescriptionHtml(string $descriptionHtml): void + { + self::$db->query('UPDATE public.events + SET description_html = $2 + WHERE eventid = $1', [ + $this->getID(), + $descriptionHtml + ]); + $this->descriptionHtml = $descriptionHtml; + $this->updateCacheObject(); + } + + /** + * @param int $startTime + */ + public function setStartTime(int $startTime): void + { + self::$db->query('UPDATE public.events + SET start_time = $2 + WHERE eventid = $1', [ + $this->getID(), + CoreUtils::getTimestamp($startTime) + ]); + $this->startTime = $startTime; + $this->updateCacheObject(); + } + + /** + * @param int $endTime + */ + public function setEndTime(int $endTime): void + { + self::$db->query('UPDATE public.events + SET end_time = $2 + WHERE eventid = $1', [ + $this->getID(), + CoreUtils::getTimestamp($endTime) + ]); + $this->endTime = $endTime; + $this->updateCacheObject(); + } + + /** + * @param int + */ + public function setHostId(int $hostId): void + { + self::$db->query('UPDATE public.events + SET hostid = $2 + WHERE eventid = $1', [ + $this->getID(), + $hostId + ]); + $this->hostId = $hostId; + $this->updateCacheObject(); + } + + /** + * @return string + */ + public function getDescriptionHtml(): string + { + return $this->descriptionHtml; + } + + /** + * @return int + */ + public function getStartTime(): int + { + return $this->startTime; + } + + /** + * @return int + */ + public function getEndTime(): int + { + return $this->endTime; + } + + /** + * @return int + */ + public function getHostId(): int + { + return $this->hostId; + } + + /** + * @return MyRadio_User + */ + public function getHost(): MyRadio_User + { + return MyRadio_User::getInstance($this->hostId); + } + + /** + * Updates this event's data. + * + * Note that this method does not do any authorisation of its own. + * + * @param array $data same shape as {@link MyRadio_Event::create} + */ + public function update(array $data) + { + $requiredFields = ['title', 'description_html', 'start_time', 'end_time', 'host']; + foreach ($requiredFields as $field) { + if (!(isset($data[$field]))) { + throw new MyRadioException("Missing $field", 400); + } + } + + $intFields = ['start_time', 'end_time']; + foreach ($intFields as $intField) { + if (!is_int($data[$intField])) { + throw new MyRadioException("Expected $intField to be an integer", 400); + } + } + + self::$db->query( + 'UPDATE public.events + SET title = $2, + description_html = $3, + start_time = $4, + end_time = $5, + hostid = $6 + WHERE eventid = $1', + [ + $this->getID(), + $data['title'], + $data['description_html'], + CoreUtils::getTimestamp($data['start_time']), + CoreUtils::getTimestamp($data['end_time']), + $data['host']->getID() + ] + ); + + $this->title = $data['title']; + $this->descriptionHtml = $data['description_html']; + $this->startTime = $data['start_time']; + $this->endTime = $data['end_time']; + $this->hostId = $data['host']->getID(); + + $this->updateCacheObject(); + } + + /** + * Deletes this event. MAKE SURE TO CHECK AUTHORISATION BEFOREHAND! + */ + public function delete() + { + self::$db->query( + 'DELETE FROM public.events WHERE eventid = $1', + [$this->getID()] + ); + self::$cache->purge(); + } + + /** + * Get the next N events. + * + * @param $n int the number of events to get + * @return MyRadio_Event[] events + */ + public static function getNext($n = 5) + { + $sql = 'SELECT eventid FROM public.events WHERE start_time >= NOW() ORDER BY start_time ASC LIMIT $1'; + $rows = self::$db->fetchColumn($sql, [$n]); + return self::resultSetToObjArray($rows); + } + + public static function getInRange($start, $end) + { + $sql = 'SELECT eventid FROM public.events WHERE start_time >= $1 AND end_time <= $2 ORDER BY start_time ASC'; + $rows = self::$db->fetchColumn( + $sql, + [ + CoreUtils::getTimestamp(strtotime($start)), + CoreUtils::getTimestamp(strtotime($end)) + ] + ); + return self::resultSetToObjArray($rows); + } + + public static function create($data = []) + { + // Validate + $requiredFields = ['title', 'description_html', 'start_time', 'end_time', 'host']; + foreach ($requiredFields as $field) { + if (!(isset($data[$field]))) { + throw new MyRadioException("Missing $field", 400); + } + } + + $intFields = ['start_time', 'end_time']; + foreach ($intFields as $intField) { + if (!is_int($data[$intField])) { + throw new MyRadioException("Expected $intField to be an integer", 400); + } + } + + + $sql = "INSERT INTO public.events (title, description_html, start_time, end_time, hostid) + VALUES ($1, $2, $3, $4, $5) RETURNING eventid"; + + $result = self::$db->fetchColumn($sql, [ + $data['title'], $data['description_html'], + CoreUtils::getTimestamp($data['start_time']), CoreUtils::getTimestamp($data['end_time']), + $data['host']->getID() + ]); + + return self::factory($result[0]); + } + + protected static function factory($itemid) + { + $sql = self::BASE_SQL . " WHERE eventid = $1"; + + $result = self::$db->fetchOne($sql, [$itemid]); + if (empty($result)) { + throw new MyRadioException("Event $itemid does not exist", 404); + } + + return new self($result); + } + + public static function getForm() + { + return ( + new MyRadioForm( + 'event', + 'Events', + 'editEvent', + [ + 'title' => 'Events', + 'subtitle' => 'Create Event' + ] + ) + )->addField( + new MyRadioFormField( + 'title', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Event Title', + 'explanation' => 'Give this event a name.' + ] + ) + )->addField( + new MyRadioFormField( + 'description_html', + MyRadioFormField::TYPE_BLOCKTEXT, + [ + 'explanation' => 'Describe your event as best you can.', + 'label' => 'Description', + ] + ) + )->addField( + new MyRadioFormField( + 'start_time', + MyRadioFormField::TYPE_DATETIME, + [ + 'required' => true, + 'label' => 'Start Time', + ] + ) + ) + ->addField( + new MyRadioFormField( + 'end_time', + MyRadioFormField::TYPE_DATETIME, + [ + 'required' => false, + 'label' => 'End Time', + ] + ) + ) + ->addField( + new MyRadioFormField( + 'host', + MyRadioFormField::TYPE_MEMBER, + [ + 'required' => true, + 'label' => 'Organizer', + 'explanation' => 'The person running the event', + ] + + ) + ); + } + + public function getEditForm() + { + return self::getForm() + ->setSubtitle('Edit Event') + ->editMode( + $this->getID(), + [ + 'title' => $this->getTitle(), + 'description_html' => $this->getDescriptionHtml(), + 'start_time' => date('d/m/Y H:i', $this->getStartTime()), + 'end_time' => date('d/m/Y H:i', $this->getEndTime()), + 'host' => MyRadio_User::getInstance($this->getHostId()), + ] + ); + } + + public function toDataSource($mixins = []) + { + return [ + 'id' => $this->getID(), + 'title' => $this->title, + 'description_html' => $this->descriptionHtml, + 'start' => CoreUtils::getIso8601Timestamp($this->startTime), + 'end' => CoreUtils::getIso8601Timestamp($this->endTime), + 'host' => MyRadio_User::getInstance($this->hostId)->toDataSource($mixins), + ]; + } + + public function toIcalEvent() + { + return Event::create($this->getTitle()) + ->startsAt((new DateTime())->setTimestamp($this->getStartTime())) + ->endsAt((new DateTime())->setTimestamp($this->getEndTime())) + ->description(html_entity_decode(strip_tags($this->getDescriptionHtml()))) + ->organizer($this->getHost()->getPublicEmail(), $this->getHost()->getName()); + } + + public function canWeEdit() + { + return $this->getHost()->getID() === MyRadio_User::getCurrentOrSystemUser()->getID() + || AuthUtils::hasPermission(AUTH_EDITANYEVENT); + } + + public function checkEditPermissions() + { + if ($this->getHost()->getID() !== MyRadio_User::getCurrentOrSystemUser()->getID()) { + AuthUtils::requirePermission(AUTH_EDITANYEVENT); + } + } + + public static function createCalendarTokenFor($memberid = null) + { + if ($memberid === null) { + $currentUser = MyRadio_User::getCurrentUser(); + if ($currentUser === null) { + throw new MyRadioException('Can\'t create a calendar token with no user!'); + } + $memberid = $currentUser->getId(); + } + // We could in theory have a collision, but it'll get caught by postgres. + // The likelihood is so small that we'll just let it crash and let the user try again. + $tokenStr = CoreUtils::randomString(16); + self::$db->query( + 'INSERT INTO public.calendar_tokens (memberid, token_str) VALUES ($1, $2)', + [$memberid, $tokenStr] + ); + return $tokenStr; + } + + public static function validateCalendarToken($token) + { + $result = self::$db->fetchOne( + 'SELECT memberid FROM public.calendar_tokens WHERE token_str = $1 AND revoked = FALSE', + [$token] + ); + if (empty($result)) { + return null; + } + return $result['memberid']; + } + + public static function getCalendarTokenFor($memberid = null) + { + if ($memberid === null) { + $currentUser = MyRadio_User::getCurrentUser(); + if ($currentUser === null) { + throw new MyRadioException('Can\'t revoke a calendar token with no user!'); + } + $memberid = $currentUser->getId(); + } + $result = self::$db->fetchOne( + 'SELECT token_str FROM public.calendar_tokens WHERE memberid = $1 AND revoked = FALSE', + [$memberid] + ); + if (empty($result)) { + return null; + } + return $result['token_str']; + } + + public static function revokeCalendarTokenFor($memberid = null) + { + if ($memberid === null) { + $currentUser = MyRadio_User::getCurrentUser(); + if ($currentUser === null) { + throw new MyRadioException('Can\'t revoke a calendar token with no user!'); + } + $memberid = $currentUser->getId(); + } + self::$db->query( + 'UPDATE public.calendar_tokens SET revoked = TRUE WHERE memberid = $1', + [$memberid] + ); + } +} diff --git a/src/Classes/ServiceAPI/MyRadio_List.php b/src/Classes/ServiceAPI/MyRadio_List.php index 1361952e7..5ddd8a0e9 100644 --- a/src/Classes/ServiceAPI/MyRadio_List.php +++ b/src/Classes/ServiceAPI/MyRadio_List.php @@ -1,305 +1,426 @@ - * @package MyRadio_Mail - * @uses \Database + * The List class stores and manages information about a URY Mailing List. + * + * @uses \Database */ -class MyRadio_List extends ServiceAPI { - - /** - * Stores the primary key for the list - * @var int - */ - private $listid; - - /** - * Stores the user-friendly name of the list - * @var String - */ - private $name; - - /** - * If non-optin, stores the SQL query that returns the member memberids - * @var String - */ - private $sql; - - /** - * If true, this mailing list has an @ury.org.uk alias that is publically usable - * @var boolean - */ - private $public; - - /** - * If public, this is the prefix for the email address (i.e. "cactus") - * would be cactus@ury.org.uk - * @var String - */ - private $address; - - /** - * If true, this means that members subscribe themselves to this list - * @var boolean - */ - private $optin; - - /** - * This is the set of members that receive messages to this list - * @var int[] - */ - private $members = array(); - - /** - * Initialised on first request, stores an archive of all the email IDs - * sent to this list. - * @var int[] - */ - private $archive = []; - - /** - * Initiates the MyRadio_List object - * @param int $listid The ID of the Mailing List to initialise - */ - protected function __construct($listid) { - $this->listid = $listid; - - $result = self::$db->fetch_one('SELECT * FROM mail_list WHERE listid=$1', array($listid)); - if (empty($result)) { - throw new MyRadioException('List ' . $listid . ' does not exist!'); - return null; +class MyRadio_List extends ServiceAPI +{ + /** + * Stores the primary key for the list. + * + * @var int + */ + private $listid; + + /** + * Stores the user-friendly name of the list. + * + * @var string + */ + private $name; + + public static function getGraphQLTypeName() + { + return 'MailingList'; } - $this->name = $result['listname']; - $this->sql = $result['defn']; - $this->public = $result['toexim']; - $this->address = $result['listaddress']; - $this->optin = $result['subscribable'] === 't'; - - if ($this->optin) { - //Get subscribed members - $this->members = self::$db->fetch_column('SELECT memberid FROM mail_subscription WHERE listid=$1', array($listid)); - } else { - //Get members joined with opted-out members - $this->members = self::$db->fetch_column('SELECT memberid FROM (' . $this->parseSQL($this->sql) . ') as t1 WHERE memberid NOT IN - (SELECT memberid FROM mail_subscription WHERE listid=$1)', array($listid)); + /** + * If non-optin, stores the SQL query that returns the member memberids. + * + * @var string + */ + private $sql; + + /** + * If true, this mailing list has an @ury.org.uk alias that is publically usable. + * + * @var bool + */ + private $public; + + /** + * If public, this is the prefix for the email address (i.e. "cactus") + * would be cactus@ury.org.uk. + * + * @var string + */ + private $address; + + /** + * If true, this means that members subscribe themselves to this list. + * + * @var bool + */ + private $optin; + + /** + * This is the set of members that receive messages to this list. + * + * @var int[] + */ + private $members = []; + + /** + * Initialised on first request, stores an archive of all the email IDs + * sent to this list. + * + * @var int[] + */ + private $archive = []; + + /** + * Initiates the MyRadio_List object. + * + * @param $listid The ID of the Mailing List to initialise + */ + protected function __construct($listid) + { + $this->listid = (int) $listid; + + $result = self::$db->fetchOne('SELECT * FROM mail_list WHERE listid=$1', [$this->listid]); + if (empty($result)) { + throw new MyRadioException('List '.$listid.' does not exist!'); + + return; + } + + $this->name = $result['listname']; + $this->sql = $result['defn']; + $this->public = $result['toexim']; + $this->address = $result['listaddress']; + $this->optin = $result['subscribable'] === 't'; + + if ($this->optin) { + //Get subscribed members + $this->members = self::$db->fetchColumn( + 'SELECT memberid FROM mail_subscription WHERE listid=$1', + [$listid] + ); + } else { + //Get members joined with opted-out members + $this->members = self::$db->fetchColumn( + 'SELECT memberid FROM ('.$this->parseSQL($this->sql).') as t1 WHERE memberid NOT IN + (SELECT memberid FROM mail_subscription WHERE listid=$1)', + [$listid] + ); + } + $this->members = array_map( + function ($x) { + return (int) $x; + }, + $this->members + ); } - $this->members = array_map(function($x) {return (int)$x;}, $this->members); - } - - private function parseSQL($sql) { - $sql = str_replace(array('%LISTID', '%Y', '%BOY'), array( - $this->getID(), - CoreUtils::getAcademicYear(), - '\'' . CoreUtils::getAcademicYear() . '-10-01 00:00:00\'' - ), $sql); - return $sql; - } - - public function getMembers() { - return MyRadio_User::resultSetToObjArray($this->members); - } - - public function getID() { - return $this->listid; - } - - public function getName() { - return $this->name; - } - - public function getAddress() { - return $this->address; - } - - public function isPublic() { - return $this->public; - } - - public function isMember(MyRadio_User $user) { - return in_array($user->getID(), $this->members); - } - - /** - * Returns if the user has permission to email this list - * @param MyRadio_User $user - * @return boolean - */ - public function hasSendPermission(MyRadio_User $user) { - if (!$this->public && !$user->hasAuth(AUTH_MAILALLMEMBERS)) { - return false; + + private function parseSQL($sql) + { + $sql = str_replace( + ['%LISTID', '%Y', '%BOY'], + [ + $this->getID(), + CoreUtils::getAcademicYear(), + '\''.CoreUtils::getAcademicYear().'-10-01 00:00:00\'', + ], + $sql + ); + + return $sql; } - return true; - } - - /** - * Returns true if the user has *actively opted out* of an *automatic* mailing list - * Returns false if they are still a member of the list, or if this is subscribable - * @param MyRadio_User $user - */ - public function hasOptedOutOfAuto(MyRadio_User $user) { - if ($this->optin) { - return false; + + public function getMembers() + { + return MyRadio_User::resultSetToObjArray($this->members); } - return sizeof(self::$db->query('SELECT memberid FROM public.mail_subscription WHERE memberid=$1 AND listid=$2', - array($user->getID(), $this->getID()))) === 1; - } - - /** - * If the mailing list is subscribable, opt the user in if they aren't already. - * If the mailing list is automatic, but the user has previously opted out, remove this opt-out entry. - * @param MyRadio_User $user - * @return boolean True if the user is now opted in, false if they could not be opted in. - * @todo Auto-rebuild Exim routing after change - */ - public function optin(MyRadio_User $user) { - if ($this->isMember($user)) { - return false; + public function getID() + { + return $this->listid; } - - if (!$this->optin && !$this->hasOptedOutOfAuto($user)) { - return false; + + public function getName() + { + return $this->name; } - - //User is already opted in - if (in_array($user, $this->getMembers())) { - return true; + + public function getAddress() + { + return $this->address; } - if ($this->optin) { - self::$db->query('INSERT INTO public.mail_subscription (memberid, listid) VALUES ($1, $2)', - array($user->getID(), $this->getID())); - } else { - self::$db->query('DELETE FROM public.mail_subscription WHERE memberid=$1 AND listid=$2', - array($user->getID(), $this->getID())); + public function isPublic() + { + return $this->public; } - $this->members[] = $user->getID(); - $this->updateCacheObject(); - return true; - } - - /** - * If the mailing list is subscribable, opt the user out if they are currently subscribed. - * If the mailing list is automatic, opt-the user out of the list. - * @param MyRadio_User $user - * @return boolean True if the user is now opted out, false if they could not be opted out. - * @todo Auto-rebuild Exim routing after change - */ - public function optout(MyRadio_User $user) { - if (!$this->isMember($user)) { - return false; + public function isMember($userid) + { + return in_array($userid, $this->members); } - if (!$this->optin) { - self::$db->query('INSERT INTO public.mail_subscription (memberid, listid) VALUES ($1, $2)', - array($user->getID(), $this->getID())); - } else { - self::$db->query('DELETE FROM public.mail_subscription WHERE memberid=$1 AND listid=$2', - array($user->getID(), $this->getID())); + /** + * Returns if the user has permission to email this list. + * + * @param MyRadio_User $user + * + * @return bool + */ + public function hasSendPermission(MyRadio_User $user) + { + if (!$this->public && !$user->hasAuth(AUTH_MAILALLMEMBERS)) { + return false; + } + + return true; } - $key = array_search($user->getID(), $this->members); - if ($key !== false) { - unset($this->members[$key]); + /** + * Returns true if the user has *actively opted out* of an *automatic* mailing list + * Returns false if they are still a member of the list, or if this is subscribable. + * + * @param int $userid + */ + public function hasOptedOutOfAuto($userid) + { + if ($this->optin) { + return false; + } + + return sizeof( + self::$db->fetchColumn( + 'SELECT memberid FROM public.mail_subscription WHERE memberid=$1 AND listid=$2', + [$userid, $this->getID()] + ) + ) === 1; } - $this->updateCacheObject(); - return true; - } - - /** - * Takes an email and puts it in the online Email Archive - * - * @param MyRadio_User $from - * @param String $email - */ - public function archiveMessage($from, $email) { - $body = str_replace("=\r\n",'',preg_split("/\r?\n\r?\n/", utf8_encode($email), 2)[1]); - preg_match('/(^|\s)Subject:(.*)/i', $email, $subject); - $subject = trim($subject[2]); - - MyRadioEmail::create(array('lists' => array($this)), $subject, $body, $from, time(), true); - $this->archive = []; - $this->updateCacheObject(); - } - - /** - * Return all the emails Archived in this List. - * @return MyRadioEmail[] - */ - public function getArchive() { - if (empty($this->archive)) { - $this->archive = self::$db->fetch_column('SELECT email.email_id ' - . 'FROM mail.email_recipient_list ' - . 'LEFT JOIN mail.email USING (email_id) ' - . 'WHERE listid=$1 ' - . 'ORDER BY timestamp DESC', - [$this->getID()]); - $this->updateCacheObject(); + + /** + * If the mailing list is subscribable, opt the user in if they aren't already. + * If the mailing list is automatic, but the user has previously opted out, remove this opt-out entry. + * + * @param int $userid + * + * @return bool True if the user is now opted in, false if they could not be opted in. + * + * @todo Auto-rebuild Exim routing after change + */ + public function optin($userid) + { + //User is already opted in + if ($this->isMember($userid)) { + return true; + } + + if (!$this->optin && !$this->hasOptedOutOfAuto($userid)) { + return false; + } + + if ($this->optin) { + self::$db->query( + 'INSERT INTO public.mail_subscription (memberid, listid) VALUES ($1, $2)', + [$userid, $this->getID()] + ); + } else { + self::$db->query( + 'DELETE FROM public.mail_subscription WHERE memberid=$1 AND listid=$2', + [$userid, $this->getID()] + ); + } + + $this->members[] = $userid; + $this->updateCacheObject(); + + return true; } - return MyRadioEmail::resultSetToObjArray($this->archive); - } - - public static function getByName($str) { - self::initDB(); - $r = self::$db->fetch_column('SELECT listid FROM mail_list WHERE listname ILIKE $1 OR listaddress ILIKE $1', - array($str)); - if (empty($r)) { - return null; - } else { - return self::getInstance($r[0]); + + /** + * If the mailing list is subscribable, opt the user out if they are currently subscribed. + * If the mailing list is automatic, opt-the user out of the list. + * + * @param $userid + * + * @return bool True if the user is now opted out, false if they could not be opted out. + * + * @todo Auto-rebuild Exim routing after change + */ + public function optout(int $userid) + { + if (!$this->isMember($userid)) { + return false; + } + + if (!$this->optin) { + self::$db->query( + 'INSERT INTO public.mail_subscription (memberid, listid) VALUES ($1, $2)', + [$userid, $this->getID()] + ); + } else { + self::$db->query( + 'DELETE FROM public.mail_subscription WHERE memberid=$1 AND listid=$2', + [$userid, $this->getID()] + ); + } + + $key = array_search($userid, $this->members); + if ($key !== false) { + unset($this->members[$key]); + } + $this->updateCacheObject(); + + return true; } - } - public static function getAllLists() { - $r = self::$db->fetch_column('SELECT listid FROM mail_list'); + /** + * Takes an email and puts it in the online Email Archive. + * + * @param MyRadio_User $from + * @param string $email + */ + public function archiveMessage($from, $email) + { + $body = str_replace("=\r\n", '', preg_split("/\r?\n\r?\n/", utf8_encode($email), 2)[1]); + preg_match('/(^|\s)Subject:(.*)/i', $email, $subject); + $subject = trim($subject[2]); + + MyRadioEmail::create(['lists' => [$this]], $subject, $body, $from, time(), true); + $this->archive = []; + $this->updateCacheObject(); + } - $lists = array(); - foreach ($r as $list) { - $lists[] = self::getInstance($list); + /** + * Return all the emails Archived in this List. + * + * @return MyRadioEmail[] + */ + public function getArchive() + { + if (empty($this->archive)) { + $this->archive = self::$db->fetchColumn( + 'SELECT email.email_id ' + .'FROM mail.email_recipient_list ' + .'LEFT JOIN mail.email USING (email_id) ' + .'WHERE listid=$1 ' + .'ORDER BY timestamp DESC', + [$this->getID()] + ); + $this->updateCacheObject(); + } + + return MyRadioEmail::resultSetToObjArray($this->archive); } - return $lists; - } + /** + * @param $str + * @return MyRadio_List|null + */ + public static function getByName($str) + { + self::initDB(); + $r = self::$db->fetchColumn( + 'SELECT listid FROM mail_list WHERE listname ILIKE $1 OR listaddress ILIKE $1', + [$str] + ); + if (empty($r)) { + return null; + } else { + return self::getInstance($r[0]); + } + } - public function toDataSource($full = true) { - if (isset($_SESSION['memberid'])) { - $subscribed = $this->isMember(MyRadio_User::getInstance()); - } else { - $subscribed = false; + /** + * Return all mailing lists. + * + * @param bool $hideExcluded if true, will exclude lists with a negative ordering + * @return MyRadio_List[] + */ + public static function getAllLists($hideExcluded = false) + { + $r = self::$db->fetchColumn('SELECT listid FROM mail_list ' + . ($hideExcluded ? 'WHERE ordering >= 0' : '') + .' ORDER BY NULLIF(ordering, -1), ordering, listid'); + + $lists = []; + foreach ($r as $list) { + $lists[] = self::getInstance($list); + } + + return $lists; } - return array( - 'listid' => $this->getID(), - 'subscribed' => $subscribed, - 'name' => $this->getName(), - 'address' => $this->getAddress(), - 'recipient_count' => sizeof($this->getMembers()), - 'optIn' => ((!$subscribed && ($this->optin || $this->hasOptedOutOfAuto(MyRadio_User::getCurrentOrSystemUser()))) ? - array('display' => 'icon', - 'value' => 'circle-plus', - 'title' => 'Subscribe to this mailing list', - 'url' => CoreUtils::makeURL('Mail', 'optin', array('list' => $this->getID()))) : null), - 'optOut' => ($subscribed ? array('display' => 'icon', - 'value' => 'circle-minus', - 'title' => 'Opt out of this mailing list', - 'url' => CoreUtils::makeURL('Mail', 'optout', array('list' => $this->getID()))) : null), - 'mail' => array('display' => 'icon', - 'value' => 'mail-closed', - 'title' => 'Send a message to this mailing list', - 'url' => CoreUtils::makeURL('Mail', 'send', array('list' => $this->getID()))), - 'archive' => array('display' => 'icon', - 'value' => 'disk', - 'title' => 'View archives for this mailing list', - 'url' => CoreUtils::makeURL('Mail', 'archive', array('list' => $this->getID()))) - ); - } + /** + * Returns data about the List. + * + * @mixin actions Returns interaction options for the UI + * @mixin recipients Lists recipients of the list + * + * @return array + */ + public function toDataSource($mixins = []) + { + $mixin_funcs = [ + 'actions' => function (&$data) { + if (!$data['subscribed'] + && ($this->optin || $this->hasOptedOutOfAuto(MyRadio_User::getCurrentOrSystemUser()->getID())) + ) { + $data['optin'] = [ + 'display' => 'icon', + 'value' => 'plus', + 'title' => 'Subscribe to this mailing list', + 'url' => URLUtils::makeURL('Mail', 'optin', ['list' => $this->getID()]), + ]; + } else { + $data['optin'] = null; + } + $data['optOut'] = ($data['subscribed'] ? [ + 'display' => 'icon', + 'value' => 'minus', + 'title' => 'Opt out of this mailing list', + 'url' => URLUtils::makeURL('Mail', 'optout', ['list' => $this->getID()]), + ] : null); + $data['mail'] = [ + 'display' => 'icon', + 'value' => 'envelope', + 'title' => 'Send a message to this mailing list', + 'url' => URLUtils::makeURL('Mail', 'send', ['list' => $this->getID()]), + ]; + $data['archive'] = [ + 'display' => 'icon', + 'value' => 'folder-close', + 'title' => 'View archives for this mailing list', + 'url' => URLUtils::makeURL('Mail', 'archive', ['list' => $this->getID()]), + ]; + }, + 'recipients' => function (&$data) { + $data['recipients'] = CoreUtils::dataSourceParser($this->getMembers()); + }, + ]; + + if (isset($_SESSION['memberid'])) { + $subscribed = $this->isMember(MyRadio_User::getInstance()->getID()); + } else { + $subscribed = false; + } + + $data = [ + 'listid' => $this->getID(), + 'subscribed' => $subscribed, + 'name' => $this->getName(), + 'address' => $this->getAddress(), + 'recipient_count' => sizeof($this->members), + ]; + + $this->addMixins($data, $mixins, $mixin_funcs); + + return $data; + } } diff --git a/src/Classes/ServiceAPI/MyRadio_MetadataSubject.php b/src/Classes/ServiceAPI/MyRadio_MetadataSubject.php index 654dfba2b..20166471a 100644 --- a/src/Classes/ServiceAPI/MyRadio_MetadataSubject.php +++ b/src/Classes/ServiceAPI/MyRadio_MetadataSubject.php @@ -1,8 +1,12 @@ - * @author Matt Windsor - * @package MyRadio_Core - * @uses \Database + * @uses \Database */ -trait MyRadio_MetadataSubject { - protected static $metadata_keys = array(); - protected $metadata; - - public function getMeta($meta_string) { - return isset($this->metadata[self::getMetadataKey($meta_string)]) ? - $this->metadata[self::getMetadataKey($meta_string)] : null; - } - - /** - * Gets the id for the string representation of a type of metadata - */ - public static function getMetadataKey($string) { - self::cacheMetadataKeys(); - if (!isset(self::$metadata_keys[$string])) { - throw new MyRadioException('Metadata Key ' . $string . ' does not exist'); +trait MyRadio_MetadataSubject +{ + protected static $metadata_keys = []; + protected $metadata; + + public function getMeta($meta_string) + { + return isset($this->metadata[self::getMetadataKey($meta_string)]) ? + $this->metadata[self::getMetadataKey($meta_string)] : null; } - return self::$metadata_keys[$string]['id']; - } - - /** - * Gets whether the type of metadata is allowed to exist more than once - */ - public static function isMetadataMultiple($id) { - self::cacheMetadataKeys(); - foreach (self::$metadata_keys as $key) { - if ($key['id'] == $id) { - return $key['multiple']; - } + + /** + * Gets the id for the string representation of a type of metadata. + */ + public static function getMetadataKey($string) + { + self::cacheMetadataKeys(); + if (!isset(self::$metadata_keys[$string])) { + throw new MyRadioException('Metadata Key '.$string.' does not exist'); + } + + return self::$metadata_keys[$string]['id']; } - throw new MyRadioException('Metadata Key ID ' . $id . ' does not exist'); - } - - /** - * Sets a *text* metadata key to the specified value. Does not work for image metadata. - * - * If any value is the same as an existing one, no action will be taken. - * If the given key has is_multiple, then the value will be added as a new, additional key. - * If the key does not have is_multiple, then any existing values will have effective_to - * set to the effective_from of this value, effectively replacing the existing value. - * This will *not* unset is_multiple values that are not in the new set. - * - * @param String $string_key The metadata key - * @param mixed $value The metadata value. If key is_multiple and value is an array, will create instance - * for value in the array. - * @param int $effective_from UTC Time the metavalue is effective from. Default now. - * @param int $effective_to UTC Time the metadata value is effective to. Default NULL (does not expire). - * @param String $table The metadata table, *including* the schema. - * @param String $id_field The ID field in the metadata table. - */ - public function setMeta($string_key, $value, $effective_from = null, $effective_to = null, $table = null, $id_field = null) { - $key = self::getMetadataKey($string_key); //Integer meta key - $multiple = self::isMetadataMultiple($key); //Bool whether multiple values are allowed - if ($effective_from === null) { - $effective_from = time(); + + /** + * Gets whether the type of metadata is allowed to exist more than once. + */ + public static function isMetadataMultiple($id) + { + self::cacheMetadataKeys(); + foreach (self::$metadata_keys as $key) { + if ($key['id'] == $id) { + return $key['multiple']; + } + } + throw new MyRadioException('Metadata Key ID '.$id.' does not exist'); } - $old = $this->normaliseMeta($this->getMeta($string_key)); - $new = $this->normaliseMeta($value); + /** + * Sets a *text* metadata key to the specified value. Does not work for image metadata. + * + * If any value is the same as an existing one, no action will be taken. + * If the given key has is_multiple, then the value will be added as a new, additional key. + * If the key does not have is_multiple, then any existing values will have effective_to + * set to the effective_from of this value, effectively replacing the existing value. + * This will *not* unset is_multiple values that are not in the new set. + * + * @param string $string_key The metadata key + * @param mixed $value The metadata value. If key is_multiple and value is an array, will create instance + * for value in the array. + * @param int $effective_from UTC Time the metavalue is effective from. Default now. + * @param int $effective_to UTC Time the metadata value is effective to. Default NULL (does not expire). + * @param string $table The metadata table, *including* the schema. + * @param string $id_field The ID field in the metadata table. + */ + protected function setMetaBase( + $string_key, + $value, + $effective_from = null, + $effective_to = null, + $table = null, + $id_field = null + ) { + $key = self::getMetadataKey($string_key); //Integer meta key + $multiple = self::isMetadataMultiple($key); //Bool whether multiple values are allowed + if ($effective_from === null) { + $effective_from = time(); + } - if (!$multiple && 1 < count($new)) { - throw new MyRadioException( - 'Tried to set multiple values for a single-instance metadata key!' - ); - } + $old = $this->normaliseMeta($this->getMeta($string_key)); + $new = $this->normaliseMeta($value); + + if (!$multiple && 1 < count($new)) { + throw new MyRadioException( + 'Tried to set multiple values for a single-instance metadata key!' + ); + } - $to_expire = array_diff($old, $new); - if (!empty($to_expire)) { - $this->expireMulti($key, $to_expire, $effective_from, $table, $id_field); + $to_expire = array_diff($old, $new); + if (!empty($to_expire)) { + $this->expireMulti($key, $to_expire, $effective_from, $table, $id_field); + } + + $to_add = array_diff($new, $old); + if (!empty($to_add)) { + $this->addMulti( + $key, + $to_add, + $effective_from, + $effective_to, + $table, + $id_field + ); + } + + // Update cache + if ($multiple) { + if (!is_array($this->metadata[$key])) { + $this->metadata[$key] = [$this->metadata[$key]]; + } + + $this->metadata[$key] = array_merge( + array_diff($this->metadata[$key], $to_expire), + $to_add + ); + } else { + $this->metadata[$key] = $value; + } + + return true; } - $to_add = array_diff($new, $old); - if (!empty($to_add)) { - $this->addMulti( - $key, $to_add, $effective_from, $effective_to, $table, $id_field - ); + /** + * Abstract actual implementation of setMetaBase. + * Passes $table & $id_field into searchMetaBase and then does something else with the results. + * + * @param string $string_key The metadata key + * @param mixed $value The metadata value. If key is_multiple and value is an array, will create instance + * for value in the array. + * @param int $effective_from UTC Time the metavalue is effective from. Default now. + * @param int $effective_to UTC Time the metadata value is effective to. Default NULL (does not expire). + */ + abstract public function setMeta($string_key, $value, $effective_from = null, $effective_to = null); + + protected static function cacheMetadataKeys() + { + if (empty(self::$metadata_keys)) { + self::initDB(); + $r = self::$db->fetchAll( + 'SELECT metadata_key_id AS id, name,' + .' allow_multiple AS multiple FROM metadata.metadata_key' + ); + foreach ($r as $key) { + self::$metadata_keys[$key['name']]['id'] = (int) $key['id']; + self::$metadata_keys[$key['name']]['multiple'] = ($key['multiple'] === 't'); + } + } } - // Update cache - if ($multiple) { - if (!is_array($this->metadata[$key])) { - $this->metadata[$key] = [$this->metadata[$key]]; - } - - $this->metadata[$key] = array_merge( - array_diff($this->metadata[$key], $to_expire), - $to_add - ); - } else { - $this->metadata[$key] = $value; + /** + * Normalises a set of metadata to be an array. + * + * @param mixed $in The incoming metadata (string or array). + * + * @return array $in, as an array. + */ + private function normaliseMeta($in) + { + if (empty($in)) { + $out = []; + } elseif (is_array($in)) { + $out = $in; + } else { + $out = [$in]; + } + + return $out; } - return true; - } - - protected static function cacheMetadataKeys() { - if (empty(self::$metadata_keys)) { - self::initDB(); - $r = self::$db->fetch_all('SELECT metadata_key_id AS id, name,' - . ' allow_multiple AS multiple FROM metadata.metadata_key'); - foreach ($r as $key) { - self::$metadata_keys[$key['name']]['id'] = (int) $key['id']; - self::$metadata_keys[$key['name']]['multiple'] = ($key['multiple'] === 't'); - } + /** + * Adds multiple metadata values for a single metadata key. + * + * @param string $key The key of the metadata row to add. + * @param array $values The values of the metadata row to add. + * @param int $from The time at which the metadata should appear. + * @param int $to The time at which the metadata should expire. + * @param string $table The metadata table on which we're adding. + * @param string $id_field The field in $table storing the object ID. + */ + private function addMulti($key, $values, $from, $to, $table, $id_field) + { + $sql = 'INSERT INTO '.$table + .' (metadata_key_id, ' + .$id_field + .', memberid, approvedid, metadata_value, effective_from, effective_to) VALUES '; + $params = [ + $key, + $this->getID(), + MyRadio_User::getCurrentOrSystemUser()->getID(), + CoreUtils::getTimestamp($from), + $to == null ? null : CoreUtils::getTimestamp($to), + ]; + + $param_counter = 6; + foreach ($values as $value) { + $sql .= '($1, $2, $3, $3, $'.$param_counter.', $4, $5),'; + $params[] = $value; + ++$param_counter; + } + //Remove the extra comma + $sql = substr($sql, 0, -1); + + self::$db->query($sql, $params); } - } - - /** - * Normalises a set of metadata to be an array. - * - * @param mixed $in The incoming metadata (string or array). - * - * @return array $in, as an array. - */ - private function normaliseMeta($in) { - if (empty($in)) { - $out = []; - } else if (is_array($in)) { - $out = $in; - } else { - $out = [$in]; + + /** + * Expires multiple metadata values for a single metadata key. + * + * @param string $key The key of the metadata row to expire. + * @param array $values The values of the metadata row to expire. + * @param int $time The time at which the metadata should expire. + * @param string $table The metadata table on which we're expiring. + * @param string $id_field The field in $table storing the object ID. + */ + private function expireMulti($key, $values, $time, $table, $id_field) + { + // TODO: Do this in one query? + foreach ($values as $value) { + $this->expire($key, $value, $time, $table, $id_field); + } } - return $out; - } - - /** - * Adds multiple metadata values for a single metadata key. - * - * @param string $key The key of the metadata row to add. - * @param array $values The values of the metadata row to add. - * @param int $from The time at which the metadata should appear. - * @param int $to The time at which the metadata should expire. - * @param string $table The metadata table on which we're adding. - * @param string $id_field The field in $table storing the object ID. - * - * @return null Nothing. - */ - private function addMulti($key, $values, $from, $to, $table, $id_field) { - $sql = 'INSERT INTO ' . $table - . ' (metadata_key_id, ' . $id_field . ', memberid, approvedid, metadata_value, effective_from, effective_to) VALUES '; - $params = array($key, $this->getID(), MyRadio_User::getCurrentOrSystemUser()->getID(), CoreUtils::getTimestamp($time), - $effective_to == null ? null : CoreUtils::getTimestamp($effective_to)); - - $param_counter = 6; - foreach ($values as $value) { - $sql .= '($1, $2, $3, $3, $' . $param_counter . ', $4, $5),'; - $params[] = $value; - $param_counter++; + + /** + * Expires any currently active metadata with a given key and value. + * + * @param string $key The key of the metadata row to expire. + * @param string $value The value of the metadata row to expire. + * @param int $time The time at which the metadata should expire. + * @param string $table The metadata table on which we're expiring. + * @param string $id_field The field in $table storing the object ID. + */ + private function expire($key, $value, $time, $table, $id_field) + { + self::$db->query( + 'UPDATE '.$table.' + SET effective_to = $1 + WHERE metadata_key_id =$2 + AND '.$id_field.' =$3 + AND metadata_value = $4 + AND (effective_to IS NULL OR effective_to > $1);', + [CoreUtils::getTimestamp($time), $key, $this->getID(), $value] + ); } - //Remove the extra comma - $sql = substr($sql, 0, -1); - - self::$db->query($sql, $params); - } - - /** - * Expires multiple metadata values for a single metadata key. - * - * @param string $key The key of the metadata row to expire. - * @param array $values The values of the metadata row to expire. - * @param int $time The time at which the metadata should expire. - * @param string $table The metadata table on which we're expiring. - * @param string $id_field The field in $table storing the object ID. - * - * @return null Nothing. - */ - private function expireMulti($key, $values, $time, $table, $id_field) { - // TODO: Do this in one query? - foreach ($values as $value) { - $this->expire($key, $value, $time, $table, $id_field); + + /** + * Searches searchable *text* metadata for the specified value. Does not work for image metadata. + * + * This function must be extended by Classes using MetadataSubject to provide the correct + * ID field and table that the metadata is stored in. + * + * @param string $query The query value. + * @param array $string_keys The metadata keys to search + * @param int|null $effective_from UTC Time to search from. + * @param int|null $effective_to UTC Time to search to. + * @param string $table The metadata table, *including* the schema. + * @param string $id_field The ID field in the metadata table. + * @param int $limit The number of results to return + * + * @return array The list of IDs of whatever is being searched. + * @noinspection PhpDocSignatureInspection + * @todo effective_from/to not yet implemented + */ + protected static function searchMetaBase( + $query, + $string_keys, + $effective_from = null, + $effective_to = null, + $table = null, + $id_field = null, + $limit = 25 + ) { + if (is_null($table) || is_null($id_field)) { + throw new MyRadioException('Search table and ID must be set.'); + } + + $keys = []; + + foreach ($string_keys as $string_key) { + $keys[] = self::getMetadataKey($string_key); //Integer meta key + } + + $meta_keys = '('.implode(',', array_unique($keys)).')'; + + $query = urldecode($query); + + $results = self::$db->fetchColumn( + 'SELECT DISTINCT '.$id_field + .' FROM '.$table + .' WHERE metadata_value ILIKE \'%\' || $1 || \'%\'' + .' AND metadata_key_id IN '.$meta_keys /* safe - we control keys via getMetadataKey */ + .' LIMIT $2', + [$query, $limit] + ); + + return $results; } - } - - /** - * Expires any currently active metadata with a given key and value. - * - * @param string $key The key of the metadata row to expire. - * @param string $value The value of the metadata row to expire. - * @param int $time The time at which the metadata should expire. - * @param string $table The metadata table on which we're expiring. - * @param string $id_field The field in $table storing the object ID. - * - * @return null Nothing. - */ - private function expire($key, $value, $time, $table, $id_field) { - self::$db->query( - 'UPDATE ' . $table . ' - SET effective_to = $1 - WHERE metadata_key_id =$2 - AND ' . $id_field . ' =$3 - AND metadata_value = $4 - AND (effective_to IS NULL OR effective_to > $1);', - [CoreUtils::getTimestamp($time), $key, $this->getID(), $value] + + /** + * Abstract actual implementation of searchMetaBase. + * Passes $table & $id_field into searchMetaBase and then does something else with the results. + * + * @param string $query The query value. + * @param array $string_keys The metadata keys to search + * @param int $effective_from UTC Time to search from. + * @param int $effective_to UTC Time to search to. + */ + abstract public static function searchMeta( + $query, + $string_keys = null, + $effective_from = null, + $effective_to = null ); - } } -?> diff --git a/src/Classes/ServiceAPI/MyRadio_Metadata_Common.php b/src/Classes/ServiceAPI/MyRadio_Metadata_Common.php index ce4050845..d19f0b27d 100644 --- a/src/Classes/ServiceAPI/MyRadio_Metadata_Common.php +++ b/src/Classes/ServiceAPI/MyRadio_Metadata_Common.php @@ -1,9 +1,9 @@ - * @package MyRadio_Scheduler - * @uses \Database - * + * @uses \Database */ -abstract class MyRadio_Metadata_Common extends ServiceAPI { - use MyRadio_Creditable; - use MyRadio_MetadataSubject; +abstract class MyRadio_Metadata_Common extends ServiceAPI +{ + use MyRadio_Creditable; + use MyRadio_MetadataSubject; } diff --git a/src/Classes/ServiceAPI/MyRadio_Officer.php b/src/Classes/ServiceAPI/MyRadio_Officer.php index 568f18824..3df9f7d83 100644 --- a/src/Classes/ServiceAPI/MyRadio_Officer.php +++ b/src/Classes/ServiceAPI/MyRadio_Officer.php @@ -1,229 +1,842 @@ - * @package MyRadio_Core - * @uses \Database - * + * + * @uses \Database */ -class MyRadio_Officer extends ServiceAPI { - /** - * The ID of the Officer - * @var int - */ - private $officerid; - - /** - * Officer title e.g. "Station Manager" - * @var String - */ - private $name; - /** - * Officer email alias e.g. "station.manager" - * @var String - */ - private $alias; - /** - * Team the Officership is a member of. - * @var int - */ - private $team; - /** - * The weight of the Officer position, when listing on a page. - * @var int - */ - private $ordering; - /** - * A description of the position. - * @var String - */ - private $description; - /** - * (c)urrent or (h)istorical. - * @var char - */ - private $status; - /** - * (o)fficer, (a)ssistant head of team, (h)ead of team - * or (m)ember (not actually an Officer, just in team) - * - * @var char - */ - private $type; - /** - * Users who have held this position. Cached on first request. - * @var Array - */ - private $history; - - - protected function __construct($id) { - $result = self::$db->fetch_one('SELECT * FROM public.officer ' - . 'WHERE officerid=$1', [$id]); - - if (empty($result)) { - throw new MyRadioException('Officer '.$id.' does not exist!', 404); - } else { - $this->officerid = (int)$id; - $this->name = $result['officer_name']; - $this->alias = $result['officer_alias']; - $this->team = (int)$result['teamid']; - $this->ordering = (int)$result['ordering']; - $this->description = $result['descr']; - $this->status = $result['status']; - $this->type = $result['type']; - } - } - - /** - * Returns all the Officers available. - * @return array - */ - public static function getAllOfficerPositions() { - return self::resultSetToObjArray(self::$db->fetch_column( - 'SELECT officerid FROM public.officer')); - } - - /** - * Get the ID fo this Officer - * @return int - */ - public function getID() { - return $this->officerid; - } - - /** - * Get the Name of this Officer Position - * @return String - */ - public function getName() { - return $this->name; - } - - /** - * Gets the Officer primary email alias. - * @return String - */ - public function getAlias() { - return $this->alias; - } - - /** - * Returns the Team this Officership is part of - * @return MyRadio_Team - */ - public function getTeam() { - return MyRadio_Team::getInstance($this->team); - } - - /** - * Returns the weight of the Officer when listing them. - * @return int - */ - public function getOrdering() { - return $this->ordering; - } - - /** - * - * @return String - */ - public function getDescription() { - return $this->description; - } - - /** - * (c)urrent or (h)istorical. - * @return char - */ - public function getStatus() { - return $this->status; - } - - /** - * (o)fficer, (a)ssistant head of team, (h)ead of team - * or (m)ember (not actually an Officer, just in team) - * @return char - */ - public function getType() { - return $this->type; - } - - /** - * Return all Users who held this Officership - * @return Array {'User':User, 'from':time, 'to':time|null, - * 'memberofficerid': int} - */ - public function getHistory() { - if (empty($this->history)) { - $result = self::$db->fetch_all('SELECT member_officerid, memberid, ' - . 'from_date, till_date FROM public.member_officer ' - . 'WHERE officerid=$1 ORDER BY from_date DESC', [$this->getID()]); - - $this->history = array_map(function($x) { - return ['User'=>$x['memberid'], - 'from'=>strtotime($x['from_date']), - 'to'=> empty($x['till_date']) ? null - : strtotime($x['till_date']), - 'memberofficerid' => (int)$x['member_officerid'] +class MyRadio_Officer extends ServiceAPI +{ + /** + * The ID of the Officer. + * + * @var int + */ + private $officerid; + + /** + * Officer title e.g. "Station Manager". + * + * @var string + */ + private $name; + /** + * Officer email alias e.g. "station.manager". + * + * @var string + */ + private $alias; + /** + * Team the Officership is a member of. + * + * @var int + */ + private $team; + /** + * The weight of the Officer position, when listing on a page. + * + * @var int + */ + private $ordering; + /** + * A description of the position. + * + * @var string + */ + private $description; + /** + * (c)urrent or (h)istorical. + * + * @var char + */ + private $status; + /** + * (o)fficer, (a)ssistant head of team, (h)ead of team + * or (m)ember (not actually an Officer, just in team). + * + * @var char + */ + private $type; + /** + * Users who have held this position. Cached on first request. + * + * @var array + */ + private $history; + /** + * Stores the Officer's permissions. + * + * @var array + */ + private $permissions; + + protected function __construct($id) + { + $result = self::$db->fetchOne( + 'SELECT * FROM public.officer ' + .'WHERE officerid=$1', + [$id] + ); + + if (empty($result)) { + throw new MyRadioException('Officer '.$id.' does not exist!', 404); + } else { + $this->officerid = (int) $id; + $this->name = $result['officer_name']; + $this->alias = $result['officer_alias']; + $this->team = (int) $result['teamid']; + $this->ordering = (int) $result['ordering']; + $this->description = $result['descr']; + $this->status = $result['status']; + $this->type = $result['type']; + + //Get the officer's permissions + $this->updatePermissions(); + } + } + + /** + * Create a new Officer position. + * + * @param string $name The position name, e.g. "Station Cat" + * @param string $descr A description of the position "official feline" + * @param string $alias Email alias (may be NULL) e.g. station.cat + * @param int $ordering Weighting when appearing in lists e.g. 0 + * @param MyRadio_Team $team The Team the Officer is part of + * @param char $type 'm'ember, 'o'fficer, 'a'ssistant head, 'h'ead + * + * @return MyRadio_Officer The new Officer position + */ + public static function createOfficer($name, $descr, $alias, $ordering, MyRadio_Team $team, $type = 'o') + { + return self::getInstance( + self::$db->fetchColumn( + 'INSERT INTO public.officer + (officer_name, officer_alias, teamid, ordering, descr, type) + VALUES ($1, $2, $3, $4, $5, $6) RETURNING officerid', + [$name, $alias, $team->getID(), $ordering, $descr, $type] + )[0] + ); + } + + /** + * Returns all the Officers available. + * + * @return array + */ + public static function getAllOfficerPositions() + { + return self::resultSetToObjArray( + self::$db->fetchColumn( + 'SELECT officerid FROM public.officer' + ) + ); + } + + /** + * Assigns an officership to the given member. + * + * @param int $memberid ID of the member for the officership + * + * @api POST + */ + public function assignOfficer($memberid) + { + self::$db->query( + 'INSERT INTO public.member_officer + (officerid, memberid, from_date) + VALUES ($1, $2, NOW())', + [$this->getID(), $memberid] + ); + $member = MyRadio_User::getInstance($memberid); + $member->updateCacheObject(); + Profile::clearCache(); + + $this->considerEmailingNewOfficer($member); + } + + /** + * Stands Down the officership provided. + * + * @param int $memberofficerid The ID of the officership to stand down + * + * @api POST + */ + public static function standDown($memberofficerid) + { + $return = self::$db->fetchColumn( + 'UPDATE public.member_officer + SET till_date = NOW() + WHERE member_officerid = $1 + RETURNING memberid', + [$memberofficerid] + ); + + MyRadio_User::getInstance($return[0]) + ->clearOfficershipCache() + ->clearPermissionCache() + ->updateCacheObject(); + Profile::clearCache(); + } + + /** + * Get the ID fo this Officer. + * + * @return int + */ + public function getID() + { + return $this->officerid; + } + + /** + * Get the Name of this Officer Position. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Sets the Name of this Officer Position. + * + * @param string $name the new name of the officer + * + * @return MyRadio_Officer the updated officer object + */ + public function setName($name) + { + if ($name !== $this->name) { + self::$db->query( + 'UPDATE public.officer + SET officer_name = $1 + WHERE officerid=$2', + [$name, $this->getID()] + ); + $this->name = $name; + $this->updateCacheObject(); + } + + return $this; + } + + /** + * Gets the Officer primary email alias. + * + * @return string + */ + public function getAlias() + { + return $this->alias; + } + + /** + * Sets the Alias of this Officer Position. + * + * @param string $alias the new alias of the officer + * + * @return MyRadio_Officer the updated officer object + */ + public function setAlias($alias) + { + if ($alias !== $this->alias) { + self::$db->query( + 'UPDATE public.officer + SET officer_alias = $1 + WHERE officerid=$2', + [$alias, $this->getID()] + ); + $this->alias = $alias; + $this->updateCacheObject(); + } + + return $this; + } + + /** + * Returns the Team this Officership is part of. + * + * @return MyRadio_Team + */ + public function getTeam() + { + return MyRadio_Team::getInstance($this->team); + } + + /** + * Sets the Team of this Officer Position. + * + * @param int $team the new team of the officer + * + * @return MyRadio_Officer the updated officer object + */ + public function setTeam($team) + { + if ($team !== $this->team) { + self::$db->query( + 'UPDATE public.officer + SET teamid = $1 + WHERE officerid=$2', + [$team, $this->getID()] + ); + $this->team = $team; + $this->updateCacheObject(); + } + + return $this; + } + + /** + * Returns the weight of the Officer when listing them. + * + * @return int + */ + public function getOrdering() + { + return $this->ordering; + } + + /** + * Sets the Ordering of this Officer Position. + * + * @param int $ordering the new ordering of the officer + * + * @return MyRadio_Officer the updated officer object + */ + public function setOrdering($ordering) + { + if (!is_int($ordering)) { + throw new MyRadioException('Ordering must be a number', 400); + } + if ($ordering !== $this->ordering) { + self::$db->query( + 'UPDATE public.officer + SET ordering = $1 + WHERE officerid=$2', + [$ordering, $this->getID()] + ); + $this->ordering = $ordering; + $this->updateCacheObject(); + } + + return $this; + } + + /** + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Sets the Description of this Officer Position. + * + * @param string $description the new description of the officer + * + * @return MyRadio_Officer the updated officer object + */ + public function setDescription($description) + { + if ($description !== $this->description) { + self::$db->query( + 'UPDATE public.officer + SET descr = $1 + WHERE officerid=$2', + [$description, $this->getID()] + ); + $this->description = $description; + $this->updateCacheObject(); + } + + return $this; + } + + /** + * (c)urrent or (h)istorical. + * + * @return char + */ + public function getStatus() + { + return $this->status; + } + + /** + * Sets the Status of this Officer Position. + * + * @param char $status the new status of the officer + * + * @return MyRadio_Officer the updated officer object + */ + public function setStatus($status) + { + if ($status !== $this->status) { + self::$db->query( + 'UPDATE public.officer + SET status = $1 + WHERE officerid=$2', + [$status, $this->getID()] + ); + $this->status = $status; + $this->updateCacheObject(); + } + + return $this; + } + + /** + * (o)fficer, (a)ssistant head of team, (h)ead of team + * or (m)ember (not actually an Officer, just in team). + * + * @return char + */ + public function getType() + { + return $this->type; + } + + /** + * Sets the Type of this Officer Position. + * + * @param char $type the new type of the officer + * + * @return MyRadio_Officer the updated officer object + */ + public function setType($type) + { + if ($type !== $this->type) { + self::$db->query( + 'UPDATE public.officer + SET type = $1 + WHERE officerid=$2', + [$type, $this->getID()] + ); + $this->type = $type; + $this->updateCacheObject(); + } + + return $this; + } + + /** + * Return all Users who held this Officership. + * + * @return array {'User':User, 'from':time, 'to':time|null, + * 'memberofficerid': int} + */ + public function getHistory() + { + if (empty($this->history)) { + $result = self::$db->fetchAll( + 'SELECT member_officerid, memberid, ' + .'from_date, till_date FROM public.member_officer ' + .'WHERE officerid=$1 ORDER BY from_date DESC', + [$this->getID()] + ); + + $this->history = array_map( + function ($x) { + return [ + 'User' => $x['memberid'], + 'from' => strtotime($x['from_date']), + 'to' => empty($x['till_date']) ? null + : strtotime($x['till_date']), + 'memberofficerid' => (int) $x['member_officerid'], + ]; + }, + $result + ); + $this->updateCacheObject(); + } + + return array_map( + function ($x) { + $x['User'] = MyRadio_User::getInstance($x['User']); + + return $x; + }, + $this->history + ); + } + + /** + * Get Users currently in the position. + * + * @return MyRadio_User[] + */ + public function getCurrentHolders() + { + $i = $this->getHistory(); + $result = []; + foreach ($i as $o) { + if ($o['to'] === null) { + $result[] = $o['User']; + } + } + + return $result; + } + + /** + * Updates the cache objects for all current holders. + */ + private function updateMemberCache() + { + foreach ($this->getCurrentHolders() as $member) { + $member->updateCacheObject(); + } + } + + /** + * Updates the permissions stored in the Officer Object. + */ + private function updatePermissions() + { + //Get the officer's permissions + $this->permissions = self::$db->fetchAll( + 'SELECT typeid AS value, descr AS text FROM public.l_action, public.auth_officer + WHERE typeid = lookupid + AND officerid=$1 + ORDER BY descr ASC', + [$this->getID()] + ); + } + + /** + * Email an officer the "new officer" email if they've never had that officership before, + * and it's actually a current officer post. + * If they have, does nothing. + */ + private function considerEmailingNewOfficer(MyRadio_User $member) + { + if ($this->getType() === 'm' || $this->getStatus() === 'h') { + return; + } + $officerships = $member->getOfficerships(); + foreach ($officerships as $officership) { + if ($officership->getOfficer()->getID() === $this->getID() && !empty($officership->getTillDate())) { + return; + } + } + // They're good. + + MyRadioEmail::sendEmailToUser( + $member, + 'Congratulations on your new officership!', + Config::$new_officer_email + ); + } + + /** + * Returns all the officer's active permission flags. + * + * @return array + */ + public function getPermissions() + { + return $this->permissions; + } + + /** + * Adds a permission flag to the officer. + * + * @param $permissionid the permission to add + */ + public function addPermission($permissionid) + { + self::$db->query( + 'INSERT INTO public.auth_officer + (officerid, lookupid) + VALUES ($1, $2)', + [$this->getID(), $permissionid] + ); + $this->updatePermissions(); + $this->updateCacheObject(); + $this->updateMemberCache(); + + return $this; + } + + /** + * Removes a permission flag from the officer. + * + * @param int $permissionid the permission to remove + * + * @api POST + */ + public function revokePermission($permissionid) + { + self::$db->query( + 'DELETE from public.auth_officer + WHERE officerid = $1 + AND lookupid = $2', + [$this->getID(), $permissionid] + ); + $this->updatePermissions(); + $this->updateCacheObject(); + $this->updateMemberCache(); + + return $this; + } + + /** + * Form for Officerships. + * + * @return MyRadioForm + */ + public static function getForm() + { + $form = ( + new MyRadioForm( + 'officerForm', + 'Profile', + 'editOfficer', + ['title' => 'Create Officer'] + ) + )->addField( + new MyRadioFormField( + 'name', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Title', + ] + ) + )->addField( + new MyRadioFormField( + 'alias', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Email Alias', + ] + ) + )->addField( + new MyRadioFormField( + 'team', + MyRadioFormField::TYPE_SELECT, + [ + 'label' => 'Team', + 'options' => array_merge( + [ + [ + 'value' => null, + 'text' => 'Select a Team', + ], + ], + MyRadio_Team::getTeamSelect() + ), + ] + ) + )->addField( + new MyRadioFormField( + 'ordering', + MyRadioFormField::TYPE_NUMBER, + [ + 'label' => 'Ordering', + ] + ) + )->addField( + new MyRadioFormField( + 'description', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Description', + 'required' => false, + ] + ) + )->addField( + new MyRadioFormField( + 'status', + MyRadioFormField::TYPE_SELECT, + [ + 'label' => 'Status', + 'options' => array_merge( + [ + [ + 'value' => null, + 'text' => 'Select Status', + ], + ], + CoreUtils::getStatusLookup() + ), + ] + ) + )->addField( + new MyRadioFormField( + 'type', + MyRadioFormField::TYPE_SELECT, + [ + 'label' => 'Officer Type', + 'options' => [ + [ + 'value' => null, + 'text' => 'Select Type', + ], + [ + 'value' => 'h', + 'text' => 'Head of Team', + ], + [ + 'value' => 'a', + 'text' => 'Assistant Head of Team', + ], + [ + 'value' => 'o', + 'text' => 'Team Officer', + ], + [ + 'value' => 'm', + 'text' => 'Team Member', + ], + ], + ] + ) + )->addField( + new MyRadioFormField( + 'permissions', + MyRadioFormField::TYPE_TABULARSET, + [ + 'label' => 'Permissions', + 'explanation' => 'Select permissions that you want to add.', + 'required' => false, + 'options' => [ + new MyRadioFormField( + 'permission', + MyRadioFormField::TYPE_SELECT, + [ + 'label' => 'Permission', + 'required' => false, + 'options' => array_merge( + [ + [ + 'value' => null, + 'text' => 'Select a Permission', + ], + ], + AuthUtils::getAllPermissions() + ), + ] + ), + ], + ] + ) + ); + + return $form; + } + + /** + * Edit form for an existing Officership. + * + * @return MyRadioForm + */ + public function getEditForm() + { + return self::getForm() + ->setTitle('Edit Officer') + ->editMode( + $this->getID(), + [ + 'name' => $this->getName(), + 'description' => $this->getDescription(), + 'alias' => $this->getAlias(), + 'ordering' => $this->getOrdering(), + 'team' => $this->getTeam()->getID(), + 'type' => $this->getType(), + 'status' => $this->getStatus(), + 'permissions.permission' => array_map( + function ($perm) { + return $perm['value']; + }, + $this->getPermissions() + ), + ] + ); + } + + /** + * Form for assigning members to an officership. + * + * @return MyRadioForm + */ + public static function getAssignForm() + { + $form = new MyRadioForm( + 'assignForm', + 'Profile', + 'assignOfficer', + ['title' => 'Assign Officer'] + ); + $form->addField( + new MyRadioFormField( + 'member', + MyRadioFormField::TYPE_MEMBER, + [ + 'explanation' => '', + 'label' => 'Member', + ] + ) + ); + + return $form; + } + + /** + * Returns data about the Officer. + * + * @mixin permissions Lists permissions the officer has + * @mixin history Lists historic position holders + * @mixin current Lists current position holders + * + * @return array + */ + public function toDataSource($mixins = []) + { + $mixin_funcs = [ + 'permissions' => function (&$data) { + $data['permissions'] = $this->getPermissions(); + }, + 'history' => function (&$data) { + $data['history'] = CoreUtils::dataSourceParser($this->getHistory()); + }, + 'current' => function (&$data) { + $data['current'] = CoreUtils::dataSourceParser($this->getCurrentHolders()); + }, ]; - }, $result); - $this->updateCacheObject(); - } - - return array_map(function($x) { - $x['User'] = MyRadio_User::getInstance($x['User']); - return $x; - },$this->history); - } - - /** - * Get Users currently in the position - * @return MyRadio_User[] - */ - public function getCurrentHolders() { - $i = $this->getHistory(); - $result = array(); - foreach ($i as $o) { - if ($o['to'] === null) { - $result[] = $o['User']; - } - } - return $result; - } - - /** - * Returns data about the Officer. - * - * @todo User who holds or has held position - * @param bool $full If true, includes info about User who holds position. - * @return Array - */ - public function toDataSource($full = false) { - $data = [ - 'officerid' => $this->getID(), - 'name' => $this->getName(), - 'alias' => $this->getAlias(), - 'team' => CoreUtils::dataSourceParser($this->getTeam(), false), - 'ordering' => $this->getOrdering(), - 'description' => $this->getDescription(), - 'status' => $this->getStatus(), - 'type' => $this->getType() - ]; - - if ($full) { - $data['current'] = CoreUtils::dataSourceParser($this->getCurrentHolders(), false); - $data['history'] = CoreUtils::dataSourceParser($this->getHistory(), false); - } - - return $data; - } + $data = [ + 'officerid' => $this->getID(), + 'name' => $this->getName(), + 'alias' => $this->getAlias(), + 'team' => CoreUtils::dataSourceParser($this->getTeam()), + 'ordering' => $this->getOrdering(), + 'description' => $this->getDescription(), + 'status' => $this->getStatus(), + 'type' => $this->getType(), + ]; + + $this->addMixins($data, $mixins, $mixin_funcs); + + return $data; + } + + public static function getGraphQLTypeName() + { + return 'Officer'; + } } diff --git a/src/Classes/ServiceAPI/MyRadio_Photo.php b/src/Classes/ServiceAPI/MyRadio_Photo.php index cdb2e3903..3e1f4c7c7 100644 --- a/src/Classes/ServiceAPI/MyRadio_Photo.php +++ b/src/Classes/ServiceAPI/MyRadio_Photo.php @@ -1,143 +1,183 @@ - * @package MyRadio_Core - * @uses \Database + * The Photo class stores and manages information about a URY Photo. + * + * @uses \Database */ -class MyRadio_Photo extends ServiceAPI { - /** - * Stores the primary key for the Photo - * @var int - */ - private $photoid; - - /** - * Stores the User that created this Photo - * @var MyRadio_User - */ - private $owner; - - /** - * Stores when the Photo was uploaded - * @var int - */ - private $date_added; - - /** - * The file extension of the photo - * @var String - */ - private $format; - - /** - * Initiates the MyRadio_Photo object - * @param int $photoid The ID of the Photo to initialise - */ - protected function __construct($photoid) { - $this->photoid = $photoid; - - $result = self::$db->fetch_one('SELECT * FROM myury.photos WHERE photoid=$1', array($photoid)); - if (empty($result)) { - throw new MyRadioException('Photo ' . $photoid . ' does not exist!'); - return null; +class MyRadio_Photo extends ServiceAPI +{ + /** + * Stores the primary key for the Photo. + * + * @var int + */ + private $photoid; + + /** + * Stores the User that created this Photo. + * + * @var MyRadio_User + */ + private $owner; + + /** + * Stores when the Photo was uploaded. + * + * @var int + */ + private $date_added; + + /** + * The file extension of the photo. + * + * @var string + */ + private $format; + + /** + * Initiates the MyRadio_Photo object. + * + * @param int $photoid The ID of the Photo to initialise + */ + protected function __construct($photoid) + { + $this->photoid = (int) $photoid; + + $result = self::$db->fetchOne( + 'SELECT * FROM myury.photos WHERE photoid=$1', + [$photoid] + ); + if (empty($result)) { + throw new MyRadioException('Photo '.$photoid.' does not exist!'); + + return; + } + + $this->owner = MyRadio_User::getInstance($result['owner']); + $this->date_added = strtotime($result['date_added']); + $this->format = $result['format']; + } + + /** + * Get array of information about the object. + * @param array $mixins Mixins. Currently unused. + * @return array + */ + public function toDataSource($mixins = []) + { + return [ + 'photoid' => $this->getID(), + 'date_added' => CoreUtils::happyTime($this->getDateAdded()), + 'format' => $this->getFormat(), + 'owner' => $this->getOwner()->getID(), + 'url' => $this->getURL(), + ]; + } + + /** + * Get the time the Photo was created. + * + * @return int + */ + public function getDateAdded() + { + return $this->date_added; } - $this->owner = MyRadio_User::getInstance($result['owner']); - $this->date_added = strtotime($result['date_added']); - $this->format = $result['format']; - } - - /** - * Get array of information about the object. - * @return Array - */ - public function toDataSource() { - return [ - 'photoid' => $this->getID(), - 'date_added' => CoreUtils::happyTime($this->getDateAdded()), - 'format' => $this->getFormat(), - 'owner' => $this->getOwner()->getID() - ]; - } - - /** - * Get the time the Photo was created - * @return int - */ - public function getDateAdded() { - return $this->date_added; - } - - /** - * Get the format (file extension) of the Photo. - * @return String - */ - public function getFormat() { - return $this->format; - } - - /** - * Get the unique ID of this Photo - * @return int - */ - public function getID() { - return $this->photoid; - } - - /** - * Get the User that owns this Photo - * @return MyRadio_User - */ - public function getOwner() { - return $this->owner; - } - - /** - * Get the web URL for loading this Photo - * @return String - */ - public function getURL() { - return Config::$public_media_uri.'/image_meta/MyRadioImageMetadata/'.$this->getID().'.'.$this->format; - } - - /** - * Get the file system path to the Photo - * @return String - */ - public function getURI() { - return Config::$public_media_path.'/image_meta/MyRadioImageMetadata/'.$this->getID().'.'.$this->format; - } - - /** - * Add a Photo - * @param String $tmp_file The path to the temporary file that is the image. - * @return MyRadio_Photo - */ - public static function create($tmp_file) { - if (!file_exists($tmp_file)) { - throw new MyRadioException('Photo path '.$tmp_file.' does not exist!', 400); + /** + * Get the format (file extension) of the Photo. + * + * @return string + */ + public function getFormat() + { + return $this->format; } - - $format = explode('/',finfo_file(finfo_open(FILEINFO_MIME_TYPE), $tmp_file))[1]; - - $result = self::$db->fetch_column('INSERT INTO myury.photos (owner, format) VALUES ($1, $2) RETURNING photoid', - [MyRadio_User::getInstance()->getID(), $format]); - $id = $result[0]; - $photo = self::getInstance($id); - if (!move_uploaded_file($tmp_file, $photo->getURI())) { - self::$db->query('DELETE FROM myury.photos WHERE photoid=$1', [$id]); - throw new MyRadioException('Failed to move new Photo from '.$tmp_file.' to '.$photo->getURI().'. Are permissions for the destination right?', 500); + + /** + * Get the unique ID of this Photo. + * + * @return int + */ + public function getID() + { + return $this->photoid; + } + + /** + * Get the User that owns this Photo. + * + * @return MyRadio_User + */ + public function getOwner() + { + return $this->owner; + } + + /** + * Get the web URL for loading this Photo. + * + * @return string + */ + public function getURL() + { + return Config::$public_media_uri.'/image_meta/MyRadioImageMetadata/'.$this->getID().'.'.$this->format; + } + + /** + * Get the file system path to the Photo. + * + * @return string + */ + public function getURI() + { + return Config::$public_media_path.'/image_meta/MyRadioImageMetadata/'.$this->getID().'.'.$this->format; + } + + public function getRelativeWebPath() + { + return '/image_meta/MyRadioImageMetadata/'.$this->getID().'.'.$this->format; } - return $photo; - } + /** + * Add a Photo. + * + * @param string $tmp_file The path to the temporary file that is the image. + * + * @return MyRadio_Photo + */ + public static function create($tmp_file) + { + if (!file_exists($tmp_file)) { + throw new MyRadioException('Photo path '.$tmp_file.' does not exist!', 400); + } + + $format = explode('/', getimagesize($tmp_file)['mime'])[1]; + + $result = self::$db->fetchColumn( + 'INSERT INTO myury.photos (owner, format) VALUES ($1, $2) RETURNING photoid', + [MyRadio_User::getInstance()->getID(), $format] + ); + $id = $result[0]; + $photo = self::getInstance($id); + if (!move_uploaded_file($tmp_file, $photo->getURI())) { + self::$db->query('DELETE FROM myury.photos WHERE photoid=$1', [$id]); + throw new MyRadioException( + 'Failed to move new Photo from ' . $tmp_file . ' to ' . $photo->getURI() + . '. Are permissions for the destination right?', + 500 + ); + } + return $photo; + } } diff --git a/src/Classes/ServiceAPI/MyRadio_Podcast.php b/src/Classes/ServiceAPI/MyRadio_Podcast.php index f9fc43c31..685e23125 100644 --- a/src/Classes/ServiceAPI/MyRadio_Podcast.php +++ b/src/Classes/ServiceAPI/MyRadio_Podcast.php @@ -1,482 +1,1080 @@ - * @package MyRadio_Podcast - * @uses \Database + * + * @uses \Database */ -class MyRadio_Podcast extends MyRadio_Metadata_Common { - /** - * The Podcast's ID - * @var int - */ - private $podcast_id; - - /** - * The path to the file, relative to Config::$public_media_uri - * @var String - */ - private $file; - - /** - * The Time the Podcast was uploaded - * @var int - */ - private $submitted; - - /** - * The ID of the User that uploaded the Podcast - * @var int - */ - private $memberid; - - /** - * The ID of the User that approved the Podcast - * @var int - */ - private $approvedid; - - /** - * Array of Users and their relation to the Podcast. - * @var Array - */ - protected $credits = array(); - - /** - * The ID of the show this is linked to, if any. - * @var int - */ - private $show_id; - - /** - * Construct the API Key Object - * @param String $key - */ - protected function __construct($podcast_id) { - $this->podcast_id = (int) $podcast_id; - - $result = self::$db->fetch_one('SELECT file, memberid, approvedid, submitted, - show_id, - (SELECT array(SELECT metadata_key_id FROM uryplayer.podcast_metadata - WHERE podcast_id=$1 AND effective_from <= NOW() - ORDER BY effective_from, podcast_metadata_id)) AS metadata_types, - (SELECT array(SELECT metadata_value FROM uryplayer.podcast_metadata - WHERE podcast_id=$1 AND effective_from <= NOW() - ORDER BY effective_from, podcast_metadata_id)) AS metadata, - (SELECT array(SELECT metadata_value FROM uryplayer.podcast_image_metadata - WHERE podcast_id=$1 AND effective_from <= NOW() - ORDER BY effective_from, podcast_image_metadata_id)) AS image_metadata, - (SELECT array(SELECT credit_type_id FROM uryplayer.podcast_credit - WHERE podcast_id=$1 AND effective_from <= NOW() - AND (effective_to IS NULL OR effective_to >= NOW()) - AND approvedid IS NOT NULL - ORDER BY podcast_credit_id)) AS credit_types, - (SELECT array(SELECT creditid FROM uryplayer.podcast_credit - WHERE podcast_id=$1 AND effective_from <= NOW() - AND (effective_to IS NULL OR effective_to >= NOW()) - AND approvedid IS NOT NULL - ORDER BY podcast_credit_id)) AS credits - FROM uryplayer.podcast - LEFT JOIN schedule.show_podcast_link USING (podcast_id) - WHERE podcast_id=$1', array($podcast_id)); - - if (empty($result)) { - throw new MyRadioException('Podcast ' . $podcast_id, ' does not exist.', 404); - } - - $this->file = $result['file']; - $this->memberid = (int) $result['memberid']; - $this->approvedid = (int) $result['approvedid']; - $this->submitted = strtotime($result['submitted']); - $this->show_id = (int) $result['show_id']; - - //Deal with the Credits arrays - $credit_types = self::$db->decodeArray($result['credit_types']); - $credits = self::$db->decodeArray($result['credits']); - - for ($i = 0; $i < sizeof($credits); $i++) { - if (empty($credits[$i])) { - continue; - } - $this->credits[] = array('type' => (int) $credit_types[$i], - 'memberid' => $credits[$i], - 'User' => MyRadio_User::getInstance($credits[$i])); - } - - - //Deal with the Metadata arrays - $metadata_types = self::$db->decodeArray($result['metadata_types']); - $metadata = self::$db->decodeArray($result['metadata']); - - for ($i = 0; $i < sizeof($metadata); $i++) { - if (self::isMetadataMultiple($metadata_types[$i])) { - //Multiples should be an array - $this->metadata[$metadata_types[$i]][] = $metadata[$i]; - } else { - $this->metadata[$metadata_types[$i]] = $metadata[$i]; - } - } - } - - /** - * Get all the Podcasts that the User is Owner of Creditor of. - * @param MyRadio_User $user Default current user. - * @return MyRadio_Podcast[] - */ - public static function getPodcastsAttachedToUser(MyRadio_User $user = null) { - return self::resultSetToObjArray(self::getPodcastIDsAttachedToUser($user)); -} +class MyRadio_Podcast extends MyRadio_Metadata_Common +{ + /** + * The Podcast's ID. + * + * @var int + */ + private $podcast_id; + + /** + * The path to the file, relative to Config::$public_media_uri. + * + * @var string + */ + private $file; + + /** + * The Time the Podcast was uploaded. + * + * @var int + */ + private $submitted; + + /** + * If the Podcast has been suspended. + * + * @var bool + */ + private $suspended; + + /** + * The ID of the User that uploaded the Podcast. + * + * @var int + */ + private $memberid; + + /** + * The ID of the User that approved the Podcast. + * + * @var int + */ + private $approvedid; + + /** + * Array of Users and their relation to the Podcast. + * + * @var array + */ + protected $credits = []; + + /** + * The ID of the show this is linked to, if any. + * + * @var int + */ + private $show_id; + + /** + * Construct the API Key Object. + * + * @param string $key + */ + protected function __construct($podcast_id) + { + $this->podcast_id = (int) $podcast_id; + + $result = self::$db->fetchOne( + 'SELECT file, memberid, approvedid, submitted, suspended, show_id, ( + SELECT array_to_json(array( + SELECT metadata_key_id FROM uryplayer.podcast_metadata + WHERE podcast_id=$1 AND effective_from <= NOW() + ORDER BY effective_from, podcast_metadata_id + )) + ) AS metadata_types, ( + SELECT array_to_json(array( + SELECT metadata_value FROM uryplayer.podcast_metadata + WHERE podcast_id=$1 AND effective_from <= NOW() + ORDER BY effective_from, podcast_metadata_id + )) + ) AS metadata, ( + SELECT array_to_json(array( + SELECT metadata_value FROM uryplayer.podcast_image_metadata + WHERE podcast_id=$1 AND effective_from <= NOW() + ORDER BY effective_from, podcast_image_metadata_id + )) + ) AS image_metadata, ( + SELECT array_to_json(array( + SELECT credit_type_id FROM uryplayer.podcast_credit + WHERE podcast_id=$1 AND effective_from <= NOW() + AND (effective_to IS NULL OR effective_to >= NOW()) + AND approvedid IS NOT NULL + ORDER BY podcast_credit_id + )) + ) AS credit_types, ( + SELECT array_to_json(array( + SELECT creditid FROM uryplayer.podcast_credit + WHERE podcast_id=$1 AND effective_from <= NOW() + AND (effective_to IS NULL OR effective_to >= NOW()) + AND approvedid IS NOT NULL + ORDER BY podcast_credit_id + )) + ) AS credits + FROM uryplayer.podcast + LEFT JOIN schedule.show_podcast_link USING (podcast_id) + WHERE podcast_id=$1', + [$podcast_id] + ); + + if (empty($result)) { + throw new MyRadioException('Podcast '. $podcast_id . ' does not exist.', 404); + } + + $this->file = $result['file']; + $this->memberid = (int) $result['memberid']; + $this->approvedid = (int) $result['approvedid']; + $this->submitted = strtotime($result['submitted']); + $this->suspended = ($result['suspended'] === 't') ? true : false; + $this->show_id = (int) $result['show_id']; + + //Deal with the Credits arrays + $credit_types = json_decode($result['credit_types']); + $credits = json_decode($result['credits']); + + for ($i = 0; $i < sizeof($credits); ++$i) { + if (empty($credits[$i])) { + continue; + } + $this->credits[] = [ + 'type' => (int) $credit_types[$i], + 'memberid' => $credits[$i], + 'User' => MyRadio_User::getInstance($credits[$i]), + ]; + } + + //Deal with the Metadata arrays + $metadata_types = json_decode($result['metadata_types']); + $metadata = json_decode($result['metadata']); + + for ($i = 0; $i < sizeof($metadata); ++$i) { + if (self::isMetadataMultiple($metadata_types[$i])) { + //Multiples should be an array + $this->metadata[$metadata_types[$i]][] = $metadata[$i]; + } else { + $this->metadata[$metadata_types[$i]] = $metadata[$i]; + } + } + } + + /** + * Get all the Podcasts that the User is Owner of Creditor of. + * + * @param int|string|MyRadio_User $user Default current user. + * + * @return MyRadio_Podcast[] + */ + public static function getPodcastsAttachedToUser($user = null) + { + return self::resultSetToObjArray(self::getPodcastIDsAttachedToUser($user)); + } + + /** + * Get the IDs of all the Podcasts that the User is Owner of Creditor of. + * + * @param int|string|MyRadio_User $user Default current user. + * + * @return int[] + */ + public static function getPodcastIDsAttachedToUser($user = null) + { + if ($user === null) { + $user = MyRadio_User::getInstance(); + } elseif (is_int($user)) { + $user = MyRadio_User::getInstance($user); + } elseif (is_string($user) && is_numeric($user)) { + $user = MyRadio_User::getInstance(intval($user)); + } elseif (!($user instanceof MyRadio_User)) { + throw new MyRadioException('Invalid user input'); + } + + return self::$db->fetchColumn( + 'SELECT podcast_id FROM uryplayer.podcast + WHERE memberid=$1 OR podcast_id IN ( + SELECT podcast_id FROM uryplayer.podcast_credit + WHERE creditid=$1 AND effective_from <= NOW() + AND (effective_to >= NOW() OR effective_to IS NULL) + )', + [$user->getID()] + ); + } + + public static function getPending() + { + self::initDB(); + + return self::resultSetToObjArray( + self::$db->fetchColumn( + 'SELECT podcast_id FROM uryplayer.podcast WHERE submitted IS NULL' + ) + ); + } + + public static function getForm() + { + $form = ( + new MyRadioForm( + 'createpodcastfrm', + 'Podcast', + 'editPodcast', + [ + 'title' => 'Podcasts', + 'subtitle' => 'Create Podcast' + ] + ) + )->addField( + new MyRadioFormField( + 'title', + MyRadioFormField::TYPE_TEXT, + ['label' => 'Title'] + ) + )->addField( + new MyRadioFormField( + 'description', + MyRadioFormField::TYPE_BLOCKTEXT, + ['label' => 'Description'] + ) + )->addField( + new MyRadioFormField( + 'tags', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Tags', + 'explanation' => 'A set of keywords to describe your podcast generally, seperated with commas.', + ] + ) + ); + + //Get User's shows, or all shows if they have AUTH_PODCASTANYSHOW + //Format them into a select field format. + $auth = MyRadio_User::getInstance()->hasAuth(AUTH_PODCASTANYSHOW); + $shows = array_map( + function ($x) { + return ['text' => $x->getMeta('title'), 'value' => $x->getID()]; + }, + $auth ? MyRadio_Show::getAllShows() : MyRadio_User::getInstance()->getShows() + ); + + //Add an option for not attached to a show + if (MyRadio_User::getInstance()->hasAuth(AUTH_STANDALONEPODCAST)) { + $shows = array_merge([['text' => 'Standalone']], $shows); + } + + $photos = array_merge( + [[]], // Blank Option if they want to upload a new cover photo + array_map( + function ($pod) { + return ['text' => $pod->getMeta('title'), 'value' => $pod->getCover()]; + }, + self::getPodcastsAttachedToUser() + ) + ); + + + $form->addField( + new MyRadioFormField( + 'show', + MyRadioFormField::TYPE_SELECT, + [ + 'options' => $shows, + 'explanation' => 'This Podcast will be attached to the Show you select here.', + 'label' => 'Show', + 'required' => false, + ] + ) + )->addField( + new MyRadioFormField( + 'credits', + MyRadioFormField::TYPE_TABULARSET, + [ + 'label' => 'Credits', + 'options' => [ + new MyRadioFormField( + 'member', + MyRadioFormField::TYPE_MEMBER, + [ + 'explanation' => '', + 'label' => 'Person', + ] + ), + new MyRadioFormField( + 'credittype', + MyRadioFormField::TYPE_SELECT, + [ + 'options' => array_merge( + [ + [ + 'text' => 'Please select...', + 'disabled' => true, + ], + ], + MyRadio_Scheduler::getCreditTypes() + ), + 'explanation' => '', + 'label' => 'Role', + ] + ), + ], + ] + ) + )->addField( + new MyRadioFormField( + 'file', + MyRadioFormField::TYPE_FILE, + [ + 'label' => 'Audio', + 'explanation' => 'Upload the original, high-quality audio for' + .' this podcast. We\'ll publish a version optimised for the web' + .' and archive the original. Max size 500MB.', + 'options' => ['progress' => true], + ] + ) + )->addField( + new MyRadioFormField( + 'existing_cover', + MyRadioFormField::TYPE_SELECT, + [ + 'options' => $photos, + 'label' => 'Existing Cover Photo', + 'explanation' => 'To use an existing cover photo of another podcast, ' + . 'select the podcast you\'d like to use the same image as. ' + . 'For new images, keep blank.', + 'required' => false, + ] + ) + )->addField( + new MyRadioFormField( + 'new_cover', + MyRadioFormField::TYPE_FILE, + [ + 'label' => 'Upload New Cover Photo', + 'explanation' => 'If you haven\'t specified an existing cover photo, upload one ' + . 'here - it should be square and at least 3000x3000 pixels.', + 'required' => false, + ] + ) + )->addField( + new MyRadioFormField( + 'terms', + MyRadioFormField::TYPE_CHECK, + [ + 'label' => 'I have read and confirm that this audio file complies' + .' with ' + .Config::$short_name.'\'s Podcasting Policy.' + ] + ) + ); + + return $form; + } + + public function getEditForm() + { + return self::getForm() + ->setSubtitle('Edit Podcast') + ->editMode( + $this->getID(), + [ + 'title' => $this->getMeta('title'), + 'description' => $this->getMeta('description'), + 'tags' => is_null($this->getMeta('tag')) ? null : implode(', ', $this->getMeta('tag')), + 'show' => empty($this->show_id) ? null : $this->show_id, + 'credits.member' => array_map( + function ($credit) { + return $credit['User']; + }, + $this->getCredits() + ), + 'credits.credittype' => array_map( + function ($credit) { + return $credit['type']; + }, + $this->getCredits() + ), + 'existing_cover' => $this->getCover(), + 'terms' => 'on', + ] + ); + } + + public static function getSuspendForm() + { + return ( + new MyRadioForm( + "suspendpodcastfrm", + "Podcast", + "suspendPodcast", + [ + "title" => "Podcasts", + "subtitle" => "Suspend Podcast" + ] + ) + )->addField( + new MyRadioFormField( + "confirm", + MyRadioFormField::TYPE_CHECK, + [ + "label" => "I'm sure I want to suspend this podcast" + ] + ) + )->addField( + new MyRadioFormField( + "podcast_id", + MyRadioFormField::TYPE_HIDDEN, + ["value" => $_REQUEST["podcast_id"]] + ) + ); + } + + public static function getUnsuspendForm() + { + return ( + new MyRadioForm( + "unsuspendpodcastfrm", + "Podcast", + "suspendPodcast", + [ + "title" => "Podcasts", + "subtitle" => "Request to Unsuspend Podcast" + ] + ) + )->addField( + new MyRadioFormField( + "reason", + MyRadioFormField::TYPE_BLOCKTEXT, + ["label" => "Please explain why this podcast should be unsuspended"] + ) + )->addField( + new MyRadioFormField( + "podcast_id", + MyRadioFormField::TYPE_HIDDEN, + ["value" => $_REQUEST["podcast_id"]] + ) + ); + } + + /** + * Create a new Podcast. + * + * @param string $title The Podcast's title + * @param string $description The Podcast's description + * @param array $tags An array of String tags + * @param string $file The local filesystem path to the Podcast file + * @param MyRadio_Show $show The show to attach the Podcast to + * @param array $credits Credit data. Format compatible with a credit TABULARSET (see Scheduler) + */ + public static function create( + $title, + $description, + $tags, + $file, + MyRadio_Show $show = null, + $credits = null + ) { + //Validate the tags + $tags = CoreUtils::explodeTags($tags); + + self::$db->query('BEGIN'); + + //Get an ID for the new Podcast + $id = (int) self::$db->fetchColumn( + 'INSERT INTO uryplayer.podcast ' + .'(memberid, approvedid, submitted) VALUES ($1, $1, NULL) ' + .'RETURNING podcast_id', + [MyRadio_User::getInstance()->getID()] + )[0]; + + // DANGER WILL ROBINSON DANGER + /** @var self $podcast */ + $podcast = self::getInstance($id); + + $podcast->setMeta('title', $title); + $podcast->setMeta('description', $description); + $podcast->setMeta('tag', $tags); + $podcast->setCredits($credits['member'], $credits['credittype']); + if (!empty($show)) { + $podcast->setShow($show); + } + + // Pre-emptively write the current state of this object to the cache. + // We need to do this, because the self::getInstance() call above + // poisoned the cache with a NULL $podcast->metadata. So anything + // that tries to read it from now on will hit the cache and get + // an outdated copy. The only remedy is to, effectively, re-poison it + // with the correct value. Yes, this is absolutely cursed. + // + // This is, however, safe: nothing should be able to know the podcast's + // ID until the COMMIT statement afterwards. So by the time it + // has an ID, it will have a freshly baked, correct value waiting + // for it in the cache. + // + // This is not a unique problem; in theory, every ServiceAPI subclass + // is vulnerable. + // + // This is more likely to happen to podcasts than any other type, + // because podcasts are immediately touched by the podcast daemon upon + // creation, which will hold on to the cached instance while it + // converts the file, which may take _a while_ - and then write + // back the copy, poisoning the cache until it expires or gets flushed. + // Putting $this->updateCacheObject() (or even a self::$cache->purge()) + // at the end of this method will not help, because by the time this + // function finishes the daemon will have already obtained a reference + // to a poisoned copy. + + $podcast->updateCacheObject(true); + self::$db->query('COMMIT'); + + //Ship the file off to the archive location to be converted + if (!move_uploaded_file($file, $podcast->getArchiveFile())) { + throw new MyRadioException( + "Failed to move podcast file $file to {$podcast->getArchiveFile()}", + 500 + ); + } + self::$cache->purge(); + return $podcast; + } + + /** + * Create a new Podcast Cover. + * + * @param string $temporary_file The image file uploaded. + */ + public function createCover($temporary_file) + { + if (empty($temporary_file)) { + throw new MyRadioException('No new cover file uploaded.', 400); + } + + $path = '/image_meta/MyRadioImageMetadata/' + .'podcast' + .$this->getID() + .'-' + .time() + .'.' + .explode('/', getimagesize($temporary_file)['mime'])[1]; + + $file_path = Config::$public_media_path.$path; + + if (file_exists($file_path)) { + throw new MyRadioException('The cover filename chosen already exists.', 500); + } + + move_uploaded_file($temporary_file, $file_path); + if (!file_exists($file_path)) { + throw new MyRadioException('File move failed.', 500); + } + + $this->setCover($path); + } + + /** + * Get the Podcast ID. + * + * @return int + */ + public function getID() + { + return $this->podcast_id; + } + + /** + * Get the Show this Podcast is linked to, if there is one. + * + * @return MyRadio_Show + */ + public function getShow() + { + if (!empty($this->show_id)) { + return MyRadio_Show::getInstance($this->show_id); + } else { + return; + } + } - /** - * Get the IDs of all the Podcasts that the User is Owner of Creditor of. - * @param MyRadio_User $user Default current user. - * @return int[] - */ - public static function getPodcastIDsAttachedToUser(MyRadio_User $user = null) { - if ($user === null) { - $user = MyRadio_User::getInstance(); - } - - return self::$db->fetch_column('SELECT podcast_id FROM uryplayer.podcast - WHERE memberid=$1 OR podcast_id IN - (SELECT podcast_id FROM uryplayer.podcast_credit - WHERE creditid=$1 AND effective_from <= NOW() AND - (effective_to >= NOW() OR effective_to IS NULL))', [$user->getID()]); - } - - public static function getPending() { - self::initDB(); - return self::resultSetToObjArray(self::$db->fetch_column('SELECT podcast_id ' - . 'FROM uryplayer.podcast WHERE submitted IS NULL')); - } - - public static function getCreateForm() { - $form = (new MyRadioForm('createpodcastfrm', 'Podcast', 'doCreatePodcast', - ['title' => 'Create Podcast'])) - ->addField(new MyRadioFormField('title', MyRadioFormField::TYPE_TEXT, [ - 'label' => 'Title' - ]))->addField(new MyRadioFormField('description', MyRadioFormField::TYPE_BLOCKTEXT, [ - 'label' => 'Description' - ]))->addField(new MyRadioFormField('tags', MyRadioFormField::TYPE_TEXT, [ - 'label' => 'Tags', - 'explanation' => 'A set of keywords to describe your podcast ' - . 'generally, seperated with spaces.' - ])); - - //Get User's shows, or all shows if they have AUTH_PODCASTANYSHOW - //Format them into a select field format. - $shows = array_map(function($x) { - return ['text' => $x->getMeta('title'), 'value' => $x->getID()]; - }, MyRadio_User::getInstance()->hasAuth(AUTH_PODCASTANYSHOW) ? - MyRadio_Show::getAllShows() - : MyRadio_User::getInstance()->getShows()); - - //Add an option for not attached to a show - if (MyRadio_User::getInstance()->hasAuth(AUTH_STANDALONEPODCAST)) { - $shows = array_merge([['text' => 'Standalone']], $shows); - } - - $form->addField(new MyRadioFormField('show', MyRadioFormField::TYPE_SELECT, [ - 'options' => $shows, - 'explanation' => 'This Podcast will be attached to the ' - . 'Show you select here.', - 'label' => 'Show', - 'required' => false - ]))->addField(new MyRadioFormField('credits', MyRadioFormField::TYPE_TABULARSET, [ - 'label' => 'Credits', 'options' => [ - new MyRadioFormField('member', MyRadioFormField::TYPE_MEMBER, [ - 'explanation' => '', - 'label' => 'Person' - ]), - new MyRadioFormField('credittype', MyRadioFormField::TYPE_SELECT, [ - 'options' => array_merge([['text' => 'Please select...', - 'disabled' => true]], MyRadio_Scheduler::getCreditTypes()), - 'explanation' => '', - 'label' => 'Role' - ])]]))->addField(new MyRadioFormField('file', MyRadioFormField::TYPE_FILE, [ - 'label' => 'Audio', - 'explanation' => 'Upload the original, high-quality audio for' - . ' this podcast. We\'ll publish a version optimised for the web' - . ' and archive the original. Max size 500MB.', - 'options' => ['progress' => true] - ]))->addField(new MyRadioFormField('terms', MyRadioFormField::TYPE_CHECK, [ - 'label' => 'I have read and confirm that this audio file complies' - . ' with ' - . Config::$short_name . '\'s Podcasting Policy.' - ])); - - return $form; - } - - /** - * Create a new Podcast - * @param String $title The Podcast's title - * @param String $description The Podcast's description - * @param Array $tags An array of String tags - * @param String $file The local filesystem path to the Podcast file - * @param MyRadio_Show $show The show to attach the Podcast to - * @param Array $credits Credit data. Format compatible with a credit - * TABULARSET (see Scheduler) - */ - public static function create($title, $description, $tags, $file, - MyRadio_Show $show = null, $credits = null) { - - //Get an ID for the new Podcast - $id = (int)self::$db->fetch_column('INSERT INTO uryplayer.podcast ' - . '(memberid, approvedid, submitted) VALUES ($1, $1, NULL) ' - . 'RETURNING podcast_id', [MyRadio_User::getInstance()->getID()])[0]; - - $podcast = self::getInstance($id); - - $podcast->setMeta('title', $title); - $podcast->setMeta('description', $description); - $podcast->setMeta('tag', $tags); - $podcast->setCredits($credits['member'], $credits['credittype']); - if (!empty($show)) { - $podcast->setShow($show); - } - - //Ship the file off to the archive location to be converted - if (!move_uploaded_file($file, $podcast->getArchiveFile())) { - throw new MyRadioException("Failed to move podcast file ". - "$file to {$podcast->getArchiveFile()}", 500); - } - } - - /** - * Get the Podcast ID - * @return int - */ - public function getID() { - return $this->podcast_id; - } - - /** - * Get the Show this Podcast is linked to, if there is one. - * @return MyRadio_Show - */ - public function getShow() { - if (!empty($this->show_id)) { - return MyRadio_Show::getInstance($this->show_id); - } else { - return null; - } - } - - /** - * Returns a human-readable explanation of the Podcast's state. - * @return String - */ - public function getStatus() { - if (empty($this->submitted)) { - return 'Processing...'; - } elseif ($this->submitted > time()) { - return 'Scheduled for publication ('.CoreUtils::happyTime($this->submitted).')'; - } else { - return 'Published'; - } - } - - /** - * Get the file system path to where the original file is stored. - * @return String - */ - public function getArchiveFile() { - return Config::$podcast_archive_path.'/'.$this->getID().'.orig'; - } - - /** - * Get the file system path to where the web file should be stored - * @return String - */ - public function getWebFile() { - return Config::$public_media_path.'/podcasts/MyRadioPodcast'.$this->getID().'.mp3'; - } - - /** - * Get the value that *should* be stored in uryplayer.podcast.file - * @return String - */ - public function getFile() { - return 'podcasts/MyRadioPodcast'.$this->getID().'.mp3'; - } - - /** - * Set the Show this Podcast is linked to. If null, removes any link. - * @param MyRadio_Show $show - */ - public function setShow(MyRadio_Show $show) { - self::$db->query('DELETE FROM schedule.show_podcast_link ' - . 'WHERE podcast_id=$1', [$this->getID()]); - - if (!empty($show)) { - self::$db->query('INSERT INTO schedule.show_podcast_link ' - . '(show_id, podcast_id) VALUES ($1, $2)', - [$show->getID(), $this->getID()]); - $this->show_id = $show->getID(); - } else { - $this->show_id = null; - } - - } - - /** - * Get data in array format - * @param boolean $full If true, returns more data. - * @return Array - */ - public function toDataSource($full = true) { - $data = array( - 'podcast_id' => $this->getID(), - 'title' => $this->getMeta('title'), - 'description' => $this->getMeta('description'), - 'status' => $this->getStatus(), - 'editlink' => array( - 'display' => 'icon', - 'value' => 'script', - 'title' => 'Edit Podcast', - 'url' => CoreUtils::makeURL('Podcast', 'editPodcast', array('podcastid' => $this->getID()))) - ); - - if ($full) { - $data['credits'] = implode(', ', $this->getCreditsNames(false)); - $data['show'] = $this->getShow() ? - $this->getShow()->toDataSource(false) : null; - } - - return $data; - } - - /** - * Sets the current podcast cover for this podcast. - * - * @param string $url The URL of the incoming podcast cover. - */ - public function setCover($url) { - // TODO: Plumb this into the metadata system. - // At time of writing, MyRadio's metadata system doesn't do images. - if (empty($url)) { - throw new MyRadioException('URL is blank.'); - } - - self::$db->query(' - INSERT INTO - uryplayer.podcast_image_metadata( - metadata_key_id, podcast_id, memberid, approvedid, - metadata_value, effective_from, effective_to - ) - VALUES - (10, $1, $2, $2, $3, NOW(), NULL), - (11, $1, $2, $2, $3, NOW(), NULL) - ', - [ - $this->getID(), - MyRadio_User::getInstance()->getID(), - $url - ] - ); - } - - /** - * Gets the current podcast cover for this podcast. - * - * @return string The URL of the current podcast cover. - */ - public function getCover() { - // TODO: Plumb this into the metadata system. - // At time of writing, MyRadio's metadata system doesn't do images. - return self::$db->fetch_one(' - SELECT - metadata_value AS url - FROM - uryplayer.podcast_image_metadata - WHERE - podcast_id = $1 - AND - effective_from <= NOW() - AND - (effective_to IS NULL OR effective_to > NOW()) - ORDER BY - effective_from DESC - LIMIT 1 - ;', - [$this->getID()] - )['url']; - } - - - /** - * Sets a metadata key to the specified value. - * - * If any value is the same as an existing one, no action will be taken. - * If the given key has is_multiple, then the value will be added as a new, additional key. - * If the key does not have is_multiple, then any existing values will have effective_to - * set to the effective_from of this value, effectively replacing the existing value. - * This will *not* unset is_multiple values that are not in the new set. - * - * @param String $string_key The metadata key - * @param mixed $value The metadata value. If key is_multiple and value is an array, will create instance - * for value in the array. - * @param int $effective_from UTC Time the metavalue is effective from. Default now. - * @param int $effective_to UTC Time the metadata value is effective to. Default NULL (does not expire). - * @param null $table Used for compatibility with parent. - * @param null $pkey Used for compatibility with parent. - */ - public function setMeta($string_key, $value, $effective_from = null, $effective_to = null, $table = null, $pkey = null) { - parent::setMeta($string_key, $value, $effective_from, $effective_to, 'uryplayer.podcast_metadata', 'podcast_id'); - } - - /** - * Updates the list of Credits. - * - * Existing credits are kept active, ones that are not in the new list are set to effective_to now, - * and ones that are in the new list but not exist are created with effective_from now. - * - * @param MyRadio_User[] $users An array of Users associated. - * @param int[] $credittypes The relevant credittypeid for each User. - */ - public function setCredits($users, $credittypes, $table = null, $pkey = null) { - parent::setCredits($users, $credittypes, 'uryplayer.podcast_credit', 'podcast_id'); - } - - /** - * Set the time that the Podcast is submitted as visible on the website. - * @param int $time - */ - public function setSubmitted($time) { - $this->submitted = $time; - self::$db->query('UPDATE uryplayer.podcast SET submitted=$1 ' - . 'WHERE podcast_id=$2', - [CoreUtils::getTimestamp($time), $this->getID()]); - } - - /** - * Convert the Archive file to the Web format. - * - * If the preferred format is changed, re-run this on every Podcast to - * reencode them. - */ - public function convert() { - $tmpfile = $this->getArchiveFile(); - $dbfile = $this->getWebFile(); - shell_exec("nice -n 15 ffmpeg -i '$tmpfile' -ab 192k -f mp3 - >'{$dbfile}'"); - - self::$db->query('UPDATE uryplayer.podcast SET file=$1 WHERE podcast_id=$2', - [$this->getFile(), $this->getID()]); - if (empty($this->submitted)) { - $this->setSubmitted(time()); - } - } + /** + * Returns a human-readable explanation of the Podcast's state. + * + * @return string + */ + public function getStatus() + { + if ($this->suspended) { + return 'Suspended'; + } elseif (empty($this->submitted)) { + return 'Processing...'; + } elseif ($this->submitted > time()) { + return 'Scheduled for publication ('.CoreUtils::happyTime($this->submitted).')'; + } else { + return 'Published'; + } + } + + /** + * Returns if the Podcast is suspended. + * + * @return bool + */ + public function isSuspended() + { + return $this->suspended; + } + + /** + * Whether this podcast should be live right now + * @return bool + */ + public function isPublished() + { + return !$this->isSuspended() + && !empty($this->submitted) + && $this->submitted < time(); + } + + /** + * Get the file system path to where the original file is stored. + * + * @return string + */ + public function getArchiveFile() + { + return Config::$podcast_archive_path.'/'.$this->getID().'.orig'; + } + + /** + * Get the file system path to where the web file should be stored. + * + * @return string + */ + public function getWebFile() + { + return Config::$public_media_path.'/podcasts/MyRadioPodcast'.$this->getID().'.mp3'; + } + + /** + * Get the value that *should* be stored in uryplayer.podcast.file when a new podcast is created. + * + * @return string + */ + public function getFile() + { + return 'podcasts/MyRadioPodcast'.$this->getID().'.mp3'; + } + + /** + * Get the web uri for the podcast. + * + * @return string + */ + public function getURI() + { + return Config::$public_media_uri.'/'.$this->file; + } + + /** + * Get the time the podcast is due to be, or was published. + * + * @return int + */ + public function getSubmitted() + { + return $this->submitted; + } + + /** + * Get the microsite URI. + * + * @return string + */ + public function getWebpage() + { + return '/uryplayer/podcasts/'.$this->getID(); + } + + /** + * Set the suspended status of this podcast. + * + * @param bool $is_suspended + */ + public function setSuspended(bool $is_suspended) + { + $this->suspended = $is_suspended; + self::$db->query( + 'UPDATE uryplayer.podcast SET suspended=$1 + WHERE podcast_id=$2', + [$this->isSuspended(), $this->getID()] + ); + + return $this; + } + + /** + * Request a podcast to be suspended + * + * @param string $reason - The reason the user wants unsuspension + */ + + public function requestUnsuspend($reason) + { + if (AuthUtils::hasPermission(AUTH_PODCASTANYSHOW)) { + $this->setSuspended(false); + } else { + MyRadioEmail::sendEmailToList( + MyRadio_List::getByName("podcasting"), + "Request for podcast unsuspension", + "Hi! \r\n\r\n" + . "A request for podcast: " . $this->getID() + . " to be unsuspended has been sent. \r\n\r\n" + . "Reason: " . $reason + . "You can unsuspend this at " + . URLUtils::makeURL("Podcast", "suspendPodcast", ["podcast_id" => $this->getID()]) + . "\r\n\r\n MyRadio Podcasting" + ); + } + } + + /** + * Set the Show this Podcast is linked to. If null, removes any link. + * + * @param MyRadio_Show $show + */ + public function setShow($show) + { + self::$db->query( + 'DELETE FROM schedule.show_podcast_link + WHERE podcast_id=$1', + [$this->getID()] + ); + + if (!empty($show)) { + if ($show instanceof MyRadio_Show) { + self::$db->query( + 'INSERT INTO schedule.show_podcast_link + (show_id, podcast_id) VALUES ($1, $2)', + [$show->getID(), $this->getID()] + ); + $this->show_id = $show->getID(); + } else { + throw new MyRadioException("The parameter provided is not a MyRadio_Show instance.", 500); + } + } else { + $this->show_id = null; + } + + return $this; + } + + /** + * Get a GUID for iTunes + * @return string + */ + public function getGUID() + { + return 'https:' . Config::$website_url . $this->getWebpage(); + } + + /** + * Get data in array format. + * @param bool $include_suspended Whether to return a suspended podcast + * @param array $mixins Mixins. + * @mixin show Provides data about the show this podcast is from + * @mixin credits Returns the names of the credited people, as a comma-separated list + * @param bool $full If true, returns more data. + * + * @return array + */ + public function toDataSource($mixins = []) + { + $mixin_funcs = [ + 'show' => function (&$data) use ($mixins) { + $data['show'] = $this->getShow() ? + $this->getShow()->toDataSource($mixins) : null; + }, + 'credits' => function (&$data) { + $data['credits'] = implode(', ', $this->getCreditsNames(false)); + }, + ]; + $data = [ + 'podcast_id' => $this->getID(), + 'title' => $this->getMeta('title'), + 'description' => $this->getMeta('description'), + 'status' => $this->getStatus(), + 'time' => $this->getSubmitted(), + 'uri' => $this->getURI(), + 'editlink' => [ + 'display' => 'icon', + 'value' => 'pencil', + 'title' => 'Edit Podcast', + 'url' => URLUtils::makeURL('Podcast', 'editPodcast', ['podcast_id' => $this->getID()]), + ], + 'micrositelink' => [ + 'display' => 'icon', + 'value' => 'link', + 'title' => 'View Podcast Microsite', + 'url' => $this->getWebpage(), + ], + 'suspendlink' => [ + 'display' => 'text', + 'title' => 'Click to suspend/ususpend podcast', + 'value' => ($this->isSuspended() ? "Unsuspend Podcast" : "Suspend Podcast"), + 'url' => URLUtils::makeURL("Podcast", "suspendPodcast", ["podcast_id" => $this->getID()]) + ] + ]; + + $cover = $this->getCover(); + if (!empty($cover)) { + $cover_path = Config::$public_media_uri . '/' . $cover; + $cover_path = preg_replace('(//)', '/', $cover_path); + $data['photo'] = $cover_path; + } else { + $data["photo"] = ""; + } + + $this->addMixins($data, $mixins, $mixin_funcs); + return $data; + } + + /** + * Sets the current podcast cover for this podcast. + * + * @param string $url The URL of the incoming podcast cover. + */ + public function setCover($url) + { + // TODO: Plumb this into the metadata system. + // At time of writing, MyRadio's metadata system doesn't do images. + if (empty($url)) { + throw new MyRadioException('URL is blank.'); + } + + self::$db->query( + 'INSERT INTO uryplayer.podcast_image_metadata + (metadata_key_id, podcast_id, memberid, approvedid, + metadata_value, effective_from, effective_to) + VALUES + (10, $1, $2, $2, $3, NOW(), NULL), + (11, $1, $2, $2, $3, NOW(), NULL)', + [ + $this->getID(), + MyRadio_User::getInstance()->getID(), + $url, + ] + ); + + return $this; + } + + /** + * Gets the current podcast cover for this podcast. + * + * @return string The URL of the current podcast cover. + */ + public function getCover() + { + // TODO: Plumb this into the metadata system. + // At time of writing, MyRadio's metadata system doesn't do images. + $result = self::$db->fetchOne( + 'SELECT metadata_value AS url + FROM uryplayer.podcast_image_metadata + WHERE podcast_id = $1 + AND effective_from <= NOW() + AND (effective_to IS NULL OR effective_to > NOW()) + ORDER BY effective_from DESC + LIMIT 1;', + [$this->getID()] + ); + return $result ? $result['url'] : Config::$public_media_uri . "/image_meta/PodcastImageMetadata/default.png"; + } + + /** + * Searches searchable *text* metadata for the specified value. Does not work for image metadata. + * if $q is set, then $path_query must be set to "search". This allows the query to be given in path or in parameters. + + * + * @todo effective_from/to not yet implemented + * + * @param string $path_query The query value encoded in the path (DEPRECATED). + * @param string $q The query value as a query string. to use this, $path_query must be set to "search" + * @param array $string_keys The metadata keys to search + * @param int $effective_from UTC Time to search from. + * @param int $effective_to UTC Time to search to. + * + * @return array The shows that match the search terms + */ + public static function searchMeta($path_query, $q="", $string_keys = null, $effective_from = null, $effective_to = null) + { + if($path_query != "search" && $q != "") { + throw new MyRadioException( + "the path_query must be set to 'search' if q is set in the query string", + 400 + ); + } + if ($path_query == "search" && $q != ""){ + $query = $q; + } else { + $query = $path_query; + } + if (is_null($string_keys)) { + $string_keys = ['title', 'description', 'tag']; + } + + $r = parent::searchMetaBase( + $query, + $string_keys, + $effective_from, + $effective_to, + 'uryplayer.podcast_metadata', + 'podcast_id' + ); + return self::resultSetToObjArray($r); + } + + /** + * Sets a metadata key to the specified value. + * + * If any value is the same as an existing one, no action will be taken. + * If the given key has is_multiple, then the value will be added as a new, additional key. + * If the key does not have is_multiple, then any existing values will have effective_to + * set to the effective_from of this value, effectively replacing the existing value. + * This will *not* unset is_multiple values that are not in the new set. + * + * @param string $string_key The metadata key + * @param mixed $value The metadata value. If key is_multiple and value is an array, will create instance + * for value in the array. + * @param int $effective_from UTC Time the metavalue is effective from. Default now. + * @param int $effective_to UTC Time the metadata value is effective to. Default NULL (does not expire). + */ + public function setMeta($string_key, $value, $effective_from = null, $effective_to = null) + { + $result = parent::setMetaBase( + $string_key, + $value, + $effective_from, + $effective_to, + 'uryplayer.podcast_metadata', + 'podcast_id' + ); + $this->updateCacheObject(); + return $this; + } + + /** + * Updates the list of Credits. + * + * Existing credits are kept active, ones that are not in the new list are set to effective_to now, + * and ones that are in the new list but not exist are created with effective_from now. + * + * @param MyRadio_User[] $users An array of Users associated. + * @param int[] $credittypes The relevant credittypeid for each User. + */ + public function setCredits($users, $credittypes, $table = null, $pkey = null) + { + parent::setCredits($users, $credittypes, 'uryplayer.podcast_credit', 'podcast_id'); + + return $this; + } + + /** + * Set the time that the Podcast is submitted as visible on the website. + * + * @param int $time + */ + public function setSubmitted($time) + { + $this->submitted = $time; + self::$db->query( + 'UPDATE uryplayer.podcast SET submitted=$1 + WHERE podcast_id=$2', + [CoreUtils::getTimestamp($time), $this->getID()] + ); + $this->updateCacheObject(); + + return $this; + } + + /** + * Convert the Archive file to the Web format. + * + * If the preferred format is changed, re-run this on every Podcast to + * reencode them. + * @note See CoreUtils::encodeTrack + */ + public function convert() + { + $tmpfile = $this->getArchiveFile(); + $dbfile = $this->getWebFile(); + shell_exec("nice -n 15 ffmpeg -i '{$tmpfile}' -ab 128k -f mp3 -map 0:a '{$dbfile}'"); + + self::$db->query( + 'UPDATE uryplayer.podcast SET file=$1 WHERE podcast_id=$2', + [$this->getFile(), $this->getID()] + ); + if (empty($this->submitted)) { + $this->setSubmitted(time()); + } + + $this->file = $this->getFile(); + $this->updateCacheObject(true); + } + + /** + * Returns all Podcasts. Caches for 1h. + * + * @param int $num_results The number of results to return per page. 0 for all podcasts. + * @param int $page The page required. + * @param bool $include_suspended Whether to include suspended podcasts in the result + * @param bool $include_pending Whether to include pending (future publish/processing) podcasts in the result + * + * @return Array[MyRadio_Podcast] + */ + public static function getAllPodcasts( + $include_suspended = false, + $include_pending = false, + $num_results = 0, + $page = 1 + ) { + $where = ''; + if (!$include_suspended || !$include_pending) { + $where = 'WHERE '; + if (!$include_suspended) { + $where .= 'suspended = false'; + } + if (!$include_suspended && !$include_pending) { + $where .= ' AND '; + } + if (!$include_pending) { + $where .= 'submitted IS NOT NULL'; + } + } + + $filterLimit = $num_results == 0 ? 'ALL' : $num_results; + $filterOffset = $num_results * $page; + $query = "SELECT podcast_id FROM uryplayer.podcast $where "; + $query .= "ORDER BY submitted DESC OFFSET $filterOffset LIMIT $filterLimit;"; + + $result = self::$db->fetchColumn($query); + + $podcasts = []; + foreach ($result as $row) { + $podcast = self::getInstance($row); + $podcast->updateCacheObject(); + $podcasts[] = $podcast; + } + return $podcasts; + } } diff --git a/src/Classes/ServiceAPI/MyRadio_Quote.php b/src/Classes/ServiceAPI/MyRadio_Quote.php deleted file mode 100644 index fd4dbda16..000000000 --- a/src/Classes/ServiceAPI/MyRadio_Quote.php +++ /dev/null @@ -1,271 +0,0 @@ - - * @package MyRadio_Core - * @uses \Database - */ -class MyRadio_Quote extends ServiceAPI { - const GET_INSTANCE_SQL = ' - SELECT - * - FROM - people.quote - WHERE - quote_id = $1 - ;'; - - const GET_ALL_SQL = ' - SELECT - quote_id - FROM - people.quote - ORDER BY - date DESC - ;'; - - const INSERT_SQL = ' - INSERT INTO - people.quote(text, source, date) - VALUES - ($1, $2, $3); - ;'; - - const SET_TEXT_SQL = ' - UPDATE - people.quote - SET - text = $1 - WHERE - quote_id = $2; - ;'; - - const SET_SOURCE_SQL = ' - UPDATE - people.quote - SET - source = $1 - WHERE - quote_id = $2 - ;'; - - const SET_DATE_SQL = ' - UPDATE - people.quote - SET - date = $1 - WHERE - quote_id = $2 - ;'; - - /** - * The quote ID. - * @var int - */ - private $id; - - /** - * The quote itself. - * @var string - */ - private $text; - - /** - * The member who said the quote. - * @var User - */ - private $source; - - /** - * The date of the quote, as a UNIX timestamp. - * @var int - */ - private $date; - - /** - * The singleton store of all Quotes. - * @var array - */ - private static $quotes = []; - - /** - * Constructs a new MyRadio_Quote from the database. - * - * You should generally use MyRadio_Quote::getInstance instead. - * - * @param int $quote_id The numeric ID of the quote. - * - * @return MyRadio_Quote The quote with the given ID. - */ - protected function __construct($quote_id) { - $quote_data = self::$db->fetch_one( - self::GET_INSTANCE_SQL, - [$quote_id] - ); - if (empty($quote_data)) { - throw new MyRadioException('The specified Quote does not seem to exist.'); - return; - } - - $this->text = $quote_data['text']; - $this->source = MyRadio_User::getInstance($quote_data['source']); - $this->date = strtotime($quote_data['date']); - } - - /** - * Retrieves the quite with the given numeric ID. - * - * @param $quote_id The numeric ID of the quote. - * - * @return The quote release with the given ID. - */ - public static function getInstance($quote_id=-1) { - self::__wakeup(); - - if (!is_numeric($quote_id)) { - throw new MyRadioException( - 'Invalid Quote ID!', - MyRadioException::FATAL - ); - } - - if (!isset(self::$quotes[$quote_id])) { - self::$quotes[$quote_id] = new self($quote_id); - } - return self::$quotes[$quote_id]; - } - - /** - * Retrieves all current quotes. - * - * @return array An array of all active quotes. - */ - public function getAll() { - $quote_ids = self::$db->fetch_column(self::GET_ALL_SQL, []); - return array_map('MyRadio_Quote::getInstance', $quote_ids); - } - - /** - * @return int The quote ID. - */ - public function getID() { - return $this->id; - } - - /** - * @return string The quote text. - */ - public function getText() { - return $this->text; - } - - /** - * @return User The quote source. - */ - public function getSource() { - return $this->source; - } - - /** - * @return int The quote time, as a UNIX timestamp. - */ - public function getDate() { - return $this->date; - } - - /** - * Creates a new quote in the database. - * - * @param $data array An array of data to populate the row with. - * Must contain 'text', 'source' and 'date'. - * @return nothing. - */ - public function create($data) { - self::$db->query( - self::INSERT_SQL, - [ - $data['text'], - $data['source']->getID(), - date('%c', intval($data['date'])) // Expecting UNIX timestamp - ], - true - ); - } - - /** - * Sets this quote's text. - * @param string $text The quote text. - * @return MyRadio_Quote This object, for method chaining. - */ - public function setText($text) { - $this->text = $text; - return $this->set(SET_SOURCE_SQL, $text); - } - - /** - * Sets this quote's source. - * @param User $source The quote source. - * @return MyRadio_Quote This object, for method chaining. - */ - public function setSource($source) { - $this->source = $source; - return $this->set(SET_SOURCE_SQL, $source->getID()); - } - - /** - * Sets this quote's date. - * @param int|string $date The date, as a UNIX timestamp or date string. - * @return MyRadio_Quote This object, for method chaining. - */ - public function setDate($date) { - $this->date = $date; - return $this->set(SET_DATE_SQL, strtotime($date)); - } - - /** - * Sets a property on this quote. - * - * @param string $sql The SQL to use for setting this property. - * @param $value The value of the property to set on this quote. - * - * @return MyRadio_Quote This object, for method chaining. - */ - private function set($sql, $value) { - self::$db->query($sql, [$value, $this->getID()]); - return $this; - } - - - /** - * Converts this quote to a table data source. - * - * @return array The object as a data source. - */ - public function toDataSource() { - return [ - 'source' => $this->getSource()->getName(), - 'date' => strftime('%x', $this->getDate()), - 'text' => $this->getText(), - /*'editlink' => [ - 'display' => 'icon', - 'value' => 'script', - 'title' => 'Edit Quote', - 'url' => CoreUtils::makeURL( - 'Charts', - 'editQuote', - ['quote_id' => $this->getID()] - ) - ],*/ - ]; - } -} - -?> diff --git a/src/Classes/ServiceAPI/MyRadio_Scheduler.php b/src/Classes/ServiceAPI/MyRadio_Scheduler.php index b068eec61..20d1bcb5a 100644 --- a/src/Classes/ServiceAPI/MyRadio_Scheduler.php +++ b/src/Classes/ServiceAPI/MyRadio_Scheduler.php @@ -1,222 +1,241 @@ - * @version 20130813 - * @package MyRadio_Scheduler - * @uses \Database - * @todo Dedicated Term class + * @todo Dedicated Term class */ -class MyRadio_Scheduler extends MyRadio_Metadata_Common { - /** - * This provides a temporary cache of the result from pendingAllocationsQuery - * @var Array - */ - private static $pendingAllocationsResult = null; - - /** - * Returns an Array of pending Season allocations. - * @return Array An Array of MyRadio_Season objects which do not have an allocated timeslot, ordered by time submitted - * @todo Move to MyRadio_Season? - */ - private static function pendingAllocationsQuery() { - if (self::$pendingAllocationsResult === null) { - /** - * Must not be null - otherwise it hasn't been submitted yet - */ - $result = - self::$db->fetch_column('SELECT show_season_id FROM schedule.show_season - WHERE show_season_id NOT IN (SELECT show_season_id FROM schedule.show_season_timeslot) - AND submitted IS NOT NULL - ORDER BY submitted ASC'); - - self::$pendingAllocationsResult = array(); - foreach ($result as $application) { - self::$pendingAllocationsResult[] = MyRadio_Season::getInstance($application); - } +class MyRadio_Scheduler extends ServiceAPI +{ + /** + * This provides a temporary cache of the result from pendingAllocationsQuery. + * + * @var array + */ + private static $pendingAllocationsResult = null; + + /** + * Returns an Array of pending Season allocations. + * + * @return array An Array of MyRadio_Season objects which do not have an allocated timeslot, + * ordered by time submitted + * + * @todo Move to MyRadio_Season? + */ + private static function pendingAllocationsQuery() + { + if (self::$pendingAllocationsResult === null) { + /* + * Must not be null - otherwise it hasn't been submitted yet + */ + $result = self::$db->fetchColumn( + 'SELECT show_season_id FROM schedule.show_season + WHERE show_season_id NOT IN (SELECT show_season_id FROM schedule.show_season_timeslot) + AND submitted IS NOT NULL + ORDER BY submitted ASC' + ); + + self::$pendingAllocationsResult = []; + foreach ($result as $application) { + self::$pendingAllocationsResult[] = MyRadio_Season::getInstance($application); + } + } + + return self::$pendingAllocationsResult; + } + + /** + * Returns the number of seasons awaiting a timeslot allocation. + * + * @return int the number of pending season allocations + */ + public static function countPendingAllocations() + { + return sizeof(self::pendingAllocationsQuery()); + } + + /** + * Returns all show requests awaiting a timeslot allocation. + * + * @return Array[MyRadio_Season] An array of Seasons of pending allocation + */ + public static function getPendingAllocations() + { + return self::pendingAllocationsQuery(); + } + + /** + * Return the number of show application disputes pending response from Master of Scheduling. + * + * @todo implement this + * + * @return int Zero. + */ + public static function countPendingDisputes() + { + return 0; + } + + + /** + * Returns if we are currently in term time. + * + * @return Boolean + */ + public static function isTerm() + { + return MyRadio_Term::isTerm(); + } + + + /** + * Returns a list of show locations, organised so they can be used as a SELECT MyRadioFormField data source. + */ + public static function getLocations() + { + self::wakeup(); + + return self::$db->fetchAll( + 'SELECT location_id AS value, location_name AS text + FROM schedule.location + ORDER BY location_name ASC' + ); + } + + /** + * Returns a list of potential genres, organised so they can be used as a SELECT MyRadioFormField data source. + */ + public static function getGenres() + { + self::wakeup(); + + return self::$db->fetchAll('SELECT genre_id AS value, name AS text FROM schedule.genre ORDER BY name ASC'); + } + + /** + * Returns a list of potential credit types, organsed so they can be used as a SELECT MyRadioFormField data source. + */ + public static function getCreditTypes() + { + self::wakeup(); + + return self::$db->fetchAll( + 'SELECT credit_type_id AS value, name AS text, is_in_byline + FROM people.credit_type ORDER BY name ASC' + ); } - - return self::$pendingAllocationsResult; - } - - /** - * Returns the number of seasons awaiting a timeslot allocation - * @return int the number of pending season allocations - */ - public static function countPendingAllocations() { - return sizeof(self::pendingAllocationsQuery()); - } - - /** - * Returns all show requests awaiting a timeslot allocation - * @return Array[MyRadio_Season] An array of Seasons of pending allocation - */ - public static function getPendingAllocations() { - return self::pendingAllocationsQuery(); - } - - /** - * Return the number of show application disputes pending response from Master of Scheduling - * @todo implement this - * @return int Zero. - */ - public static function countPendingDisputes() { - return 0; - } - - /** - * Returns a list of terms in the present or future - * @return Array[Array] an array of arrays of terms - */ - public static function getTerms() { - return self::$db->fetch_all('SELECT termid, EXTRACT(EPOCH FROM start) AS start, descr - FROM terms - WHERE finish > now() - ORDER BY start ASC'); - } - - public static function getActiveApplicationTermInfo() { - $termid = self::getActiveApplicationTerm(); - if (empty($termid)) return null; - return array('termid' => $termid, 'descr' => self::getTermDescr($termid)); - } - - public static function getTermDescr($termid) { - $return = self::$db->fetch_one('SELECT descr, start FROM terms WHERE termid=$1', - array($termid)); - return $return['descr'] . date(' Y',strtotime($return['start'])); - } - - public static function getTermStartDate($term_id = null) { - if ($term_id === null) $term_id = self::getActiveApplicationTerm(); - $result = self::$db->fetch_one('SELECT start FROM terms WHERE termid=$1', array($term_id)); + /** - * An extra hour is added here due to some issues with timezones and public.terms - some - * terms are set to start at 11pm Sunday instead of Midnight Monday. It's annoying because then we convert it back. - * If we didn't it's not the end of the world - the usage for this does not include time so just the date *should* - * be sufficient. - * @todo Fix terms database so it isn't silly. + * Returns an Array of Shows matching the given partial title. + * + * @param string $title A partial or total title to search for + * @param int $limit The maximum number of shows to return + * + * @return array 2D with each first dimension an Array as follows:
    + * title: The title of the show
    + * show_id: The unique id of the show */ - return strtotime('Midnight '.date('d-m-Y',strtotime($result['start'])+3600)); - } - - /** - * Returns a list of potential genres, organised so they can be used as a SELECT MyRadioFormField data source - */ - public static function getGenres() { - self::wakeup(); - return self::$db->fetch_all('SELECT genre_id AS value, name AS text FROM schedule.genre ORDER BY name ASC'); - } - - /** - * Returns a list of potential credit types, organsed so they can be used as a SELECT MyRadioFormField data source - */ - public static function getCreditTypes() { - self::wakeup(); - return self::$db->fetch_all('SELECT credit_type_id AS value, name AS text' - . ' FROM people.credit_type ORDER BY name ASC'); - } - - /** - * Returns an Array of Shows matching the given partial title - * @param String $title A partial or total title to search for - * @param int $limit The maximum number of shows to return - * @return Array 2D with each first dimension an Array as follows:
    - * title: The title of the show
    - * show_id: The unique id of the show - */ - public static function findShowByTitle($term, $limit) { - self::initDB(); - return self::$db->fetch_all('SELECT schedule.show.show_id, metadata_value AS title - FROM schedule.show, schedule.show_metadata - WHERE schedule.show.show_id = schedule.show_metadata.show_id - AND metadata_key_id IN (SELECT metadata_key_id FROM metadata.metadata_key WHERE name=\'title\') - AND metadata_value ILIKE \'%\' || $1 || \'%\' LIMIT $2', array($term, $limit)); - } - - /** - * @todo This probably shouldn't implement ServiceAPI - */ - public function getID() { - return 0; - } - - /** - * Returns the Term currently available for Season applications. - * Users can only apply to the current term, or one week before the next one - * starts. - * - * @return int|null Returns the id of the term or null if no active term - * - * @todo Move this into the relevant scheduler class or CoreUtils - */ - public static function getActiveApplicationTerm() { - $return = self::$db->fetch_column('SELECT termid FROM terms - WHERE start <= $1 AND finish >= NOW() LIMIT 1', array(CoreUtils::getTimestamp(strtotime('+28 Days')))); - return $return[0]; - } - - /** - * - * @param int $term_id The term to check for - * @param Array $time: - * day: The day ID (0-6) to check for - * start_time: The start time in seconds since midnight - * duration: The duration in seconds - * - * Return: Array of conflicts with week # as key and show as value - * - * @todo Move this into the relevant scheduler class - */ - public static function getScheduleConflicts($term_id, $time) { - self::initDB(); - $conflicts = array(); - $date = MyRadio_Scheduler::getTermStartDate($term_id); - //Iterate over each week - for ($i = 1; $i <= 10; $i++) { - //Get the start and end times - $start = $date + $time['start_time']; - $end = $date + $time['start_time'] + $time['duration']; - //Query for conflicts - $r = self::getScheduleConflict($start, $end); - - //If there's a conflict, log it - if (!empty($r)) { - $conflicts[$i] = $r['show_season_id']; - } - - //Increment week - $date += 3600 * 24 * 7; + public static function findShowByTitle($term, $limit) + { + self::initDB(); + + return self::$db->fetchAll( + 'SELECT DISTINCT ON (schedule.show.show_id) + schedule.show.show_id, metadata_value AS title + FROM schedule.show, schedule.show_metadata + WHERE schedule.show.show_id = schedule.show_metadata.show_id + AND metadata_key_id IN (SELECT metadata_key_id FROM metadata.metadata_key WHERE name=\'title\') + AND metadata_value ILIKE \'%\' || $1 || \'%\' LIMIT $2', + [$term, $limit] + ); } - return $conflicts; - } - - /** - * Returns a schedule conflict between the given times, if one exists - * @param int $start Start time - * @param int $end End time - * @return Array empty if no conflict, show information otherwise - * - * @todo Move this into the relevant scheduler class - */ - public static function getScheduleConflict($start, $end) { - $start = CoreUtils::getTimestamp($start); - $end = CoreUtils::getTimestamp($end-1); - - return self::$db->fetch_one('SELECT show_season_timeslot_id, - show_season_id, start_time, start_time+duration AS end_time, - \'$1\' AS requested_start, \'$2\' AS requested_end - FROM schedule.show_season_timeslot - WHERE (start_time <= $1 AND start_time + duration > $1) - OR (start_time > $1 AND start_time < $2)', array($start, $end)); - } + /** + * @todo This probably shouldn't implement ServiceAPI + */ + public function getID() + { + return 0; + } + /** + * @param int $term_id The term to check for + * @param array $time: + * day: The day ID (0-6) to check for + * start_time: The start time in seconds since midnight + * duration: The duration in seconds + * + * Return: Array of conflicts with week # as key and show as value + */ + public static function getScheduleConflicts($term_id, $time) + { + self::initDB(); + $conflicts = []; + $term = new MyRadio_Term($term_id); + $start_day = $term->getTermStartDate() + ($time['day'] * 86400); + //Iterate over each week + for ($i = 1; $i <= $term->getTermWeeks(); ++$i) { + $day_start = $start_day + (($i - 1) * 7 * 86400); + + //Get the start time + $gmt_start = $day_start + $time['start_time']; + + $dst_offset = timezone_offset_get(timezone_open(Config::$timezone), date_create('@'.$gmt_start)); + + if ($dst_offset !== false) { + $start = $gmt_start - $dst_offset; + } else { + $start = $gmt_start; + } + + //Query for conflicts + $r = self::getScheduleConflict($start, $start + $time['duration']); + + //If there's a conflict, log it + if (!empty($r)) { + $conflicts[$i] = $r['show_season_id']; + } + } + + return $conflicts; + } + + /** + * Returns a schedule conflict between the given times, if one exists. + * + * @param int $start Start time + * @param int $end End time + * + * @return array empty if no conflict, show information otherwise + */ + public static function getScheduleConflict($start, $end) + { + $start = CoreUtils::getTimestamp($start); + $end = CoreUtils::getTimestamp($end - 1); + + return self::$db->fetchOne( + 'SELECT show_season_timeslot_id, + show_season_id, start_time, start_time+duration AS end_time, + \'$1\' AS requested_start, \'$2\' AS requested_end + FROM schedule.show_season_timeslot + WHERE (start_time <= $1 AND start_time + duration > $1) + OR (start_time > $1 AND start_time < $2)', + [$start, $end] + ); + } + } diff --git a/src/Classes/ServiceAPI/MyRadio_Season.php b/src/Classes/ServiceAPI/MyRadio_Season.php index 67fda8adc..85c624f88 100644 --- a/src/Classes/ServiceAPI/MyRadio_Season.php +++ b/src/Classes/ServiceAPI/MyRadio_Season.php @@ -1,661 +1,1390 @@ - * @package MyRadio_Scheduler + * The Season class is used to create, view and manipulate Seasons within the new MyRadio Scheduler Format. + * * @uses \Database * @uses \MyRadio_Show */ -class MyRadio_Season extends MyRadio_Metadata_Common { - - private $season_id; - private $show_id; - private $term_id; - private $submitted; - private $owner; - private $timeslots; - private $requested_times = []; - private $requested_weeks = []; - private $season_num; - - protected function __construct($season_id) { - $this->season_id = $season_id; - //Init Database - self::initDB(); - - //Get the basic info about the season - $result = self::$db->fetch_one('SELECT show_id, termid, submitted, memberid, - (SELECT array(SELECT metadata_key_id FROM schedule.season_metadata - WHERE show_season_id=$1 AND effective_from <= NOW() AND - (effective_to IS NULL OR effective_to >= NOW()) - ORDER BY effective_from, season_metadata_id)) AS metadata_types, - (SELECT array(SELECT metadata_value FROM schedule.season_metadata - WHERE show_season_id=$1 AND effective_from <= NOW() AND - (effective_to IS NULL OR effective_to >= NOW()) - ORDER BY effective_from, season_metadata_id)) AS metadata, - (SELECT array(SELECT requested_day FROM schedule.show_season_requested_time - WHERE show_season_id=$1 ORDER BY preference ASC)) AS requested_days, - (SELECT array(SELECT start_time FROM schedule.show_season_requested_time WHERE show_season_id=$1 - ORDER BY preference ASC)) AS requested_start_times, - (SELECT array(SELECT duration FROM schedule.show_season_requested_time WHERE show_season_id=$1 - ORDER BY preference ASC)) AS requested_durations, - (SELECT array(SELECT show_season_timeslot_id FROM schedule.show_season_timeslot WHERE show_season_id=$1 - ORDER BY start_time ASC)) AS timeslots, - (SELECT array(SELECT week FROM schedule.show_season_requested_week WHERE show_season_id=$1)) AS requested_weeks, - (SELECT COUNT(*) FROM schedule.show_season - WHERE show_id=(SELECT show_id FROM schedule.show_season WHERE show_season_id=$1) AND show_season_id<=$1 - AND show_season_id IN (SELECT show_season_id FROM schedule.show_season_timeslot)) AS season_num - FROM schedule.show_season WHERE show_season_id=$1', array($season_id)); - if (empty($result)) { - //Invalid Season - throw new MyRadioException('The MyRadio_Season with instance ID #' . $season_id . ' does not exist.'); - } - - //Deal with the easy bits - $this->owner = MyRadio_User::getInstance($result['memberid']); - $this->show_id = (int) $result['show_id']; - $this->submitted = strtotime($result['submitted']); - $this->term_id = (int) $result['termid']; - $this->season_num = (int) $result['season_num']; - - $metadata_types = self::$db->decodeArray($result['metadata_types']); - $metadata = self::$db->decodeArray($result['metadata']); - //Deal with the metadata - for ($i = 0; $i < sizeof($metadata_types); $i++) { - if (self::isMetadataMultiple($metadata_types[$i])) { - $this->metadata[$metadata_types[$i]][] = $metadata[$i]; - } else { - $this->metadata[$metadata_types[$i]] = $metadata[$i]; - } - } - - //Requested Weeks - $requested_weeks = self::$db->decodeArray($result['requested_weeks']); - $this->requested_weeks = array(); - foreach ($requested_weeks as $requested_week) { - $this->requested_weeks[] = intval($requested_week); - } - - //Requested timeslots - $requested_days = self::$db->decodeArray($result['requested_days']); - $requested_start_times = self::$db->decodeArray($result['requested_start_times']); - $requested_durations = self::$db->decodeArray($result['requested_durations']); - - for ($i = 0; $i < sizeof($requested_days); $i++) { - $this->requested_times[] = array( - 'day' => (int) $requested_days[$i], - 'start_time' => (int) $requested_start_times[$i], - 'duration' => self::$db->intervalToTime($requested_durations[$i]) - ); - } - - //And now initiate timeslots - $timeslots = self::$db->decodeArray($result['timeslots']); - $this->timeslots = []; - foreach ($timeslots as $timeslot) { - $this->timeslots[] = MyRadio_Timeslot::getInstance($timeslot); - } - } - - /** - * Creates a new MyRadio Season Application and returns an object representing it - * @param Array $params An array of Seasons properties compatible with the Models/Scheduler/seasonfrm Form, - * with a few additional potential customisation options: - * weeks: An Array of weeks, keyed wk1-10, representing the requested week
    - * times: a 2D Array of:
    - * day: An Array of one or more requested days, 0 being Monday, 6 being Sunday. Corresponds to (s|e)time
    - * stime: An Array of sizeof(day) times, represeting the time of day the show should start
    - * etime: An Array of sizeof(day) times, represeting the time of day the show should end
    - * description: A description of this Season of the Show, in addition to the Show description
    - * tags: A string of 0 or more space-seperated tags this Season relates to, in addition to the Show tags
    - * show_id: The ID of the Show to assign the application to - * termid: The ID of the term being applied for. Defaults to the current Term - * - * weeks, day, stime, etime, show_id are all required fields - * - * As this is the initial creation, all tags are approved by the submitter so the Season has some initial values - * - * @throws MyURYException - */ - public static function apply($params = array()) { - //Validate input - $required = array('show_id', 'weeks', 'times'); - foreach ($required as $field) { - if (!isset($params[$field])) { - throw new MyRadioException('Parameter ' . $field . ' was not provided.', 400); - } +class MyRadio_Season extends MyRadio_Metadata_Common +{ + private $season_id; + private $show_id; + private $term_id; + private $submitted; + private $timeslots; + private $requested_times = []; + private $requested_weeks = []; + private $season_num; + private $subtype_id; + protected $owner; + + protected function __construct($season_id) + { + $this->season_id = (int) $season_id; + //Init Database + self::initDB(); + + //Get the basic info about the season + $result = self::$db->fetchOne( + 'SELECT show_id, termid, submitted, memberid, ( + SELECT array_to_json(array( + SELECT metadata_key_id FROM schedule.season_metadata + WHERE show_season_id=$1 AND effective_from <= NOW() + AND (effective_to IS NULL OR effective_to >= NOW()) + ORDER BY effective_from, season_metadata_id + )) + ) AS metadata_types, ( + SELECT array_to_json(array( + SELECT metadata_value FROM schedule.season_metadata + WHERE show_season_id=$1 AND effective_from <= NOW() + AND (effective_to IS NULL OR effective_to >= NOW()) + ORDER BY effective_from, season_metadata_id + )) + ) AS metadata, ( + SELECT array_to_json(array( + SELECT requested_day FROM schedule.show_season_requested_time + WHERE show_season_id=$1 ORDER BY preference ASC + )) + ) AS requested_days, ( + SELECT array_to_json(array( + SELECT start_time FROM schedule.show_season_requested_time + WHERE show_season_id=$1 + ORDER BY preference ASC + )) + ) AS requested_start_times, ( + SELECT array_to_json(array( + SELECT duration FROM schedule.show_season_requested_time + WHERE show_season_id=$1 + ORDER BY preference ASC + )) + ) AS requested_durations, ( + SELECT array_to_json(array( + SELECT show_season_timeslot_id FROM schedule.show_season_timeslot + WHERE show_season_id=$1 + ORDER BY start_time ASC + )) + ) AS timeslots, ( + SELECT array_to_json(array( + SELECT week FROM schedule.show_season_requested_week WHERE show_season_id=$1 + )) + ) AS requested_weeks, ( + SELECT COUNT(*) FROM schedule.show_season + WHERE show_id=(SELECT show_id FROM schedule.show_season WHERE show_season_id=$1) + AND show_season_id<=$1 + AND show_season_id IN (SELECT show_season_id FROM schedule.show_season_timeslot) + ) AS season_num, ( + SELECT show_subtype_id + FROM schedule.show_season_subtype + WHERE season_id=$1 OR show_id = show_season.show_id + AND effective_from <= NOW() + AND (effective_to IS NULL OR effective_to >= NOW()) + GROUP BY show_season_subtype_id + ORDER BY effective_from, season_id, show_id + LIMIT 1 + ) AS subtype_id + FROM schedule.show_season WHERE show_season_id=$1', + [$season_id] + ); + if (empty($result)) { + //Invalid Season + throw new MyRadioException('The MyRadio_Season with instance ID #'.$season_id.' does not exist.'); + } + + //Deal with the easy bits + $this->owner = MyRadio_User::getInstance($result['memberid']); + $this->show_id = (int) $result['show_id']; + $this->submitted = $result['submitted'] !== null ? strtotime($result['submitted']) : null; + $this->term_id = (int) $result['termid']; + $this->season_num = (int) $result['season_num']; + $this->subtype_id = (int) $result['subtype_id']; + + $metadata_types = json_decode($result['metadata_types']); + $metadata = json_decode($result['metadata']); + //Deal with the metadata + for ($i = 0; $i < sizeof($metadata_types); ++$i) { + if (self::isMetadataMultiple($metadata_types[$i])) { + $this->metadata[$metadata_types[$i]][] = $metadata[$i]; + } else { + $this->metadata[$metadata_types[$i]] = $metadata[$i]; + } + } + + //Requested Weeks + $requested_weeks = json_decode($result['requested_weeks']); + $this->requested_weeks = []; + foreach ($requested_weeks as $requested_week) { + $this->requested_weeks[] = intval($requested_week); + } + + //Requested timeslots + $requested_days = json_decode($result['requested_days']); + $requested_start_times = json_decode($result['requested_start_times']); + $requested_durations = json_decode($result['requested_durations']); + + for ($i = 0; $i < sizeof($requested_days); ++$i) { + $this->requested_times[] = [ + 'day' => (int) $requested_days[$i], + 'start_time' => (int) $requested_start_times[$i], + 'duration' => self::$db->intervalToTime($requested_durations[$i]), + ]; + } + + $this->timeslots = json_decode($result['timeslots']); } /** - * Select an appropriate value for $term_id + * Creates a new MyRadio Season Application and returns an object representing it. + * + * @param array $params An array of Seasons properties compatible with the Models/Scheduler/seasonfrm Form, with a + * few additional potential customisation options: + * weeks: An Array of weeks, keyed wk1-10, representing the requested week
    + * times: a 2D Array of:
    + * day: An Array of one or more requested days, 0 being Monday, 6 being Sunday. Corresponds to + * (s|e)time
    + * stime: An Array of sizeof(day) times, represeting the time of day the show should start
    + * etime: An Array of sizeof(day) times, represeting the time of day the show should end
    + * description: A description of this Season of the Show, in addition to the Show + * description
    + * tags: A string of 0 or more space-seperated tags this Season relates to, in addition to the + * Show tags
    + * show_id: The ID of the Show to assign the application to + * termid: The ID of the term being applied for. Defaults to the current Term + * + * weeks, day, stime, etime, show_id are all required fields + * + * As this is the initial creation, all tags are approved by the submitter + * so the Season has some initial values + * + * @throws MyRadioException */ - $term_id = MyRadio_Scheduler::getActiveApplicationTerm(); - - //Start a transaction - self::$db->query('BEGIN'); - - //Right, let's start by getting a Season ID created for this entry - $season_create_result = self::$db->fetch_column('INSERT INTO schedule.show_season - (show_id, termid, submitted, memberid) - VALUES ($1, $2, $3, $4) RETURNING show_season_id', array($params['show_id'], $term_id, CoreUtils::getTimestamp(), MyRadio_User::getInstance()->getID()), true); - - $season_id = $season_create_result[0]; - - //Now let's allocate store the requested weeks for a term - for ($i = 1; $i <= 10; $i++) { - if ($params['weeks']["wk$i"]) { - self::$db->query('INSERT INTO schedule.show_season_requested_week (show_season_id, week) VALUES ($1, $2)', array($season_id, $i), true); - } - } - - //Now for requested times - for ($i = 0; $i < sizeof($params['times']['day']); $i++) { - //Deal with the possibility of a show from 11pm to midnight etc. - /** - * @todo make this not be completely stupid - */ - if ($params['times']['stime'][$i] < $params['times']['etime'][$i]) { - $interval = CoreUtils::makeInterval($params['times']['stime'][$i], $params['times']['etime'][$i]); - } else { - $interval = CoreUtils::makeInterval($params['times']['stime'][$i], $params['times']['etime'][$i] + 86400); - } - - //Enter the data - self::$db->query('INSERT INTO schedule.show_season_requested_time - (requested_day, start_time, preference, duration, show_season_id) VALUES ($1, $2, $3, $4, $5)', - array($params['times']['day'][$i], $params['times']['stime'][$i], $i, $interval, $season_id)); - } - - //If the description metadata is non-blank, then update that too - if (!empty($params['description'])) { - self::$db->query('INSERT INTO schedule.season_metadata - (metadata_key_id, show_season_id, metadata_value, effective_from, memberid, approvedid) VALUES - ($1, $2, $3, NOW(), $4, $4)', array( - self::getMetadataKey('description'), $season_id, $params['description'], MyRadio_User::getInstance()->getID() - ), true); - } - - //Same with tags - if (!empty($params['tags'])) { - $tags = explode(' ', $params['tags']); - foreach ($tags as $tag) { - if (empty($tag)) { - continue; - } - self::$db->query('INSERT INTO schedule.season_metadata - (metadata_key_id, show_season_id, metadata_value, effective_from, memberid, approvedid) VALUES - ($1, $2, $3, NOW(), $4, $4)', array( - self::getMetadataKey('tag'), $season_id, $tag, MyRadio_User::getInstance()->getID() - ), true); - } - } - - //Actually commit the show to the database! - self::$db->query('COMMIT'); - - MyRadio_Show::getInstance($params['show_id'])->addSeason($season_id); - - return self::getInstance($season_id); - } - - /** - * Get a list of all Seasons that were for the current term, or - * if we are not currently in a Term, the most recenly finished term. - * - * @return MyRadio_Season[] - */ - public static function getAllSeasonsInLatestTerm() { - $result = self::$db->fetch_column('SELECT termid FROM public.terms ' - . 'WHERE start <= NOW() ORDER BY finish DESC LIMIT 1'); - return self::getAllSeasonsInTerm($result[0]); - } - - /** - * Get all the Seasons in the active term. - * - * @param int $term_id - * @return MyRadio_Season[] - */ - public static function getAllSeasonsInTerm($term_id) { - return self::resultSetToObjArray(self::$db->fetch_column( - 'SELECT show_season_id FROM schedule.show_season WHERE termid=$1', [$term_id])); - } - - /** - * Rejects the application for the Season, notifying the creditors if asked. - * - * Will not reject if already rejected.
    - * A Season is "Rejected" by setting the "Submitted" field in schedule.show_season to NULL, - * and adding a "reject-reason" metadata key, with the effective_from set to the time the application - * was rejected.
    - * A Season can be reapplied for by setting the "Submitted" field to the re-submit time. - * It is also best practice to then set the "reject-reason" key to have the same effective_to. - * - * @param String $reason Why the application was rejected - * @param bool $notify_user If true, all creditors will be notified about the rejection. - */ - public function reject($reason, $notify_user = true) { - if ($this->submitted == null) { - return false; - } - self::$db->query('BEGIN'); - self::$db->query('UPDATE schedule.show_season SET submitted=NULL WHERE show_season_id=$1', array($this->getID()), true); - $this->submitted = null; - - $this->setMeta('reject-reason', $reason); - - if ($notify_user) { - MyRadioEmail::sendEmailToUserSet($this->getShow()->getCreditObjects(), $this->getMeta('title') . ' Application Rejected', <<getID(); + $num_weeks = MyRadio_Term::getActiveApplicationTerm()->getTermWeeks(); + + //Start a transaction + self::$db->query('BEGIN'); + + //Right, let's start by getting a Season ID created for this entry + $season_create_result = self::$db->fetchColumn( + 'INSERT INTO schedule.show_season + (show_id, termid, submitted, memberid) + VALUES ($1, $2, $3, $4) RETURNING show_season_id', + [ + $params['show_id'], + $term_id, + CoreUtils::getTimestamp(), + MyRadio_User::getInstance()->getID(), + ], + true + ); + + $season_id = $season_create_result[0]; + + //Now let's allocate store the requested weeks for a term + $any_weeks = false; + for ($i = 1; $i <= $num_weeks; ++$i) { + if ($params['weeks']["wk$i"]) { + self::$db->query( + 'INSERT INTO schedule.show_season_requested_week (show_season_id, week) VALUES ($1, $2)', + [$season_id, $i], + true + ); + $any_weeks = true; + } + } + if (!$any_weeks) { + self::$db->query('ROLLBACK'); + throw new MyRadioException('A Season must at least have one requested week.', 400); + } + + //Now for requested times + for ($i = 0; $i < sizeof($params['times']['day']); ++$i) { + $stime = $params['times']['stime'][$i]; + $etime = $params['times']['etime'][$i]; + if (is_null($params['times']['day'][$i]) || is_null($stime) || is_null($etime)) { + throw new MyRadioException('Each requested time must have a day, start time and end time.', 400); + } + //Deal with the possibility of a show from 11pm to midnight etc. + if ($stime < $etime) { + $interval = CoreUtils::makeInterval($stime, $etime); + } else { + $interval = CoreUtils::makeInterval($stime, $etime + 86400); + } + + //Enter the data + self::$db->query( + 'INSERT INTO schedule.show_season_requested_time + (requested_day, start_time, preference, duration, show_season_id) + VALUES ($1, $2, $3, $4, $5)', + [ + $params['times']['day'][$i], + $stime, + $i, + $interval, + $season_id, + ] + ); + } + + //If the description metadata is non-blank, then update that too + if (!empty($params['description'])) { + self::$db->query( + 'INSERT INTO schedule.season_metadata + (metadata_key_id, show_season_id, metadata_value, effective_from, memberid, approvedid) + VALUES ($1, $2, $3, NOW(), $4, $4)', + [ + self::getMetadataKey('description'), + $season_id, + $params['description'], + MyRadio_User::getInstance()->getID(), + ] + ); + } + + //Same with tags + foreach ($tags as $tag) { + self::$db->query( + 'INSERT INTO schedule.season_metadata + (metadata_key_id, show_season_id, metadata_value, effective_from, memberid, approvedid) + VALUES ($1, $2, $3, NOW(), $4, $4)', + [ + self::getMetadataKey('tag'), + $season_id, + $tag, + MyRadio_User::getInstance()->getID(), + ] + ); + } + + // If the subtype isn't blank, add that in too + if (!empty($params['subtype'])) { + self::$db->query( + 'INSERT INTO schedule.show_season_subtype + (season_id, show_subtype_id, effective_from) + VALUES ($1, ( + SELECT show_subtype_id FROM schedule.show_subtypes WHERE show_subtypes.class = $2 + ), NOW())', + [ + $season_id, + $params['subtype'] + ] + ); + } + + //Actually commit the show to the database! + self::$db->query('COMMIT'); + + MyRadio_Show::getInstance($params['show_id'])->addSeason($season_id); + + $newSeason = self::getInstance($season_id); + + // COVID Studio Usage - Summer 2021 + if (isset($params['studio-request']) && $params['studio-request']) { + MyRadioEmail::sendEmailToList( + MyRadio_List::getByName("programming"), + "WebStudio Usage Request", + "Season " . $season_id . " of " . $newSeason->getMeta("title") + . " has requested WebStudio usage." + ); + } + + return $newSeason; + } + + public static function getForm() + { + $current_term_info = MyRadio_Term::getActiveApplicationTerm(); + $num_weeks = $current_term_info->getTermWeeks(); + $startdate = $current_term_info->getTermStartDate(); + $week_names = $current_term_info->getTermWeekNames(); + + //Set up the weeks checkboxes + $weeks = []; + $date = $startdate; + for ($i = 1; $i <= $num_weeks; ++$i) { + $weeks[] = new MyRadioFormField( + 'wk'.$i, + MyRadioFormField::TYPE_CHECK, + ['label' => $week_names[$i - 1] . ' (w/c ' . date("Y-m-d",$date) .')', 'required' => false] + ); + $date = $date + (86400 * 7); //one week + } + + return ( + new MyRadioForm( + 'sched_season', + 'Scheduler', + 'editSeason', + [ + 'debug' => true, + 'title' => 'Scheduler', + 'subtitle' => 'New Season', + ] + ) + )->addField( + new MyRadioFormField('show_id', MyRadioFormField::TYPE_HIDDEN) + )->addField( + new MyRadioFormField( + 'grp-basics', + MyRadioFormField::TYPE_SECTION, + ['label' => ''] + ) + )->addField( + new MyRadioFormField( + 'weeks', + MyRadioFormField::TYPE_CHECKGRP, + [ + 'options' => $weeks, + 'explanation' => 'Select what weeks this term this show will be on air', + 'label' => 'Schedule for Weeks', + ] + ) + )->addField( + new MyRadioFormField( + 'times', + MyRadioFormField::TYPE_TABULARSET, + [ + 'label' => 'Preferred Times', + 'options' => [ + new MyRadioFormField( + 'day', + MyRadioFormField::TYPE_DAY, + ['label' => 'On'] + ), + new MyRadioFormField( + 'stime', + MyRadioFormField::TYPE_TIME, + ['label' => 'from'] + ), + new MyRadioFormField( + 'etime', + MyRadioFormField::TYPE_TIME, + ['label' => 'until'] + ), + ], + ] + ) + )->addField( + new MyRadioFormField( + 'studio-request', + MyRadioFormField::TYPE_CHECK, + [ + 'explanation' => 'If ticked, you\'ll be requesting to present your show from home. + Please contact the Programme Controller to give more information.', + 'label' => 'Request WebStudio Usage', + 'options' => ['checked' => false], + 'required' => false, + ] + ) + )->addField( + new MyRadioFormField( + 'grp-basics_close', + MyRadioFormField::TYPE_SECTION_CLOSE + ) + )->addField( + new MyRadioFormField( + 'grp-adv', + MyRadioFormField::TYPE_SECTION, + ['label' => 'Advanced Options'] + ) + )->addField( + new MyRadioFormField( + 'description', + MyRadioFormField::TYPE_BLOCKTEXT, + [ + 'explanation' => 'Each season of your show can have its own description. ' + .'If you leave this blank, the main description for your Show will be used.', + 'label' => 'Description', + 'options' => ['minlength' => 140], + 'required' => false, + ] + ) + )->addField( + new MyRadioFormField( + 'tags', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Tags', + 'explanation' => 'A set of keywords to describe this Season, separated by commas. ' + .'These will be added onto the tags you already have set for the Show.', + 'required' => false, + ] + ) + )->addField( + new MyRadioFormField( + 'subtype', + MyRadioFormField::TYPE_SELECT, + [ + 'options' => array_merge( + [ + ['value' => '', 'text' => 'Leave unchanged'] + ], + MyRadio_ShowSubtype::getOptions() + ), + 'label' => 'Subtype', + 'explanation' => 'If necessary, override the subtype for this season. ' + .'If you\'re not sure what you\'re doing, leave it as it is.', + 'required' => false + ] + ) + )->addField( + new MyRadioFormField( + 'grp-adv_close', + MyRadioFormField::TYPE_SECTION_CLOSE + ) + ); + } + + public function getEditForm() + { + $showSubtype = $this->getShow()->getSubtype()->getClass(); + $seasonSubtype = $this->getSubtype()->getClass(); + return self::getForm() + ->setSubTitle('Edit Season') + ->editMode( + $this->getID(), + [ + 'show_id' => $this->show_id, + 'description' => $this->getMeta('description'), + 'tags' => implode(', ', $this->getMeta('tag')), + 'subtype' => $showSubtype === $seasonSubtype ? '' : $seasonSubtype + ] + ); + } + + public function getAllocateForm() + { + $current_term_info = MyRadio_Term::getActiveApplicationTerm(); + $num_weeks = $current_term_info->getTermWeeks(); + $startdate = $current_term_info->getTermStartDate(); + $week_names = $current_term_info->getTermWeekNames(); + $form = ( + new MyRadioForm( + 'sched_allocate', + 'Scheduler', + 'allocate', + [ + 'title' => 'Scheduler', + 'subtitle' => 'Allocate Timeslots to Season', + 'template' => 'Scheduler/allocate.twig', + ] + ) + )->addField( + new MyRadioFormField( + 'season_id', // NOTE: Needed by this name for passing the season ID around for allocation + MyRadioFormField::TYPE_HIDDEN, + ['value' => $this->getID()] + ) + ); + + //Set up the weeks checkboxes + $weeks = []; + $date = $startdate; + for ($i = 1; $i <= $num_weeks; ++$i) { + $weeks[] = new MyRadioFormField( + 'wk'.$i, + MyRadioFormField::TYPE_CHECK, + [ + 'label' => $week_names[$i - 1] . ' (w/c '.date("Y-m-d",$date) . ')', + 'required' => false, + 'options' => ['checked' => in_array($i, $this->getRequestedWeeks())], + ] + ); + $date = $date + (86400 * 7); //one week + } + + //Set up the requested times radios + $times = []; + $i = 0; + foreach ($this->getRequestedTimesAvail() as $time) { + $times[] = [ + 'value' => $i, + 'text' => empty($time['info']) ? $time['time'] : $time['time'].' - '.$time['info'], + 'disabled' => $time['conflict'], + 'class' => $time['conflict'] ? 'alert alert-danger' : '', + ]; + ++$i; + } + + $times[] = ['value' => -1, 'text' => 'Other (Choose below)']; + + $form->addField( + new MyRadioFormField( + 'weeks', + MyRadioFormField::TYPE_CHECKGRP, + [ + 'options' => $weeks, + 'label' => 'Schedule for Weeks', + ] + ) + )->addField( + new MyRadioFormField( + 'time', + MyRadioFormField::TYPE_RADIO, + [ + 'options' => $times, + 'label' => 'Timeslot', + 'required' => false, + ] + ) + )->addField( + new MyRadioFormField( + 'timecustom_day', + MyRadioFormField::TYPE_DAY, + [ + 'label' => 'Other Day: ', + 'required' => false, + ] + ) + )->addField( + new MyRadioFormField( + 'timecustom_stime', + MyRadioFormField::TYPE_TIME, + [ + 'label' => 'from', + 'required' => false, + ] + ) + )->addField( + new MyRadioFormField( + 'timecustom_etime', + MyRadioFormField::TYPE_TIME, + [ + 'label' => 'duration', + 'required' => false, + 'value' => '01:00', + ] + ) + ); + + return $form; + } + + public static function getRejectForm() + { + return ( + new MyRadioForm( + 'sched_reject', + 'Scheduler', + 'reject', + [ + 'debug' => false, + 'title' => 'Scheduler', + 'subtitle' => 'Reject Season Application' + ] + ) + )->addField( + new MyRadioFormField('season_id', MyRadioFormField::TYPE_HIDDEN) + )->addField( + new MyRadioFormField( + 'reason', + MyRadioFormField::TYPE_BLOCKTEXT, + [ + 'label' => 'Reason for Rejection: ', + 'explanation' => 'You can enter a reason here for the application being rejected.' + .' If you then choose to send this response to the applicant, they can then edit their' + .' application and resubmit.', + ] + ) + )->addField( + new MyRadioFormField( + 'notify_user', + MyRadioFormField::TYPE_CHECK, + [ + 'label' => 'Notify the Applicant via Email?', + 'options' => ['checked' => true], + 'required' => false, + ] + ) + ); + } + + public function setCredits($users, $credittypes, $table = null, $pkey = null) + { + // We don't have season credits, just show credits at the appropriate times. + $r = parent::setCredits($users, $credittypes, 'schedule.show_credit', 'show_id'); + $this->updateCacheObject(); + + return $r; + } + + /** + * Get a list of all Seasons that were for the current term, or + * if we are not currently in a Term, the most recenly finished term. + * + * @return MyRadio_Season[] + */ + public static function getAllSeasonsInLatestTerm() + { + $result = self::$db->fetchColumn( + 'SELECT termid FROM public.terms + WHERE start <= NOW() ORDER BY finish DESC LIMIT 1' + ); + + if (empty($result)) { + return []; + } + + return self::getAllSeasonsInTerm($result[0]); + } + + /** + * Get all the Seasons in the active term. + * + * @param int $term_id + * + * @return MyRadio_Season[] + */ + public static function getAllSeasonsInTerm($term_id) + { + return self::resultSetToObjArray( + self::$db->fetchColumn( + 'SELECT show_season_id FROM schedule.show_season WHERE termid=$1', + [$term_id] + ) + ); + } + + /** + * Rejects the application for the Season, notifying the creditors if asked. + * + * Will not reject if already rejected.
    + * A Season is "Rejected" by setting the "Submitted" field in schedule.show_season to NULL, + * and adding a "reject-reason" metadata key, with the effective_from set to the time the application + * was rejected.
    + * A Season can be reapplied for by setting the "Submitted" field to the re-submit time. + * It is also best practice to then set the "reject-reason" key to have the same effective_to. + * + * @param string $reason Why the application was rejected + * @param bool $notify_user If true, all creditors will be notified about the rejection. + */ + public function reject($reason, $notify_user = true) + { + if ($this->submitted == null) { + return false; + } + self::$db->query('BEGIN'); + self::$db->query('UPDATE schedule.show_season SET submitted=NULL WHERE show_season_id=$1', [$this->getID()]); + $this->submitted = null; + + $this->setMeta('reject-reason', $reason); + + if ($notify_user) { + $sname = Config::$short_name; // Heredocs can't do static class variables + $email = Config::$email_domain; + MyRadioEmail::sendEmailToUserSet( + $this->getShow()->getCreditObjects(), + $this->getMeta('title').' Application Rejected', + <<query('COMMIT'); - } - - public function getMeta($meta_string) { - $key = self::getMetadataKey($meta_string); - if (isset($this->meta[$key])) { - return $this->meta[$key]; - } else { - return $this->getShow()->getMeta($meta_string); - } - } - - /** - * Alias for getCredits($this->getShow()), which enables credits to be - * automatically inherited from the show. - * - * @return Array[] - */ - public function getCredits() { - return parent::getCredits($this->getShow()); - } - - /** - * Sets a metadata key to the specified value. - * - * If any value is the same as an existing one, no action will be taken. - * If the given key has is_multiple, then the value will be added as a new, additional key. - * If the key does not have is_multiple, then any existing values will have effective_to - * set to the effective_from of this value, effectively replacing the existing value. - * This will *not* unset is_multiple values that are not in the new set. - * - * @param String $string_key The metadata key - * @param mixed $value The metadata value. If key is_multiple and value is an array, will create instance - * for value in the array. - * @param int $effective_from UTC Time the metavalue is effective from. Default now. - * @param int $effective_to UTC Time the metadata value is effective to. Default NULL (does not expire). - * @param null $table No action. Used for compatibility with parent. - * @param null $pkey No action. Used for compatibility with parent. - */ - public function setMeta($string_key, $value, $effective_from = null, $effective_to = null, $table = null, $pkey = null) { - $r = parent::setMeta($string_key, $value, $effective_from, $effective_to, 'schedule.season_metadata', 'show_season_id'); - $this->updateCacheObject(); - return $r; - } - - public function getID() { - return $this->season_id; - } - - /** - * @return MyRadio_Show - */ - public function getShow() { - return MyRadio_Show::getInstance($this->show_id); - } - - public function getSubmittedTime() { - return CoreUtils::happyTime($this->submitted); - } - - public function getWebpage() { - return 'http://ury.org.uk/show/' . $this->getShow()->getID() . '/' . $this->getID(); - } - - public function getRequestedTimes() { - $return = array(); - foreach ($this->requested_times as $time) { - $return[] = $this->formatTimeHuman($time); - } - return $return; - } - - private function formatTimeHuman($time) { - date_default_timezone_set('UTC'); - $stime = date(' H:i', $time['start_time']); - $etime = date('H:i', $time['start_time'] + $time['duration']); - date_default_timezone_set('Europe/London'); - return self::getDayNameFromID($time['day']) . $stime . ' - ' . $etime; - } - - private function getDayNameFromID($dow) { - $days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; - return $days[$dow]; - } - - /** - * Returns a 2D array: - * time: Value as per getRequestedTimes() - * conflict: True if one or more of requested weeks already have a booking that time - * info: If True above, will have human-readable why-it-is-a-conflict details. It may also contain information about - * "warnings" - conflicts on weeks this show isn't planned to be aired - * @todo The Warnings part above - * @todo Discuss efficiency of this algorithm - */ - public function getRequestedTimesAvail() { - $return = array(); - foreach ($this->requested_times as $time) { - //Check for existence of shows in requested times - $conflicts = MyRadio_Scheduler::getScheduleConflicts($this->term_id, $time); - $warnings = array(); - foreach ($conflicts as $wk => $sid) { - if (!in_array($wk, $conflicts)) { - //This isn't actually a conflict because the week isn't requested by the user - $warnings[$wk] = $sid; - unset($conflicts[$wk]); - } - } - //If there's a any conflicts, let's make the nice explanation - if (!empty($conflicts)) { - $names = ''; - $weeks = ' on weeks '; - $week_num = -10; - //Count the number of conflicts with season ids - $sids = array(); - foreach ($conflicts as $k => $v) { - /** - * @todo - figure out why this loop includes 0 and 11 sometimes - * @todo Work on weeks text output - duplicates/overlaps - */ - if ($k > 10 || $k < 1) - continue; - $sids[$v]++; - //Work out blocked weeks - if ($k == ++$week_num) { - //Continuation of previous sequence - $week_num = $k; - if ($k == 10) - $weeks .= '-10'; - } else { - //New sequence - if ($week_num > 0) - $weeks .= '-' . $week_num . ', '; - $weeks .= $k; - $week_num = $k; - } - } - //Iterate over every conflicted show and log - foreach ($sids as $k => $v) { - //Get the show name and store it - if ($names !== '') - $names .= ', '; - $names .= self::getInstance($k)->getMeta('title'); - } - $return[] = array('time' => self::formatTimeHuman($time), 'conflict' => true, 'info' => 'Conflicts with ' . $names . $weeks); - } else { - //No conflicts - $return[] = array('time' => self::formatTimeHuman($time), 'conflict' => false, 'info' => ''); - } - } - return $return; - } - - public function getRequestedWeeks() { - return $this->requested_weeks; - } - - /** - * Get the Season number - for the first season of a show, this is 1, for the second it's 2 etc. - * Seasons that don't have any timeslots scheduled do not count toward this value. - * @return int - */ - public function getSeasonNumber() { - return $this->season_num; - } - - public function toDataSource($full = true) { - return array_merge($this->getShow()->toDataSource(false), array( - 'id' => $this->getID(), - 'season_num' => $this->getSeasonNumber(), - 'title' => $this->getMeta('title'), - 'description' => $this->getMeta('description'), - 'submitted' => $this->getSubmittedTime(), - 'requested_time' => sizeof($this->getRequestedTimes()) === 0 ? null : $this->getRequestedTimes()[0], - 'first_time' => (isset($this->timeslots[0]) && is_object($this->timeslots[0]) ? CoreUtils::happyTime($this->timeslots[0]->getStartTime()) : 'Not Scheduled'), - 'num_episodes' => array( - 'display' => 'text', - 'value' => sizeof($this->timeslots), - 'url' => CoreUtils::makeURL('Scheduler', 'listTimeslots', array('show_season_id' => $this->getID()))), - 'allocatelink' => array( - 'display' => 'icon', - 'value' => 'script', - 'title' => 'Edit Application or Allocate Season', - 'url' => CoreUtils::makeURL('Scheduler', 'allocate', array('show_season_id' => $this->getID()))), - 'rejectlink' => array( - 'display' => 'icon', - 'value' => 'trash', - 'title' => 'Reject Application', - 'url' => CoreUtils::makeURL('Scheduler', 'reject', array('show_season_id' => $this->getID()))) - )); - } - - /** - * This is where some of the most important MyRadio stuff happens. - * This is where an application for a presenter's dreams become reality... - * or get crushed to pieces. - * @param Array $params key=>value of the following parameters: - * weeks: A key=>value away of weeks and whether to schedule (wk1 => 0, wk1=>1...) - * time: The preference number of the show_season_requested_time that was selected, or -1 - * timecustom_day: Ignored if time is > -1 - * If time = -1, this is the day # to schedule for - * timecustom_stime: Ignored if time is > -1 - * If time = -1, this is the start time to schedule for - * timecustom_etime: Ignored if time is >-1 - * If time = -1, this is the *duration* to schedule for (not end time) - * - * @todo Validate timeslots are available before scheduling - * @todo Email the user notifying them of scheduling - * @todo Verify the timeslot is free before scheduling - */ - public function schedule($params) { - date_default_timezone_set('UTC'); - //Verify that the input time is valid - if (!isset($params['time']) or !is_numeric($params['time'])) { - throw new MyRadioException('No valid Time was sent to the Scheduling Mapper.', MyRadioException::FATAL); - } - if ($params['time'] != -1 && !isset($this->requested_times[$params['time']])) { - throw new MyRadioException('The Time value sent is not a valid Requested Time Reference.', MyRadioException::FATAL); - } - //Verify the custom times are valid - if ($params['time'] == -1 && ( - !isset($params['timecustom_day']) or //0 (monday) would fail an empty() test - !isset($params['timecustom_stime']) or //Same again with midnight (00:00) - empty($params['timecustom_etime']))) { - throw new MyRadioException('The Custom Time value sent is invalid.', MyRadioException::FATAL); - } - //Okay, let's get to business - //First, figure out what time things are happening - if ($params['time'] != -1) { - //Use the requested times value - $req_time = $this->requested_times[$params['time']]; - } else { - $req_time = array( - 'day' => $params['timecustom_day'], - 'start_time' => $params['timecustom_stime'], - 'duration' => $params['timecustom_etime'] - ); + ); + } + + self::$db->query('COMMIT'); } + + public function getMeta($meta_string) + { + $key = self::getMetadataKey($meta_string); + if (isset($this->metadata[$key])) { + return $this->metadata[$key]; + } else { + return $this->getShow()->getMeta($meta_string); + } + } + /** - * Since terms start on the Monday, we just +1 day to it + * Alias for getCredits($this->getShow()), which enables credits to be + * automatically inherited from the show. + * @param parent Unused for type compatibility with parent + * @return array[] */ - $start_day = MyRadio_Scheduler::getTermStartDate( - MyRadio_Scheduler::getActiveApplicationTerm()) + ($req_time['day'] * 86400); + public function getCredits(\MyRadio\ServiceAPI\MyRadio_Metadata_Common $parent = null) + { + return parent::getCredits($this->getShow()); + } - $start_time = date('H:i:s', $req_time['start_time']); + /** + * Sets a metadata key to the specified value. + * + * If any value is the same as an existing one, no action will be taken. + * If the given key has is_multiple, then the value will be added as a new, additional key. + * If the key does not have is_multiple, then any existing values will have effective_to + * set to the effective_from of this value, effectively replacing the existing value. + * This will *not* unset is_multiple values that are not in the new set. + * + * @param string $string_key The metadata key + * @param mixed $value The metadata value. If key is_multiple and value is an array, will create instance + * for value in the array. + * @param int $effective_from UTC Time the metavalue is effective from. Default now. + * @param int $effective_to UTC Time the metadata value is effective to. Default NULL (does not expire). + */ + public function setMeta($string_key, $value, $effective_from = null, $effective_to = null) + { + $r = parent::setMetaBase( + $string_key, + $value, + $effective_from, + $effective_to, + 'schedule.season_metadata', + 'show_season_id' + ); + $this->metadata[$string_key] = $value; + $this->updateCacheObject(); + + return $r; + } + + public function getID() + { + return $this->season_id; + } - //Now it's time to BEGIN to COMMIT! - self::$db->query('BEGIN'); /** - * This will iterate over each week, decide if it should be scheduled, - * then schedule it if it should. Simples. + * @return MyRadio_Show */ - $times = ''; - for ($i = 1; $i <= 10; $i++) { - if (isset($params['weeks']['wk' . $i]) && $params['weeks']['wk' . $i] == 1) { - $day_start = $start_day + (($i - 1) * 7 * 86400); - $show_time = date('d-m-Y ', $day_start) . $start_time; + public function getShow() + { + return MyRadio_Show::getInstance($this->show_id); + } + + public function getSubmittedTime() + { + return CoreUtils::happyTime($this->submitted); + } + + /** + * Get the microsite URI. + * + * @return string + */ + public function getWebpage() + { + return '/schedule/shows/seasons/'.$this->getID(); + } - /** - * @todo 1 is subtracted from the duration in the conflict checker here, - * as shows last precisely an hour, not 59m59s. Should we do something - * nicer here? - */ - $conflict = MyRadio_Scheduler::getScheduleConflict($day_start + $req_time['start_time'], $day_start + $start_time + $req_time['duration'] - 1); - //print_r($conflict); - //Disable because it doesn't fucking work. - /* * if (!empty($conflict)) { - self::$db->query('ROLLBACK'); - throw new MyRadioException('A show is already scheduled for this time: '.print_r($conflict, true)); - exit; - } */ - - //This week is due to be scheduled! QUERY! QUERY! - $r = self::$db->fetch_all('INSERT INTO schedule.show_season_timeslot - (show_season_id, start_time, duration, memberid, approvedid) - VALUES ($1, $2, $3, $4, $5) RETURNING show_season_timeslot_id', array( - $this->season_id, - $show_time, - $req_time['duration'], - $this->owner->getID(), - $_SESSION['memberid'] - )); - $this->timeslots[] = MyRadio_Timeslot::getInstance($r[0]['show_season_timeslot_id']); - $times .= CoreUtils::happyTime($show_time)."\n"; //Times for the email - } - } - //COMMIT - self::$db->query('COMMIT'); - $this->updateCacheObject(); - //Email the user /** - * @todo Make this nicer and configurable and stuff + * Gets the subtype for this season. + * @return MyRadio_ShowSubtype */ - $message = " + public function getSubtype() + { + if ($this->subtype_id === null) { + return $this->getShow()->getSubtype(); + } + return MyRadio_ShowSubtype::getInstance($this->subtype_id); + } + + /** + * Sets this season's subtype. + * + * @todo support effectiveFrom and effectiveTo + * @param $subtypeId + */ + public function setSubtype($subtypeId) + { + self::$db->query('UPDATE schedule.show_season_subtype SET show_subtype_id = $1 WHERE season_id = $1', [ + $subtypeId, $this->season_id + ]); + } + + /** + * Sets this season's subtype by the subtype name. + * @param $subtypeName + */ + public function setSubtypeByName($subtypeName) + { + self::$db->query( + 'UPDATE schedule.show_season_subtype + SET show_subtype_id = subtype.show_subtype_id + FROM (SELECT show_subtype_id FROM schedule.show_subtypes WHERE show_subtypes.class = $2) AS subtype + WHERE season_id = $1', + [$this->season_id, $subtypeName] + ); + } + + /** + * Clears the subtype for this season, resetting it to the show's "main" subtype. + */ + public function clearSubtype() + { + self::$db->query('DELETE FROM schedule.show_season_subtype WHERE season_id = $1', [$this->season_id]); + } + + public function getRequestedTimes() + { + $return = []; + foreach ($this->requested_times as $time) { + $return[] = $this->formatTimeHuman($time); + } + + return $return; + } + + private function formatTimeHuman($time) + { + $stime = gmdate(' H:i', $time['start_time']); + $etime = gmdate('H:i', $time['start_time'] + $time['duration']); + + return self::getDayNameFromID($time['day']).$stime.' - '.$etime; + } + + private function getDayNameFromID($dow) + { + $days = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']; + + return $days[$dow]; + } + + /** + * Fetches requested times for the season and checks for conflicts. + * + * Returns a 2D array: + * time: Value as per getRequestedTimes() + * conflict: True if one or more of requested weeks already have a booking that time + * info: If True above, will have human-readable why-it-is-a-conflict details. It may also contain information about + * "warnings" - conflicts on weeks this show isn't planned to be aired + * + * @return array time, conflict, info + */ + public function getRequestedTimesAvail() + { + $return = []; + foreach ($this->requested_times as $time) { + //Check for existence of shows in requested times + $conflicts = MyRadio_Scheduler::getScheduleConflicts($this->term_id, $time); + + if (!empty($conflicts)) { + $conflict = ''; + $warning = ''; + + foreach ($conflicts as $wk => $season_id) { + // Check if week is requested + if (in_array($wk, $this->requested_weeks)) { + $conflict .= self::getInstance($season_id)->getMeta('title').' (week '.$wk.'). '; + } else { + $warning .= self::getInstance($season_id)->getMeta('title').' (week '.$wk.'). '; + } + } + + if (!empty($conflict)) { + // return conflicts + $return[] = [ + 'time' => self::formatTimeHuman($time), + 'conflict' => true, + 'info' => 'Conflicts with: ' . $conflict + ]; + } else { + // return warning + $return[] = [ + 'time' => self::formatTimeHuman($time), + 'conflict' => false, + 'info' => 'Warnings with: ' . $warning + ]; + } + } else { + // no conflicts or warnings + $return[] = ['time' => self::formatTimeHuman($time), 'conflict' => false, 'info' => '']; + } + } + + return $return; + } + + public function getRequestedWeeks() + { + return $this->requested_weeks; + } + + /** + * Get the Season number - for the first season of a show, this is 1, for the second it's 2 etc. + * Seasons that don't have any timeslots scheduled do not count toward this value. + * + * @return int + */ + public function getSeasonNumber() + { + return $this->season_num; + } + + /** + * Serialises the season. Merges with the parent show object as well. + */ + public function toDataSource($mixins = []) + { + $first_time = $this->getFirstTime(); + $requested_times = $this->getRequestedTimes(); + + return array_merge( + $this->getShow()->toDataSource($mixins), + [ + 'season_id' => $this->getID(), + 'season_num' => $this->getSeasonNumber(), + 'title' => $this->getMeta('title'), + 'description' => $this->getMeta('description'), + 'subtype' => array_merge($this->getSubtype()->toDataSource($mixins), [ + // I don't like using html here, but if I use text it adds an unnecessary and ugly tag + 'display' => 'html', + 'html' => $this->getSubtype()->getName() + ]), + 'submitted' => $this->getSubmittedTime(), + 'requested_time' => count($requested_times) > 0 ? $requested_times[0] : null, + 'first_time' => CoreUtils::happyTime(($first_time ? $first_time : 0)), + 'num_episodes' => [ + 'display' => 'text', + 'value' => count($this->timeslots), + 'url' => URLUtils::makeURL( + 'Scheduler', + 'listTimeslots', + ['show_season_id' => $this->getID()] + ), + ], + 'addEpisodesLink' => [ + 'display' => 'icon', + 'value' => 'plus', + 'title' => 'Add Episodes', + 'url' => URLUtils::makeURL( + 'Scheduler', + 'addEpisode', + ['show_season_id' => $this->getID()] + ), + ], + 'editlink' => [ + 'display' => 'icon', + 'value' => 'pencil', + 'title' => 'Edit Season', + 'url' => URLUtils::makeURL( + 'Scheduler', + 'editSeason', + ['seasonid' => $this->getID()] + ), + ], + 'allocatelink' => [ + 'display' => 'icon', + 'value' => 'pencil', + 'title' => 'Edit Application or Allocate Season', + 'url' => URLUtils::makeURL( + 'Scheduler', + 'allocate', + ['show_season_id' => $this->getID()] + ), + ], + 'rejectlink' => [ + 'display' => 'icon', + 'value' => 'trash', + 'title' => 'Reject Application', + 'url' => URLUtils::makeURL( + 'Scheduler', + 'reject', + ['show_season_id' => $this->getID()] + ), + ], + ] + ); + } + + /** + * This is where some of the most important MyRadio stuff happens. + * This is where an application for a presenter's dreams become reality... + * or get crushed to pieces. + * + * @param array $params key=>value of the following parameters: + * weeks: A key=>value away of weeks and whether to schedule (wk1 => 0, wk1=>1...) + * time: The preference number of the show_season_requested_time that was selected, or -1 + * timecustom_day: Ignored if time is > -1 + * If time = -1, this is the day # to schedule for + * timecustom_stime: Ignored if time is > -1 + * If time = -1, this is the start time to schedule for + * timecustom_etime: Ignored if time is >-1 + * If time = -1, this is the *duration* to schedule for (not end time) + * + * @todo Validate timeslots are available before scheduling + * @todo Email the user notifying them of scheduling + * @todo Verify the timeslot is free before scheduling + */ + public function schedule($params, $num_weeks) + { + //Verify that the input time is valid + if (!isset($params['time']) or !is_numeric($params['time'])) { + throw new MyRadioException( + 'No valid Time was sent to the Scheduling Mapper.', + 400 + ); + } + if ($params['time'] != -1 && !isset($this->requested_times[$params['time']])) { + throw new MyRadioException( + 'The Time value sent is not a valid Requested Time Reference.', + 400 + ); + } + //Verify the custom times are valid + if ($params['time'] == -1 && (!isset($params['timecustom_day']) //0 (monday) would fail an empty() test + or !isset($params['timecustom_stime']) //Same again with midnight (00:00) + or empty($params['timecustom_etime'])) + ) { + throw new MyRadioException('The Custom Time value sent is invalid.', 400); + } + //Okay, let's get to business + //First, figure out what time things are happening + if ($params['time'] != -1) { + //Use the requested times value + $req_time = $this->requested_times[$params['time']]; + } else { + $req_time = [ + 'day' => $params['timecustom_day'], + 'start_time' => $params['timecustom_stime'], + 'duration' => $params['timecustom_etime'], + ]; + } + /* + * Since terms start on the Monday, we just +1 day to it + */ + $start_day = (MyRadio_Term::getActiveApplicationTerm())->getTermStartDate() + ($req_time['day'] * 86400); + + //Now it's time to BEGIN to COMMIT! + self::$db->query('BEGIN'); + /* + * This will iterate over each week, decide if it should be scheduled, + * then schedule it if it should. Simples. + */ + $times = ''; + for ($i = 1; $i <= $num_weeks; ++$i) { + if (isset($params['weeks']['wk'.$i]) && $params['weeks']['wk'.$i] == 1) { + $day_start = $start_day + (($i - 1) * 7 * 86400); + $gmt_show_time = $day_start + $req_time['start_time']; + + $dst_offset = timezone_offset_get(timezone_open(Config::$timezone), date_create('@'.$gmt_show_time)); + + if ($dst_offset !== false) { + $show_time = $gmt_show_time - $dst_offset; + } else { + $show_time = $gmt_show_time; + } + + $conflict = MyRadio_Scheduler::getScheduleConflict($show_time, $show_time + $req_time['duration']); + if (!empty($conflict)) { + self::$db->query('ROLLBACK'); + throw new MyRadioException( + 'A show is already scheduled for this time: '.print_r($conflict, true), + 400 + ); + } + + // If gone through API use placeholder approved id - could be improved by needing a member ID as part of API call. + $approvedid = 1; + if (MyRadio_User::getCurrentUser() !== null) { + $approvedid = $_SESSION['memberid']; + } + + //This week is due to be scheduled! QUERY! QUERY! + $r = self::$db->fetchAll( + 'INSERT INTO schedule.show_season_timeslot + (show_season_id, start_time, duration, memberid, approvedid) + VALUES ($1, $2, $3, $4, $5) RETURNING show_season_timeslot_id', + [ + $this->season_id, + CoreUtils::getTimestamp($show_time), + $req_time['duration'], + $this->owner->getID(), + $approvedid, + ] + ); + if (empty($r)) { + throw new MyRadioException('Failed to schedule timeslot.', 500); + } + $this->timeslots[] = $r[0]['show_season_timeslot_id']; + $times .= CoreUtils::happyTime($show_time)."\n"; //Times for the email + + // Clear the Schedule cache for this week + $weekAndYear = CoreUtils::getYearAndWeekNo($show_time); + self::$cache->delete('MyRadioScheduleFor'.$weekAndYear[0].'W'.$weekAndYear[1]); + } + } + //COMMIT + self::$db->query('COMMIT'); + $this->updateCacheObject(); + //Email the user + /* + * @todo Make this nicer and configurable and stuff + */ + $message = ' Hello, - - Please note that one of your shows has been allocated the following timeslots on the ".Config::$short_name." Schedule: - + + Please note that one of your shows has been allocated the following timeslots + on the '.Config::$short_name." Schedule: + $times - Remember that except in exceptional circumstances, you must give at least 48 hours notice for cancelling your show as part of your presenter contract. If you do not do this for two shows in one season, all other shows are forfeit and may be cancelled. + Remember that except in exceptional circumstances, you must give at least + 48 hours notice for cancelling your show as part of your presenter contract. + If you do not do this for two shows in one season, all other shows are forfeit + and may be cancelled. + + You can cancel a timeslot by going to: + My Shows -> Seasons for Show -> Timeslots for Season + and then selecting cancel for the particular time. + ".URLUtils::makeURL('Scheduler', 'myShows').' - If you have any questions about your application, direct them to pc@ury.org.uk + If you have any questions about your application, direct them to pc@'.Config::$email_domain.' - ~ ".Config::$short_name." Scheduling Legume"; + ~ '.Config::$short_name.' Scheduling Legume'; - if (!empty($times)) { - MyRadioEmail::sendEmailToUser($this->owner, $this->getMeta('title') . ' Scheduled', $message); + if (!empty($times)) { + MyRadioEmail::sendEmailToUser($this->owner, $this->getMeta('title').' Scheduled', $message); + } } - date_default_timezone_set(Config::$timezone); - } + /** + * Deletes all future occurances of a Timeslot for this Season. + */ + public function cancelRestOfSeason() + { + //Get a list of timeslots that will be cancelled and email the creditors + $timeslots = $this->getFutureTimeslots(); + if (empty($timeslots)) { + return; + } - /** - * Deletes all future occurances of a Timeslot for this Season - */ - public function cancelRestOfSeason() { - //Get a list of timeslots that will be cancelled and email the creditors - $timeslots = $this->getFutureTimeslots(); - if (empty($timeslots)) - return; + $timeslot_str = "\r\n"; + foreach ($timeslots as $timeslot) { + $timeslot_str .= CoreUtils::happyTime("{$timeslot['start_time']}\r\n"); + } - $timeslot_str = "\r\n"; - foreach ($timeslots as $timeslot) { - $timeslot_str .= CoreUtils::happyTime("{$timeslot['start_time']}\r\n"); - } + $email = 'Please note that your show, ' + . $this->getMeta('title') + . ' has been cancelled for the rest of the current Season. This is the following timeslots: ' + . $timeslot_str + . "\r\n\r\n"; + $email .= "Regards\r\n" . Config::$long_name . ' Programming Team'; - $email = 'Please note that your show, ' . $this->getMeta('title') . ' has been cancelled for the rest of the current Season. This is the following timeslots: ' . $timeslot_str; - $email .= "\r\n\r\nRegards\r\n" . Config::$long_name . " Programming Team"; + foreach ($this->getShow()->getCredits() as $credit) { + $u = MyRadio_User::getInstance($credit); + MyRadioEmail::sendEmailToUser($u, 'Show Cancelled', $email); + } - foreach ($this->getShow()->getCredits() as $credit) { - $u = MyRadio_User::getInstance($credit); - MyRadioEmail::sendEmailToUser($u, 'Show Cancelled', $email); + $r = (bool) self::$db->query( + 'DELETE FROM schedule.show_season_timeslot WHERE show_season_id=$1 AND start_time >= NOW()', + [$this->getID()] + ); + $this->updateCacheObject(); + return $r; } - $r = (bool) self::$db->query('DELETE FROM schedule.show_season_timeslot WHERE show_season_id=$1 AND start_time >= NOW()', array($this->getID())); + /** + * Returns an array of Timeslots in the future for this Season as follows: + * show_season_timeslot_id + * start_time + * duration. + * + * @todo Refactor to return MyRadio_Timeslot objects + */ + public function getFutureTimeslots() + { + return self::$db->fetchAll( + 'SELECT show_season_timeslot_id, start_time, duration FROM schedule.show_season_timeslot + WHERE show_season_id=$1 AND start_time >= NOW()', + [$this->getID()] + ); + } - $m = new Memcached(); - $m->addServer(Config::$django_cache_server, 11211); - $m->flush(); + /** + * Returns the start time of the first Timeslot in this season. + * + * @return int + */ + public function getFirstTime() + { + if (sizeof($this->timeslots) > 0) { + return MyRadio_Timeslot::getInstance($this->timeslots[0])->getStartTime(); + } else { + return false; + } + } - return $r; - } + /** + * Returns all Timeslots for this Season. + * + * @return MyRadio_Timeslot[] + */ + public function getAllTimeslots() + { + return MyRadio_Timeslot::resultSetToObjArray($this->timeslots); + } - /** - * Returns an array of Timeslots in the future for this Season as follows: - * show_season_timeslot_id - * start_time - * duration - * @todo Refactor to return MyRadio_Timeslot objects - */ - public function getFutureTimeslots() { - return self::$db->fetch_all('SELECT show_season_timeslot_id, start_time, duration FROM schedule.show_season_timeslot - WHERE show_season_id=$1 AND start_time >= NOW()', array($this->getID())); - } + /** + * Returns the percentage of Timeslots in this Season that at least one User + * has signed into. + * + * @return [float, int] + */ + public function getAttendanceInfo() + { + $signed_in = 0; + $total = 0; + foreach ($this->getAllTimeslots() as $ts) { + if ($ts->getStartTime() > time()) { + continue; + } + ++$total; + foreach ($ts->getSigninInfo() as $info) { + if (!empty($info['signedby'])) { + ++$signed_in; + break; + } + } + } - /** - * Returns all Timeslots for this Season - * @return MyRadio_Timeslot[] - */ - public function getAllTimeslots() { - return $this->timeslots; - } + if ($total === 0) { + return [100, 0]; + } - /** - * Returns the percentage of Timeslots in this Season that at least one User - * has signed into. - * - * @return [float, int] - */ - public function getAttendanceInfo() { - $signed_in = 0; - $total = 0; - foreach ($this->getAllTimeslots() as $ts) { - if ($ts->getStartTime() > time()) { - continue; - } - $total++; - foreach ($ts->getSigninInfo() as $info) { - if (!empty($info['signedby'])) { - $signed_in++; - break; + return [($signed_in / $total) * 100, $total - $signed_in]; + } + + /** + * Searches searchable *text* metadata for the specified value. Does not work for image metadata. + * + * @todo effective_from/to not yet implemented + * + * @param string $query The query value. + * @param array $string_keys The metadata keys to search + * @param int $effective_from UTC Time to search from. + * @param int $effective_to UTC Time to search to. + * + * @return array The shows that match the search terms + */ + public static function searchMeta($query, $string_keys = null, $effective_from = null, $effective_to = null) + { + if (is_null($string_keys)) { + $string_keys = ['title', 'description', 'tag']; } - } + + $r = parent::searchMetaBase( + $query, + $string_keys, + $effective_from, + $effective_to, + 'schedule.season_metadata', + 'show_season_id' + ); + return self::resultSetToObjArray($r); } - if ($total === 0) { - return [100, 0]; + public function getAddEpisodeForm() + { + $title = $this->getMeta('title'); + return (new MyRadioForm( + 'sched_add_episode', + 'Scheduler', + 'addEpisode', + [ + 'debug' => false, + 'title' => 'Add Episode', + 'subtitle' => "New Episode - $title" + ] + ))->addField(new MyRadioFormField( + 'grp_info', + MyRadioFormField::TYPE_SECTION, + [ + 'label' => 'Create new episode', + 'explanation' => 'Enter the time for the new episode in this season. Take care with the end time.' + ] + ))->addField(new MyRadioFormField( + 'new_start_time', + MyRadioFormField::TYPE_DATETIME, + [ + 'label' => 'Episde Start Time', + 'value' => date('d/m/Y H:i') + ] + ))->addField(new MyRadioFormField( + 'new_end_time', + MyRadioFormField::TYPE_DATETIME, + [ + 'label' => 'Episode End Time', + 'value' => date('d/m/Y H:i') + ] + ))->addField(new MyRadioFormField( + 'grp_info_close', + MyRadioFormField::TYPE_SECTION_CLOSE, + [] + ))->addField(new MyRadioFormField( + 'show_season_id', + MyRadioFormField::TYPE_HIDDEN, + ['value' => $this->getID()] + )); } - return [($signed_in / $total) * 100, $total - $signed_in]; - } + public function addEpisode($start_time, $end_time, $memberid = 1) + { + // If no active session we must have come through API so use placeholder user id + if (MyRadio_User::getCurrentUser() !== null) { + $memberid = MyRadio_User::getCurrentUser()->getID(); + } + + if(is_null($start_time) || is_null($end_time)) { + throw new MyRadioException('Start and end time must be set.', 400); + } + + //Deal with the possibility of a show from 11pm to midnight etc. (taken from above) + if ($start_time < $end_time) { + $interval = CoreUtils::makeInterval($start_time, $end_time); + } else { + $interval = CoreUtils::makeInterval($start_time, $end_time + 86400); + } + $r = self::$db->query( + 'INSERT INTO schedule.show_season_timeslot + (show_season_id, start_time, duration, memberid, approvedid) + VALUES ($1, $2, $3, $4, $4) RETURNING show_season_timeslot_id', + [ + $this->getID(), + CoreUtils::getTimestamp($start_time), + $interval, + $memberid + ] + ); + if ($r) { + $this->updateCacheObject(); + } + return $r; + } } diff --git a/src/Classes/ServiceAPI/MyRadio_Selector.php b/src/Classes/ServiceAPI/MyRadio_Selector.php index ed032286d..635ccb6dd 100644 --- a/src/Classes/ServiceAPI/MyRadio_Selector.php +++ b/src/Classes/ServiceAPI/MyRadio_Selector.php @@ -1,66 +1,78 @@ - * @package MyRadio_Core - * @uses \Database + * + * @uses \Database */ -class MyRadio_Selector { - +class MyRadio_Selector +{ /** - * The current studio is Studio 1 + * The current studio is Studio 1. */ const SEL_STUDIO1 = 1; /** - * The current studio is Studio 2 + * The current studio is Studio 2. */ const SEL_STUDIO2 = 2; /** - * The current studio is Jukebox + * The current studio is Jukebox. */ const SEL_JUKEBOX = 3; /** - * The current studio is Outside Broadcast + * The current studio is Outside Broadcast. */ const SEL_OB = 4; + /** The current "studio" is WebStudio */ + const SEL_WS = 5; + + /** + * The current "studio" is the off-air loop. + */ + const SEL_OFFAIR = 8; + /** - * The studio selection was made by the Selector Telnet interface + * The studio selection was made by the Selector Telnet interface. */ const FROM_AUX = 0; /** - * The studio selection was made by Studio 1 + * The studio selection was made by Studio 1. */ const FROM_S1 = 1; /** - * The studio selection was made by Studio 2 + * The studio selection was made by Studio 2. */ const FROM_S2 = 2; /** - * The studio selection was made on the main selector panel in the hub + * The studio selection was made on the main selector panel in the hub. */ const FROM_HUB = 3; /** - * The selector is unlocked + * The selector is unlocked. */ const LOCK_NONE = 0; @@ -97,100 +109,65 @@ class MyRadio_Selector { const ON_BOTH = 3; /** - * Caches the status of the selector (the query command) - * @var Array + * Construct the Selector Object. */ - private $sel_status; - - /** - * Construct the Selector Object - */ - public function __construct() { - - } - - /** - * Returns the state of the remote OB feeds in an associative array. - * @return Array - */ - public static function remoteStreams() { - $data = file(Config::$ob_remote_status_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); - - $response = []; - foreach ($data as $feed) { - $state = explode('=', $feed); - $response[trim($state[0])] = (bool) trim($state[1]); - } - - return $response; - } - - /** - * Returns the length of the current silence, if any. - * @return int - */ - public function isSilence() { - $result = Database::getInstance()->fetch_one('SELECT starttime, stoptime - FROM jukebox.silence_log - ORDER BY silenceid DESC LIMIT 1'); - - if (empty($result['stoptime'])) { - return time() - strtotime($result['starttime']); - } else { - return 0; - } + public function __construct() + { } /** - * Returns the current selector status - * + * Returns the current selector status. + * * The command 'Q' returns a 4-digit number. The first digit is the currently * selected studio. The second is where it was selected from, the third * provides information about whether the selector is locked, and the fourth * about which studios are switched on. - * - * @return Array {'studio' => [1-8], 'selectedfrom' => [0-3], 'lock' => [0-2], - * 'power' => [0-3]} - */ - public function query() { - if (empty($this->sel_status)) { - $data = $this->cmd('Q'); - - $state = str_split($data); - - $this->sel_status = [ - 'studio' => (int) $state[0], - 'lock' => (int) $state[1], - 'selectedfrom' => (int) $state[2], - 'power' => (int) $state[3] - ]; - } + * + * @return array {'studio' => [1-8], 'selectedfrom' => [0-3], 'lock' => [0-2], + * 'power' => [0-3]} + */ + public static function setQuery() + { + $data = self::cmd('Q'); + + $state = str_split($data); + + $sel_status = [ + 'studio' => (int) $state[0], + 'lock' => (int) $state[1], + 'selectedfrom' => (int) $state[2], + 'power' => (int) $state[3], + ]; - return $this->sel_status; + return $sel_status; } - + /** * Locks the Studio Selector. The remote selector panels in the studio will * no longer operate the selector. The buttons on the main panel will * continue to work. */ - public function lock() { - $this->cmd('L'); + public static function setLock() + { + self::cmd('L'); } /** * Runs a command against URY's Physical Studio Selector. Be careful. - * @param String $cmd (Q)uery, (L)ock, (U)nlock, S[1-8] - * @return String Status for Query, or ACK/FLK for other commands. + * + * @param string $cmd (Q)uery, (L)ock, (U)nlock, S[1-8] + * + * @return string Status for Query, or ACK/FLK for other commands. */ - private function cmd($cmd) { - $h = fsockopen('tcp://' . Config::$selector_telnet_host, Config::$selector_telnet_port, $errno, $errstr, 10); + private static function cmd($cmd) + { + $h = fsockopen('tcp://'.Config::$selector_telnet_host, Config::$selector_telnet_port, $errno, $errstr, 10); //Read through the welcome "studio selector:" message (16x2bytes) fgets($h, 32); //Run command - fwrite($h, $cmd . "\n"); + fwrite($h, $cmd."\n"); //Read response (4x2bytes) $response = fgets($h, 16); @@ -201,190 +178,362 @@ private function cmd($cmd) { return trim($response); } - public static function setStudio($studio) { + public static function setStudio($studio) + { if (($studio <= 0) || ($studio > 8)) { - return ['myury_errors' => 'Invalid Studio ID']; - ; + return ['myradio_errors' => 'Invalid Studio ID']; } - $status = self::getStatusAtTime(time()); + $status = self::getStatusAtTime(); if ($studio == $status['studio']) { - throw new MyRadioException('Source ' . $studio . ' is already selected'); + throw new MyRadioException('Source '.$studio.' is already selected.'); } - if ((($studio == 1) && (!$status['s1power'])) || - (($studio == 2) && (!$status['s2power'])) || - (($studio == 4) && (!$status['s4power']))) { - throw new MyRadioException('Source ' . $studio . ' is not powered'); + if ((($studio == 1) && (!$status['s1power'])) + || (($studio == 2) && (!$status['s2power'])) + || (($studio == 4) && (!$status['s4power'])) + || (($studio == 5) && (!$status['s5power'])) + ) { + throw new MyRadioException('Source '.$studio.' is not powered.'); } if ($status['lock'] != 0) { throw new MyRadioException('Selector Locked'); } - $sel = new MyRadio_Selector(); - $response = $sel->cmd('S' . $studio); + $response = self::cmd('S'.$studio); if ($response === 'FLK') { throw new MyRadioException('Selector Locked'); } elseif ($response === 'ACK') { - return; + // DB may not have updated from the physical selector, so force it. + $statusUpdated = self::getStatusAtTime(); + $statusUpdated['selectedfrom'] = 1; + $statusUpdated['studio'] = $studio; + $statusUpdated['lastmod'] = time(); + return $statusUpdated; + } + } + + /** + * Returns which selector action was last performed at the time given. + * + * @param int $time + * + * @return int + */ + public static function getSelActionAtTime($time = null) + { + if ($time === null) { + $time = time(); + } + + $result = Database::getInstance()->fetchColumn( + 'SELECT action FROM public.selector WHERE time <= $1 + AND action >= 4 AND action <= 11 + ORDER BY time DESC + LIMIT 1', + [CoreUtils::getTimestamp($time)] + ); + + if (!$result) { + return 0; } + return $result[0]; } /** - * Returns what studio was on air at the time given + * Returns what studio was on air at the time given. + * * @param int $time + * * @return int */ - public static function getStudioAtTime($time) { - $result = Database::getInstance()->fetch_column( - 'SELECT action FROM public.selector WHERE time <= $1 - AND action >= 4 AND action <= 11 - ORDER BY time DESC - LIMIT 1', [CoreUtils::getTimestamp($time)]); - return $result[0] - 3; + public static function getStudioAtTime($time = null) + { + $result = self::getSelActionAtTime($time); + + return $result - 3; } /** - * Returns where the selector was set from at the time given + * Returns where the selector was set from at the time given. + * * @param int $time + * * @return int */ - public static function getSetbyAtTime($time) { - $result = Database::getInstance()->fetch_column( - 'SELECT setby FROM public.selector WHERE time <= $1 - AND action >= 4 AND action <= 11 - ORDER BY time DESC - LIMIT 1', [CoreUtils::getTimestamp($time)]); + public static function getSetbyAtTime($time = null) + { + if ($time === null) { + $time = time(); + } + + $result = Database::getInstance()->fetchColumn( + 'SELECT setby FROM public.selector WHERE time <= $1 + AND action >= 4 AND action <= 11 + ORDER BY time DESC + LIMIT 1', + [CoreUtils::getTimestamp($time)] + ); + + if (empty($result)) { + return 0; + } + return (int) $result[0]; } /** - * Returns the power state of studio1 at the time given + * Returns the power state of studio1 at the time given. + * * @param int $time + * * @return bool */ - public static function getStudio1PowerAtTime($time) { - $result = Database::getInstance()->fetch_column( - 'SELECT action FROM public.selector WHERE time <= $1 - AND action >= 13 AND action <= 14 - ORDER BY time DESC - LIMIT 1', [CoreUtils::getTimestamp($time)]); + public static function getStudio1PowerAtTime($time = null) + { + if ($time === null) { + $time = time(); + } + + $result = Database::getInstance()->fetchColumn( + 'SELECT action FROM public.selector WHERE time <= $1 + AND action >= 13 AND action <= 14 + ORDER BY time DESC + LIMIT 1', + [CoreUtils::getTimestamp($time)] + ); + + if (empty($result)) { + return false; + } + return ($result[0] == 13) ? true : false; } /** - * Returns the power state of studio2 at the time given + * Returns the power state of studio2 at the time given. + * * @param int $time + * * @return bool */ - public static function getStudio2PowerAtTime($time) { - $result = Database::getInstance()->fetch_column( - 'SELECT action FROM public.selector WHERE time <= $1 - AND action >= 15 AND action <= 16 - ORDER BY time DESC - LIMIT 1', [CoreUtils::getTimestamp($time)]); + public static function getStudio2PowerAtTime($time = null) + { + if ($time === null) { + $time = time(); + } + + $result = Database::getInstance()->fetchColumn( + 'SELECT action FROM public.selector WHERE time <= $1 + AND action >= 15 AND action <= 16 + ORDER BY time DESC + LIMIT 1', + [CoreUtils::getTimestamp($time)] + ); + + if (empty($result)) { + return false; + } + return ($result[0] == 15) ? true : false; } /** - * Returns the lock state at the time given + * Returns the lock state at the time given. + * * @param int $time + * * @return int */ - public static function getLockAtTime($time) { - $result = Database::getInstance()->fetch_column( - 'SELECT action FROM public.selector WHERE time <= $1 - AND action >= 1 AND action <= 3 - ORDER BY time DESC - LIMIT 1', [CoreUtils::getTimestamp($time)]); + public static function getLockAtTime($time = null) + { + if ($time === null) { + $time = time(); + } + + $result = Database::getInstance()->fetchColumn( + 'SELECT action FROM public.selector WHERE time <= $1 + AND action >= 1 AND action <= 3 + ORDER BY time DESC + LIMIT 1', + [CoreUtils::getTimestamp($time)] + ); + + if (empty($result)) { + return false; + } + return ($result[0] == 3) ? 0 : (int) $result[0]; } /** - * Returns the time last modified before the time given + * Returns the time last modified before the time given. + * * @param int $time + * * @return int */ - public static function getLastModAtTime($time) { - $result = Database::getInstance()->fetch_column( - 'SELECT time FROM public.selector WHERE time <= $1 - ORDER BY time DESC - LIMIT 1', [CoreUtils::getTimestamp($time)]); + public static function getLastModAtTime($time = null) + { + if ($time === null) { + $time = time(); + } + + $result = Database::getInstance()->fetchColumn( + 'SELECT time FROM public.selector WHERE time <= $1 + ORDER BY time DESC + LIMIT 1', + [CoreUtils::getTimestamp($time)] + ); + + if (!$result) { + return 1; + } + return strtotime($result[0]); } /** - * Returns the selector status at the time given + * Returns the selector status at the time given. + * * @param int $time + * * @return array */ - public static function getStatusAtTime($time) { - return array( + public static function getStatusAtTime($time = null) + { + if ($time === null) { + $time = time(); + } + + $status = self::remoteStreams(); + + // S4 is OB Feed + $ob_status = false; + if (isset($status["s1"]) || isset($status["s2"])) { + $ob_status = $status["s1"] || $status["s2"]; + } + + return [ + 'ready' => $status['ready'], 'studio' => self::getStudioAtTime($time), 'lock' => self::getLockAtTime($time), 'selectedfrom' => self::getSetbyAtTime($time), 's1power' => self::getStudio1PowerAtTime($time), 's2power' => self::getStudio2PowerAtTime($time), - 's4power' => (self::remoteStreams()['s1']) ? true : false, - 'lastmod' => self::getLastModAtTime($time) - ); + 's3power' => true, //Jukebox + 's4power' => $ob_status, //OB + 's5power' => (isset($status['ws'])) ? $status['ws'] : false, + 's8power' => true, //Off Air + 'lastmod' => self::getLastModAtTime($time), + ]; } /** * SERIOUSLY, KNOW WHAT YOU ARE DOING WITH THIS METHOD. - * + * * Calling this method will *terminate station output*, replacing it with * our pre-mixed audio for use in cases of national emergency such as * terrorist attacks or the death of someone in the royal family. - * + * * Jukebox has this file requested multiple times, then our studio selector * is told to switch to Jukebox, then lock itself so only technical staff can * restore studio functionality. - * + * * It also emails an array of various important people to inform them that * this has happened. */ - public function startObit() { + public static function setObit() + { //Empty all existing request queues iTones_Utils::emptyQueues(); //Request the obit file a few times (5h of content) - for ($i = 0; $i < 5; $i++) { + for ($i = 0; $i < 5; ++$i) { iTones_Utils::requestFile(Config::$jukebox_obit_file); } - + //Skip to the next track iTones_Utils::skip(); - + //Switch to studio 3 try { - $this->setStudio(3); + self::setStudio(3); } catch (MyRadioException $e) { trigger_error('OBIT: Could not change selector source: '.$e->getMessage()); } - + //Lock the selector - $this->lock(); - + self::setLock(); + //Email people - MyRadioEmail::sendEmailToComputing('OBIT INITIATED', - 'Urgent: Initiated Obit procedure for station as requested by ' - . MyRadio_User::getInstance()->getName() . ' - ' - . MyRadio_User::getInstance()->getEmail()); - + MyRadioEmail::sendEmailToList( + MyRadio_List::getInstance(Config::$obit_list_id), + 'OBIT INITIATED', + 'Urgent: Initiated Obit procedure for station as requested by ' + .MyRadio_User::getInstance()->getName().' - ' + .MyRadio_User::getInstance()->getEmail() + ); + //Store the event for Timelord file_put_contents('/tmp/myradio-obit', 1); } - + /** * Returns if an obit event is happening. */ - public function isObitHappening() { + public static function isObitHappening() + { if (file_exists('/tmp/myradio-obit')) { - return (bool)file_get_contents('/tmp/myradio-obit'); + return (bool) file_get_contents('/tmp/myradio-obit'); } else { return false; } } -} \ No newline at end of file + /** + * Returns the state of the remote OB feeds in an associative array. + * + * @return array + */ + public static function remoteStreams() + { + if (file_exists(Config::$ob_remote_status_file)) { + $data = file(Config::$ob_remote_status_file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + + if ($data) { + $response = ['ready' => true]; + foreach ($data as $feed) { + $state = explode('=', $feed); + $response[trim($state[0])] = (bool) trim($state[1]); + } + + return $response; + } + } + + return [ + 'ready' => false, + ]; + } + + /** + * Returns the length of the current silence, if any. + * + * @return int + */ + public static function isSilence() + { + $result = Database::getInstance()->fetchOne( + 'SELECT starttime, stoptime + FROM jukebox.silence_log + ORDER BY silenceid DESC LIMIT 1' + ); + + if (empty($result['stoptime'])) { + return time() - strtotime($result['starttime']); + } else { + return 0; + } + } +} diff --git a/src/Classes/ServiceAPI/MyRadio_ShortURL.php b/src/Classes/ServiceAPI/MyRadio_ShortURL.php new file mode 100644 index 000000000..b3ffc0979 --- /dev/null +++ b/src/Classes/ServiceAPI/MyRadio_ShortURL.php @@ -0,0 +1,196 @@ +short_url_id = (int)$data['short_url_id']; + $this->slug = $data['slug']; + $this->redirect_to = $data['redirect_to']; + } + + public static function create(string $slug, string $redirectTo): MyRadio_ShortURL + { + $sql = 'INSERT INTO public.short_urls (slug, redirect_to) + VALUES ($1, $2) + RETURNING short_url_id'; + $result = self::$db->fetchOne($sql, [$slug, $redirectTo]); + + return self::getInstance($result['short_url_id']); + } + + public function getID() + { + return $this->short_url_id; + } + + /** + * @return string + */ + public function getSlug(): string + { + return $this->slug; + } + + /** + * @param string $slug + * @return MyRadio_ShortURL + */ + public function setSlug(string $slug): MyRadio_ShortURL + { + self::$db->query( + 'UPDATE public.short_urls + SET slug = $2 + WHERE short_url_id = $1', + [$this->short_url_id, $slug] + ); + $this->slug = $slug; + $this->updateCacheObject(); + return $this; + } + + /** + * @return string + */ + public function getRedirectTo(): string + { + return $this->redirect_to; + } + + /** + * @param string $redirectTo + * @return MyRadio_ShortURL + */ + public function setRedirectTo(string $redirectTo): MyRadio_ShortURL + { + self::$db->query( + 'UPDATE public.short_urls + SET redirect_to = $2 + WHERE short_url_id = $1', + [$this->short_url_id, $redirectTo] + ); + $this->redirect_to = $redirectTo; + $this->updateCacheObject(); + return $this; + } + + /** + * Deletes this short URL. + */ + public function delete() + { + self::$db->query( + 'DELETE FROM public.short_urls + WHERE short_url_id = $1', + [$this->short_url_id] + ); + } + + /** + * @return MyRadio_ShortURL[] + */ + public static function getAll() + { + $rows = self::$db->fetchColumn( + 'SELECT short_url_id FROM public.short_urls', + [] + ); + $results = []; + foreach ($rows as $id) { + $results[] = self::getInstance($id); + } + return $results; + } + + public function logClick($userAgent, $ipAddress) + { + self::$db->query( + 'INSERT INTO public.short_url_clicks (short_url_id, click_time, user_agent, ip_address) + VALUES ($1, NOW(), $2, $3)', + [$this->short_url_id, $userAgent, $ipAddress] + ); + } + + public static function getForm(): MyRadioForm + { + $domain = preg_replace('{^//}', '', Config::$website_url); + return (new MyRadioForm( + 'shorturlfrm', + 'Website', + 'editShortUrl', + [ + 'title' => 'Edit Short URL' + ] + ))->addField( + new MyRadioFormField( + 'slug', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Slug', + 'explanation' => "The bit that goes after https://$domain. Don't include $domain or a slash." + ] + ) + )->addField( + new MyRadioFormField( + 'redirect_to', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Redirect URL', + 'explanation' => 'Where this short URL will take people.' + ] + ) + ); + } + + public function getEditForm(): MyRadioForm + { + return self::getForm()->editMode( + $this->getID(), + [ + 'slug' => $this->getSlug(), + 'redirect_to' => $this->getRedirectTo() + ] + ); + } + + protected static function factory($itemid) + { + $sql = 'SELECT short_url_id, slug, redirect_to FROM public.short_urls + WHERE short_url_id = $1 LIMIT 1'; + $result = self::$db->fetchOne($sql, [$itemid]); + + if (empty($result)) { + throw new MyRadioException('That short URL does not exist.', 404); + } + + return new self($result); + } + + public function toDataSource($mixins = []) + { + return [ + 'short_url_id' => $this->short_url_id, + 'slug' => $this->slug, + 'redirect_to' => $this->redirect_to, + 'edit_link' => [ + 'display' => 'icon', + 'value' => 'pencil', + 'title' => 'Click here to edit this short URL', + 'url' => URLUtils::makeURL('Website', 'editShortUrl', ['shorturlid' => $this->getID()]), + ], + ]; + } +} diff --git a/src/Classes/ServiceAPI/MyRadio_Show.php b/src/Classes/ServiceAPI/MyRadio_Show.php old mode 100644 new mode 100755 index 469097082..4cbe29f77 --- a/src/Classes/ServiceAPI/MyRadio_Show.php +++ b/src/Classes/ServiceAPI/MyRadio_Show.php @@ -1,509 +1,1264 @@ - * @package MyRadio_Scheduler - * @uses \Database + * The Show class is used to create, view and manupulate Shows within the new MyRadio Scheduler Format. * + * @uses \Database */ +class MyRadio_Show extends MyRadio_Metadata_Common +{ + const BASE_SHOW_SQL = + 'SELECT show_id, + show.show_type_id, + show.submitted, + show.memberid AS owner, + show.podcast_explicit::int, + array_to_json(metadata.metadata_key_id) AS metadata_keys, + array_to_json(metadata.metadata_value) AS metadata_values, + array_to_json(image_metadata.image_metadata_key_id) AS image_metadata_keys, + array_to_json(image_metadata.image_metadata_value) AS image_metadata_values, + array_to_json(credits.credit_type_id) AS credit_types, + array_to_json(credits.creditid) AS credits, + array_to_json(genre.genre_id) AS genres, + array_to_json(season.show_season_id) AS seasons, + subtype.show_subtype_id as subtype_id + FROM + schedule.show + NATURAL FULL JOIN + ( + SELECT + show_id, + array_agg(metadata_key_id) AS metadata_key_id, + array_agg(metadata_value) AS metadata_value + FROM schedule.show_metadata + WHERE effective_from <= NOW() + AND (effective_to IS NULL OR effective_to >= NOW()) + AND approvedid IS NOT NULL + GROUP BY show_id + ) AS metadata + NATURAL FULL JOIN + ( + SELECT + show_id, + array_agg(metadata_key_id) AS image_metadata_key_id, + array_agg(metadata_value) AS image_metadata_value + FROM schedule.show_image_metadata + WHERE effective_from <= NOW() + AND (effective_to IS NULL OR effective_to >= NOW()) + AND approvedid IS NOT NULL + GROUP BY show_id + ) AS image_metadata + NATURAL FULL JOIN + ( + SELECT + show_id, + array_agg(credit_type_id) AS credit_type_id, + array_agg(creditid) AS creditid + FROM schedule.show_credit + WHERE effective_from <= NOW() + AND (effective_to IS NULL OR effective_to >= NOW()) + AND approvedid IS NOT NULL + GROUP BY show_id + ) AS credits + NATURAL FULL JOIN + ( + SELECT + show_id, + array_agg(genre_id) AS genre_id + FROM schedule.show_genre + WHERE effective_from <= NOW() + AND (effective_to IS NULL OR effective_to >= NOW()) + AND approvedid IS NOT NULL + GROUP BY show_id + ) AS genre + NATURAL FULL JOIN + ( + SELECT + show_id, + array_agg(show_season_id ORDER BY termid, submitted) AS show_season_id + FROM schedule.show_season + GROUP BY show_id + ) AS season + NATURAL FULL JOIN ( + SELECT show_subtype_id, + show_id + FROM schedule.show_season_subtype + GROUP BY show_subtype_id, show_id + ) AS subtype'; + + private $show_id; + protected $owner; + protected $credits = []; + private $genres; + private $show_type; + private $submitted_time; + private $season_ids; + private $photo_url; + private $podcast_explicit; + private $subtype_id; -class MyRadio_Show extends MyRadio_Metadata_Common { - private $show_id; - private $owner; - protected $credits = array(); - private $genres; - private $show_type; - private $submitted_time; - private $season_ids; - private $photo_url; - - protected function __construct($show_id) { - $this->show_id = $show_id; - self::initDB(); - - $result = self::$db->fetch_one('SELECT show_type_id, submitted, memberid, - (SELECT array(SELECT metadata_key_id FROM schedule.show_metadata - WHERE show_id=$1 AND effective_from <= NOW() AND - (effective_to IS NULL OR effective_to >= NOW()) - ORDER BY effective_from, show_metadata_id)) AS metadata_types, - (SELECT array(SELECT metadata_value FROM schedule.show_metadata - WHERE show_id=$1 AND effective_from <= NOW() AND - (effective_to IS NULL OR effective_to >= NOW()) - ORDER BY effective_from, show_metadata_id)) AS metadata, - (SELECT array(SELECT metadata_value FROM schedule.show_image_metadata - WHERE show_id=$1 AND effective_from <= NOW() AND - (effective_to IS NULL OR effective_to >= NOW()) - ORDER BY effective_from, show_image_metadata_id)) AS image_metadata, - (SELECT array(SELECT credit_type_id FROM schedule.show_credit - WHERE show_id=$1 AND effective_from <= NOW() AND - (effective_to IS NULL OR effective_to >= NOW()) AND approvedid IS NOT NULL - ORDER BY show_credit_id)) AS credit_types, - (SELECT array(SELECT creditid FROM schedule.show_credit - WHERE show_id=$1 AND effective_from <= NOW() AND - (effective_to IS NULL OR effective_to >= NOW()) AND approvedid IS NOT NULL - ORDER BY show_credit_id)) AS credits, - (SELECT array(SELECT genre_id FROM schedule.show_genre - WHERE show_id=$1 AND effective_from <= NOW() AND - (effective_to IS NULL OR effective_to >= NOW()) AND approvedid IS NOT NULL - ORDER BY show_genre_id)) AS genres - FROM schedule.show WHERE show_id=$1', array($show_id)); - - //Deal with the easy fields - $this->owner = (int) $result['memberid']; - $this->show_type = (int) $result['show_type_id']; - $this->submitted_time = strtotime($result['submitted']); - $this->genres = self::$db->decodeArray($result['genres']); - - //Deal with the Credits arrays - $credit_types = self::$db->decodeArray($result['credit_types']); - $credits = self::$db->decodeArray($result['credits']); - - for ($i = 0; $i < sizeof($credits); $i++) { - if (empty($credits[$i])) { - continue; - } - $this->credits[] = array('type' => (int)$credit_types[$i], 'memberid' => $credits[$i], - 'User' => MyRadio_User::getInstance($credits[$i])); - } - - - //Deal with the Metadata arrays - $metadata_types = self::$db->decodeArray($result['metadata_types']); - $metadata = self::$db->decodeArray($result['metadata']); - - for ($i = 0; $i < sizeof($metadata); $i++) { - if (self::isMetadataMultiple($metadata_types[$i])) { - //Multiples should be an array - $this->metadata[$metadata_types[$i]][] = $metadata[$i]; - } else { - $this->metadata[$metadata_types[$i]] = $metadata[$i]; - } - } - - //Deal with Show Photo /** - * @todo Support general photo attachment? + * @param $result Array + * show_id int + * show_type_id int + * submitted strtotimeable string + * owner int + * metadata_keys array[int] + * metadata_values array[string] + * image_metadata_keys array[int] + * image_metadata_values array[string] + * credit_types array[int] + * credits array[int] + * genres array[int] + * seasons array[int] */ - $this->photo_url = Config::$default_person_uri; - if ($result['image_metadata'] !== '{}') { - $this->photo_url = Config::$public_media_uri.'/'.self::$db->decodeArray($result['image_metadata'])[0]; - } - - //Get information about Seasons - $this->season_ids = self::$db->fetch_column('SELECT show_season_id - FROM schedule.show_season WHERE show_id=$1', array($show_id)); - } - - /** - * Get the cache key for the Show with this ID. - * @param int $id - * @return String - */ - public static function getCacheKey($id) { - return 'MyRadio_Show-'.$id; - } - - /** - * Creates a new MyRadio Show and returns an object representing it - * @param Array $params An array of Show properties compatible with the Models/Scheduler/showfrm Form: - * title: The name of the show
    - * description: The description of the show
    - * genres: An array of 0 or more genre ids this Show is a member of
    - * tags: A string of 0 or more space-seperated tags this Show relates to
    - * credits: a 2D Array with keys member and credittype. member is Array of Users, credittype is Array of
    - * corresponding credittypeids - * showtypeid: The ID of the type of show (see schedule.show_type). Defaults to "Show" - * location: The ID of the location the show will be in - * mixclouder: If true, the show will be published to Mixcloud after broadcast. - * Requires https://github.com/UniversityRadioYork/mixclouder. - * - * title, description, credits and credittypes are required fields. - * - * As this is the initial creation, all tags are approved by the submitted so the show has some initial values - * - * @todo location (above) Is not in the Show creation form - * @throws MyRadioException - */ - public static function create($params = array()) { - //Validate input - $required = array('title', 'description', 'credits'); - foreach ($required as $field) { - if (!isset($params[$field])) { - throw new MyRadioException('Parameter ' . $field . ' was not provided.'); - } - } - - self::initDB(); - - //Get or set the show type id - if (empty($params['showtypeid'])) { - $rtype = self::$db->fetch_column('SELECT show_type_id FROM schedule.show_type WHERE name=\'Show\''); - if (empty($rtype[0])) { - throw new MyRadioException('There is no Show ShowType Available!', MyRadioException::FATAL); - } - $params['showtypeid'] = (int) $rtype[0]; - } - - if (!isset($params['genres'])) { - $params['genres'] = array(); - } - if (!isset($params['tags'])) { - $params['tags'] = ''; - } - - //We're all or nothing from here on out - transaction time - self::$db->query('BEGIN'); - - //Add the basic info, getting the show id - - $result = self::$db->fetch_column('INSERT INTO schedule.show (show_type_id, submitted, memberid) - VALUES ($1, NOW(), $2) RETURNING show_id', array($params['showtypeid'], $_SESSION['memberid']), true); - $show_id = $result[0]; - - //Right, set the title and description next - foreach (array('title', 'description') as $key) { - self::$db->query('INSERT INTO schedule.show_metadata - (metadata_key_id, show_id, metadata_value, effective_from, memberid, approvedid) - VALUES ($1, $2, $3, NOW(), $4, $4)', - array(self::getMetadataKey($key), $show_id, $params[$key], $_SESSION['memberid']), true); - } - - //Genre time powers activate! - if (!is_array($params['genres'])) { - $params['genres'] = array($params['genres']); - } - foreach ($params['genres'] as $genre) { - if (!is_numeric($genre)) { - continue; - } - self::$db->query('INSERT INTO schedule.show_genre (show_id, genre_id, effective_from, memberid, approvedid) - VALUES ($1, $2, NOW(), $3, $3)', array($show_id, $genre, $_SESSION['memberid']), true); - } - - //Explode the tags - $tags = explode(' ', $params['tags']); - foreach ($tags as $tag) { - self::$db->query('INSERT INTO schedule.show_metadata - (metadata_key_id, show_id, metadata_value, effective_from, memberid, approvedid) - VALUES ($1, $2, $3, NOW(), $4, $4)', - array(self::getMetadataKey('tag'), $show_id, $tag, $_SESSION['memberid']), true); - } - - //Set a location - if (empty($params['location'])) { - /** - * Hardcoded default to Studio 1 - * @todo Location support - */ - $params['location'] = 1; - } - self::$db->query('INSERT INTO schedule.show_location - (show_id, location_id, effective_from, memberid, approvedid) VALUES ($1, $2, NOW(), $3, $3)', array( - $show_id, $params['location'], $_SESSION['memberid'] - ), true); - - //And now all that's left is who's on the show - for ($i = 0; $i < sizeof($params['credits']['member']); $i++) { - //Skip blank entries - if (empty($params['credits']['member'][$i])) { - continue; - } - self::$db->query('INSERT INTO schedule.show_credit (show_id, credit_type_id, creditid, effective_from, - memberid, approvedid) VALUES ($1, $2, $3, NOW(), $4, $4)', - array($show_id, (int) $params['credits']['credittype'][$i],$params['credits']['member'][$i]->getID(), $_SESSION['memberid']), true); - } - - //Actually commit the show to the database! - self::$db->query('COMMIT'); - - $show = new self($show_id); - + protected function __construct($result) + { + $this->show_id = (int) $result['show_id']; + + //Deal with the easy fields + $this->owner = (int) $result['owner']; + $this->show_type = (int) $result['show_type_id']; + $this->submitted_time = strtotime($result['submitted']); + $this->podcast_explicit = (bool) $result['podcast_explicit']; + $this->subtype_id = (int) $result['subtype_id']; + + $this->genres = json_decode($result['genres']); + if ($this->genres === null) { + $this->genres = []; + } + + //Deal with the Credits arrays + $credit_types = $result['credit_types'] !== null ? json_decode($result['credit_types']) : []; + $credits = $result['credits'] !== null ? json_decode($result['credits']) : []; + + for ($i = 0; $i < sizeof($credits); ++$i) { + if (empty($credits[$i])) { + continue; + } + $this->credits[] = [ + 'type' => (int) $credit_types[$i], + 'memberid' => $credits[$i], + 'User' => MyRadio_User::getInstance($credits[$i]), + ]; + } + + //Deal with the Metadata arrays + $metadata_types = json_decode($result['metadata_keys']); + $metadata = json_decode($result['metadata_values']); + if ($metadata_types === null) { + $metadata_types = []; + } + if ($metadata === null) { + $metadata = []; + } + + for ($i = 0; $i < sizeof($metadata); ++$i) { + if (self::isMetadataMultiple($metadata_types[$i])) { + //Multiples should be an array + $this->metadata[$metadata_types[$i]][] = $metadata[$i]; + } else { + $this->metadata[$metadata_types[$i]] = $metadata[$i]; + } + } + + //Deal with Show Photo + /* + * @todo Support general photo attachment? + */ + $this->photo_url = Config::$default_person_uri; + if ($result['image_metadata_values'] !== null) { + $image_metadata = json_decode($result['image_metadata_values']); + $this->photo_url = Config::$public_media_uri.'/'.$image_metadata[0]; + } + + //Get information about Seasons + if ($result['seasons'] !== null) { + $this->season_ids = json_decode($result['seasons']); + } else { + $this->season_ids = []; + } + } + + protected static function factory($showid) + { + $sql = self::BASE_SHOW_SQL.' WHERE show_id=$1'; + $result = self::$db->fetchOne($sql, [$showid]); + + if (empty($result)) { + throw new MyRadioException("The specified Show (show id: " . $showid . ") does not seem to exist", 404); + } + + return new self($result); + } + /** - * Enable mixcloud upload if requested + * Creates a new MyRadio Show and returns an object representing it. + * + * @param array $params An assoc array (possibly decoded from JSON), + * taking a format generally based on what toDataSource produces + * Properties may be "genres" (["Jazz", ...], "credits" ([["memberid": 7449, "typeid": 1], ...]), + * location or any valid metadata key. + * The title/description metadata keys, and the credits key, are all required. + * e.g. Set upload_state: "Requested" to set this show to be uploaded to Mixclouder after broadcast. + * + * As this is the initial creation, all data are approved by the submitter + * so the show has some initial values + * + * @throws MyRadioException */ - if ($params['mixclouder']) { - $show->setMeta('upload_state', 'Requested'); - } - - return $show; - } - - public function getNumberOfSeasons() { - return sizeof($this->season_ids); - } - - public function getAllSeasons() { - $seasons = array(); - foreach ($this->season_ids as $season_id) { - $seasons[] = MyRadio_Season::getInstance($season_id); - } - return $seasons; - } - - /** - * Internally associates a Season with this Show. - * Does not persist in database. Used for updating the cache. - * @param int $id - */ - public function addSeason($id) { - $this->season_ids[] = $id; - $this->updateCacheObject(); - } - - public function getID() { - return $this->show_id; - } - - public function getWebpage() { - return '//ury.org.uk/schedule/shows/' . $this->getID(); - } - - /** - * Get the web url for the Show Photo - * @return String - */ - public function getShowPhoto() { - return $this->photo_url; - } - - /** - * Returns the ID for the type of Show - * @return int - */ - public function getShowType() { - return $this->show_type; - } - - /** - * Return the primary Genre. Shows generally only have one anyway. - */ - public function getGenre() { - return $this->genres[0]; - } - - public function isCurrentUserAnOwner() { - if ($this->owner === $_SESSION['memberid']) { - return true; - } - foreach ($this->getCreditObjects() as $user) { - if ($user->getID() === $_SESSION['memberid']) { - return true; - } - } - return false; - } - - public function setShowPhoto($tmp_path) { - $result = self::$db->fetch_column('INSERT INTO schedule.show_image_metadata (memberid, approvedid, - metadata_key_id, metadata_value, show_id) VALUES ($1, $1, $2, $3, $4) RETURNING show_image_metadata_id', - array($_SESSION['memberid'], self::getMetadataKey('player_image'), 'tmp', $this->getID()))[0]; - - $suffix = 'image_meta/ShowImageMetadata/'.$result.'.png'; - $path = Config::$public_media_path.'/'.$suffix; - move_uploaded_file($tmp_path, $path); - - self::$db->query('UPDATE schedule.show_image_metadata SET effective_to=NOW() WHERE metadata_key_id=$1 AND show_id=$2 - AND effective_from IS NOT NULL', array(self::getMetadataKey('player_image'), $this->getID())); - - self::$db->query('UPDATE schedule.show_image_metadata SET effective_from=NOW(), metadata_value=$1 - WHERE show_image_metadata_id=$2', array($suffix, $result)); - } - - /** - * Sets a metadata key to the specified value. - * - * If any value is the same as an existing one, no action will be taken. - * If the given key has is_multiple, then the value will be added as a new, additional key. - * If the key does not have is_multiple, then any existing values will have effective_to - * set to the effective_from of this value, effectively replacing the existing value. - * This will *not* unset is_multiple values that are not in the new set. - * - * @param String $string_key The metadata key - * @param mixed $value The metadata value. If key is_multiple and value is an array, will create instance - * for value in the array. - * @param int $effective_from UTC Time the metavalue is effective from. Default now. - * @param int $effective_to UTC Time the metadata value is effective to. Default NULL (does not expire). - * @param null $table Used for compatibility with parent. - * @param null $pkey Used for compatibility with parent. - */ - public function setMeta($string_key, $value, $effective_from = null, $effective_to = null, - $table = null, $pkey = null) { - $r = parent::setMeta($string_key, $value, $effective_from, $effective_to, - 'schedule.show_metadata', 'show_id'); - $this->updateCacheObject(); - return $r; - } - - /** - * Sets the Genre, if it hasn't changed - * @param int $genreid - */ - public function setGenre($genreid) { - if (empty($genreid)) { - throw new MyRadioException('Genre cannot be empty!', 400); - } - if ($genreid != $this->getGenre()) { - self::$db->query('UPDATE schedule.show_genre SET effective_to=NOW() WHERE show_id=$1', - array($this->getID())); - self::$db->query('INSERT INTO schedule.show_genre (show_id, genre_id, effective_from, memberid, approvedid) - VALUES ($1, $2, NOW(), $3, $3)', array($this->getID(), $genreid, MyRadio_User::getInstance()->getID())); - $this->genres = [$genreid]; - $this->updateCacheObject(); - } - } - - /** - * Updates the list of Credits. - * - * Existing credits are kept active, ones that are not in the new list are set to effective_to now, - * and ones that are in the new list but not exist are created with effective_from now. - * - * @param MyRadio_User[] $users An array of Users associated. - * @param int[] $credittypes The relevant credittypeid for each User. - */ - public function setCredits($users, $credittypes, $table = null, $pkey = null) { - $r = parent::setCredits($users, $credittypes, 'schedule.show_credit', 'show_id'); - $this->updateCacheObject(); - return $r; - } - - /** - * @todo Document this method - * @todo Ajax the All Shows page - this isn't a particularly nice query - */ - public static function getAllShows($show_type_id = 1) { - $show_ids = self::$db->fetch_column( - 'SELECT show_id FROM schedule.show ' - . 'WHERE show_type_id=$1 ' - . 'ORDER BY (' - . ' SELECT metadata_value FROM schedule.show_metadata ' - . ' WHERE show_id=show_id AND metadata_key_id=2 ' - . ' AND effective_from <= NOW() ' - . ' AND (effective_to IS NULL OR effective_to > NOW()) ' - . ' ORDER BY effective_from DESC LIMIT 1' - . ');', - [$show_type_id] - ); - return array_map( - function($show_id) { return self::getInstance($show_id); }, - array_values($show_ids) - ); - } - - /** - * Find the most messaged shows - * @param int $date If specified, only messages for timeslots since $date are counted. - * @return array An array of 30 Shows that have been put through toDataSource, with the addition of a msg_count key, - * referring to the number of messages sent to that show. - */ - public static function getMostMessaged($date = 0) { - $result = self::$db->fetch_all('SELECT show.show_id, count(*) as msg_count FROM sis2.messages - LEFT JOIN schedule.show_season_timeslot ON messages.timeslotid = show_season_timeslot.show_season_timeslot_id - LEFT JOIN schedule.show_season ON show_season_timeslot.show_season_id = show_season.show_season_id - LEFT JOIN schedule.show ON show_season.show_id = show.show_id - WHERE show_season_timeslot.start_time > $1 GROUP BY show.show_id ORDER BY msg_count DESC LIMIT 30', - array(CoreUtils::getTimestamp($date))); - - $top = array(); - foreach ($result as $r) { - $show = self::getInstance($r['show_id'])->toDataSource(); - $show['msg_count'] = intval($r['msg_count']); - $top[] = $show; - } - - return $top; - } - - /** - * Returns the current Show on air, if there is one. - * @param int $time Optional integer timestamp - * - * @return MyRadio_Show|null - */ - public static function getCurrentShow($time = null) { - $timeslot = MyRadio_Timeslot::getCurrentTimeslot($time); - if (empty($timeslot)) { - return null; - } else { - return $timeslot->getSeason()->getShow(); - } - } - - /** - * Find the most listened Shows - * @param int $date If specified, only messages for timeslots since $date are counted. - * @return array An array of 30 Timeslots that have been put through toDataSource, with the addition of a msg_count key, - * referring to the number of messages sent to that show. - */ - public static function getMostListened($date = 0) { - $key = 'stats_show_mostlistened'; - if (($top = self::$cache->get($key)) !== false) { - return $top; - } - - $result = self::$db->fetch_all('SELECT show_id, SUM(listeners) AS listeners_sum FROM (SELECT show_season_id, - (SELECT COUNT(*) FROM strm_log - WHERE (starttime < show_season_timeslot.start_time AND endtime >= show_season_timeslot.start_time) - OR (starttime >= show_season_timeslot.start_time - AND starttime < show_season_timeslot.start_time + show_season_timeslot.duration)) AS listeners - FROM schedule.show_season_timeslot - WHERE start_time > $1) AS t1 LEFT JOIN schedule.show_season ON t1.show_season_id = show_season. show_season_id - GROUP BY show_id ORDER BY listeners_sum DESC LIMIT 30', - array(CoreUtils::getTimestamp($date))); - - $top = array(); - foreach ($result as $r) { - $show = self::getInstance($r['show_id'])->toDataSource(); - $show['listeners'] = intval($r['listeners_sum']); - $top[] = $show; - } - - self::$cache->set($key, $top, 86400); - return $top; - } - - public function toDataSource($full = true) { - $data = array( - 'show_id' => $this->getID(), - 'title' => $this->getMeta('title'), - 'credits' => implode(', ', $this->getCreditsNames(false)), - 'description' => $this->getMeta('description'), - 'show_type_id' => $this->show_type, - 'seasons' => array( - 'display' => 'text', - 'value' => $this->getNumberOfSeasons(), - 'title' => 'Click to see Seasons for this show', - 'url' => CoreUtils::makeURL('Scheduler', 'listSeasons', array('showid' => $this->getID()))), - 'editlink' => array( - 'display' => 'icon', - 'value' => 'script', - 'title' => 'Edit Show', - 'url' => CoreUtils::makeURL('Scheduler', 'editShow', array('showid' => $this->getID()))), - 'applylink' => array('display' => 'icon', - 'value' => 'calendar', - 'title' => 'Apply for a new Season', - 'url' => CoreUtils::makeURL('Scheduler', 'createSeason', array('showid' => $this->getID()))), - 'micrositelink' => array('display' => 'icon', - 'value' => 'extlink', - 'title' => 'View Show Microsite', - 'url' => $this->getWebpage()), - 'photo' => $this->getShowPhoto() - ); - - if ($full) { - $data['credits'] = array_map(function($x) { - $x['User'] = $x['User']->toDataSource(false); - return $x; - }, $this->getCredits()); - } - - return $data; - } + public static function create($params = []) + { + //Validate input + $required = ['title', 'description', 'credits', 'subtype']; + foreach ($required as $field) { + if (!isset($params[$field])) { + throw new MyRadioException('You must provide ' . $field, 400); + } + } + + self::initDB(); + + //Get or set the show type id + if (empty($params['showtypeid'])) { + $rtype = self::$db->fetchColumn('SELECT show_type_id FROM schedule.show_type WHERE name=\'Show\''); + if (empty($rtype[0])) { + throw new MyRadioException('There is no Show ShowType Available!', MyRadioException::FATAL); + } + $params['showtypeid'] = (int) $rtype[0]; + } + + if (!isset($params['genres'])) { + $params['genres'] = []; + } + if (!isset($params['tags'])) { + $params['tags'] = ''; + } + + // Support API calls where there is no session. + // @todo should this be system_user? + if (!empty($_SESSION['memberid'])) { + $creator = $_SESSION['memberid']; + } else { + $creator = $params['credits']['memberid'][0]; + } + + //We're all or nothing from here on out - transaction time + self::$db->query('BEGIN'); + + //Add the basic info, getting the show id + $result = self::$db->fetchColumn( + 'INSERT INTO schedule.show (show_type_id, submitted, memberid, podcast_explicit) + VALUES ($1, NOW(), $2, $3::boolean) RETURNING show_id', + [ + $params['showtypeid'], + $creator, + (isset($params['podcast_explicit']) && $params['podcast_explicit']) ? 1 : 0 + ], + true + ); + if (empty($result)) { + throw new MyRadioException('Inserting show record failed!', 500); + } + $show_id = $result[0]; + + //Right, set the title and description next + foreach (['title', 'description'] as $key) { + self::$db->query( + 'INSERT INTO schedule.show_metadata + (metadata_key_id, show_id, metadata_value, effective_from, memberid, approvedid) + VALUES ($1, $2, $3, NOW(), $4, $4)', + [self::getMetadataKey($key), $show_id, $params[$key], $creator], + true + ); + } + + //Genre time powers activate! + if (!is_array($params['genres'])) { + $params['genres'] = [$params['genres']]; + } + foreach ($params['genres'] as $genre) { + if (!is_numeric($genre)) { + continue; + } + self::$db->query( + 'INSERT INTO schedule.show_genre (show_id, genre_id, effective_from, memberid, approvedid) + VALUES ($1, $2, NOW(), $3, $3)', + [$show_id, $genre, $creator], + true + ); + } + + // Explode the tags + $tags = CoreUtils::explodeTags($params['tags']); + foreach ($tags as $tag) { + self::$db->query( + 'INSERT INTO schedule.show_metadata + (metadata_key_id, show_id, metadata_value, effective_from, memberid, approvedid) + VALUES ($1, $2, $3, NOW(), $4, $4)', + [self::getMetadataKey('tag'), $show_id, $tag, $creator], + true + ); + } + + //Set a location + if (empty($params['location'])) { + /* + * Hardcoded default to Studio 1 + * @todo Location support + */ + $params['location'] = 1; + } + self::$db->query( + 'INSERT INTO schedule.show_location + (show_id, location_id, effective_from, memberid, approvedid) + VALUES ($1, $2, NOW(), $3, $3)', + [ + $show_id, + $params['location'], + $creator, + ], + true + ); + + // Subtype too + self::$db->query( + 'INSERT INTO schedule.show_season_subtype + (show_id, show_subtype_id, effective_from) + VALUES ($1, (SELECT show_subtype_id FROM schedule.show_subtypes WHERE show_subtypes.class = $2), NOW())', + [$show_id, $params['subtype']] + ); + + //And now all that's left is who's on the show + for ($i = 0; $i < sizeof($params['credits']['memberid']); ++$i) { + //Skip blank entries + if (empty($params['credits']['memberid'][$i])) { + continue; + } + // Both a memberid and a User object are valid here. + // This is icky and should be fixed. + if (is_numeric($params['credits']['memberid'][$i])) { + $member = MyRadio_User::getInstance($params['credits']['memberid'][$i]); + } else { + $member = $params['credits']['memberid'][$i]; + } + self::$db->query( + 'INSERT INTO schedule.show_credit + (show_id, credit_type_id, creditid, effective_from, memberid, approvedid) + VALUES ($1, $2, $3, NOW(), $4, $4)', + [ + $show_id, + (int) $params['credits']['credittype'][$i], + $member->getID(), + $creator, + ], + true + ); + } + + //Actually commit the show to the database! + self::$db->query('COMMIT'); + + $show = self::factory($show_id); + + /* + * Enable mixcloud upload if requested + */ + if ($params['mixclouder']) { + $show->setMeta('upload_state', 'Requested'); + } + + return $show; + } + + public static function getForm() + { + return ( + new MyRadioForm( + 'sched_show', + 'Scheduler', + 'editShow', + [ + 'debug' => true, + 'title' => 'Scheduler', + 'subtitle' => 'Create a Show' + ] + ) + )->addField( + new MyRadioFormField('grp-basics', MyRadioFormField::TYPE_SECTION, ['label' => 'About My Show']) + )->addField( + new MyRadioFormField( + 'title', + MyRadioFormField::TYPE_TEXT, + [ + 'explanation' => 'Enter a name for your new show. Try and make it unique.', + 'label' => 'Show Name', + ] + ) + )->addField( + new MyRadioFormField( + 'description', + MyRadioFormField::TYPE_BLOCKTEXT, + [ + 'explanation' => 'Describe your show as best you can. This goes on the public-facing website.', + 'label' => 'Description', + ] + ) + )->addField( + new MyRadioFormField( + 'genres', + MyRadioFormField::TYPE_SELECT, + [ + 'options' => array_merge( + [['text' => 'Please select...', 'disabled' => true]], + MyRadio_Scheduler::getGenres() + ), + 'label' => 'Genre', + 'explanation' => 'What type of music do you play, if any?', + ] + ) + )->addField( + new MyRadioFormField( + 'subtype', + MyRadioFormField::TYPE_SELECT, + [ + 'options' => MyRadio_ShowSubtype::getOptions(), + 'label' => 'Subtype', + 'explanation' => 'Select the subtype for this show (speech, music, news, etc.)' + . ' If unsure, leave as Regular.' + ] + ) + )->addField( + new MyRadioFormField( + 'tags', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Tags', + 'explanation' => 'A set of keywords to describe your show generally, seperated with commas.', + ] + ) + )->addField( + new MyRadioFormField('grp-basics_close', MyRadioFormField::TYPE_SECTION_CLOSE) + )->addField( + new MyRadioFormField('grp-credits', MyRadioFormField::TYPE_SECTION, ['label' => 'Who\'s On My Show']) + )->addField( + new MyRadioFormField( + 'credits', + MyRadioFormField::TYPE_TABULARSET, + [ + 'label' => 'Credits', + 'options' => [ + new MyRadioFormField( + 'memberid', + MyRadioFormField::TYPE_MEMBER, + [ + 'explanation' => '', + 'label' => 'Member Name', + ] + ), + new MyRadioFormField( + 'credittype', + MyRadioFormField::TYPE_SELECT, + [ + 'options' => array_merge( + [['text' => 'Please select...', 'disabled' => true]], + MyRadio_Scheduler::getCreditTypes() + ), + 'explanation' => '', + 'label' => 'Role', + ] + ), + ], + ] + ) + )->addField( + new MyRadioFormField('grp-credits_close', MyRadioFormField::TYPE_SECTION_CLOSE) + )->addField( + new MyRadioFormField( + 'mixclouder', + MyRadioFormField::TYPE_CHECK, + [ + 'explanation' => 'If ticked, your shows will automatically be uploaded to mixcloud', + 'label' => 'Enable Mixcloud', + 'options' => ['checked' => true], + 'required' => false, + ] + ) + )->addField( + new MyRadioFormField( + 'podcast_explicit', + MyRadioFormField::TYPE_CHECK, + [ + 'required' => false, + 'label' => 'Podcast contains explicit content', + 'explanation' => 'Check this box if, and only if, this show is a podcast ' + . 'and it contains explicit content. ' + . 'Remember: explicit content is NEVER acceptable to broadcast!', + 'options' => ['checked' => false], + ] + ) + ); + } + + public function getEditForm() + { + return self::getForm() + ->setSubtitle('Edit Show') + ->editMode( + $this->getID(), + [ + 'title' => $this->getMeta('title'), + 'description' => $this->getMeta('description'), + 'genres' => $this->getGenre(), + 'subtype' => $this->getSubtype()->getClass(), + 'tags' => is_null($this->getMeta('tag')) ? null : implode(', ', $this->getMeta('tag')), + 'credits.memberid' => array_map( + function ($ar) { + return $ar['User']; + }, + $this->getCredits() + ), + 'credits.credittype' => array_map( + function ($ar) { + return $ar['type']; + }, + $this->getCredits() + ), + 'mixclouder' => ($this->getMeta('upload_state') === 'Requested'), + 'podcast_explicit' => $this->isPodcastExplicit() + ] + ); + } + + public static function getPhotoForm() + { + return ( + new MyRadioForm( + 'sched_showphoto', + 'Scheduler', + 'showPhoto', + [ + 'debug' => true, + 'title' => 'Update Show Photo', + ] + ) + )->addField( + new MyRadioFormField( + 'show_id', + MyRadioFormField::TYPE_HIDDEN + ) + )->addField( + new MyRadioFormField( + 'image_file', + MyRadioFormField::TYPE_FILE, + ['label' => 'Photo'] + ) + ); + } + + public function getNumberOfSeasons() + { + return sizeof($this->season_ids); + } + + public function getAllSeasons() + { + $seasons = []; + foreach ($this->season_ids as $season_id) { + $seasons[] = MyRadio_Season::getInstance($season_id); + } + + return $seasons; + } + + /** + * A simplified version of getAllTimeslots in MyRadio_Season. + * This gets all the timeslots that were part of a show, but only returns a few values. + * Note that start_time is a (PSQL) timestamp, not an epoch. + * + * @return Array timeslots with season_id, timeslot_id, start_time and duration + */ + public function getAllTimeslots() + { + $sql = + 'SELECT + show_season.show_season_id AS season_id, + show_season_timeslot.show_season_timeslot_id AS timeslot_id, + show_season_timeslot.start_time, + show_season_timeslot.duration + FROM schedule.show + INNER JOIN + schedule.show_season ON show.show_id = show_season.show_id + INNER JOIN + schedule.show_season_timeslot ON show_season.show_season_id = show_season_timeslot.show_season_id + WHERE show.show_id = $1 + ORDER BY timeslot_id ASC'; + $result = self::$db->fetchAll($sql, [$this->show_id]); + $timeslots = []; + foreach ($result as $row) { + $timeslots[] = [ + 'season_id' => (int) $row['season_id'], + 'timeslot_id' => (int) $row['timeslot_id'], + 'start_time' => $row['start_time'], + 'duration' => $row['duration'], + ]; + } + return $timeslots; + } + + /** + * Internally associates a Season with this Show. + * Does not persist in database. Used for updating the cache. + * + * @param int $id + */ + public function addSeason($id) + { + $this->season_ids[] = $id; + $this->updateCacheObject(); + } + + public function getID() + { + return $this->show_id; + } + + /** + * Get the microsite URI. + * + * @return string + */ + public function getWebpage() + { + return '/schedule/shows/'.$this->getID(); + } + + /** + * Get the web url for the Show Photo. + * + * @return string + */ + public function getShowPhoto() + { + return $this->photo_url; + } + + /** + * Returns the ID for the type of Show. + * + * @return int + */ + public function getShowType() + { + return $this->show_type; + } + + /** + * Return the primary Genre. Shows generally only have one anyway. + */ + public function getGenre() + { + return isset($this->genres[0]) ? $this->genres[0] : null; + } + + /** + * Gets the subtype for this show. + * + * Note that subtypes can be overridden per-season, so you should probably use MyRadio_Season->getSubtype(). + * @return MyRadio_ShowSubtype + */ + public function getSubtype() + { + return MyRadio_ShowSubtype::getInstance($this->subtype_id); + } + + /** + * If this show is a podcast, does it contain explicit content? + * @return bool + */ + public function isPodcastExplicit() + { + return $this->podcast_explicit; + } + + /** + * Sets this show's subtype. + * + * @todo support effectiveFrom and effectiveTo + * @param $subtypeId + */ + public function setSubtype($subtypeId) + { + self::$db->query('UPDATE schedule.show_season_subtype SET show_subtype_id = $1 WHERE show_id = $1', [ + $subtypeId, $this->show_id + ]); + } + + /** + * Sets this show's subtype by the subtype name. + * @param $subtypeName + */ + public function setSubtypeByName($subtypeName) + { + self::$db->query( + 'UPDATE schedule.show_season_subtype + SET show_subtype_id = subtype.show_subtype_id + FROM (SELECT show_subtype_id FROM schedule.show_subtypes WHERE show_subtypes.class = $2) AS subtype + WHERE show_id = $1', + [$this->show_id, $subtypeName] + ); + } + + /** + * Sets show photo + * + * @param string $tmp_path + */ + public function setShowPhoto($tmp_path) + { + // The getimagesize() below can fail, so we want this in a transaction + self::$db->query('BEGIN'); + $result = self::$db->fetchColumn( + 'INSERT INTO schedule.show_image_metadata (memberid, approvedid, metadata_key_id, metadata_value, show_id) + VALUES ($1, $1, $2, $3, $4) RETURNING show_image_metadata_id', + [ + MyRadio_User::getCurrentOrSystemUser()->getID(), + self::getMetadataKey('player_image'), + 'tmp', + $this->getID() + ] + )[0]; + + $filetype = explode('/', getimagesize($tmp_path)['mime'])[1]; + $suffix = 'image_meta/ShowImageMetadata/'.$result.'.'.$filetype; + $path = Config::$public_media_path.'/'.$suffix; + move_uploaded_file($tmp_path, $path); + chmod($path, 0644); + + self::$db->query( + 'UPDATE schedule.show_image_metadata SET effective_to=NOW() + WHERE metadata_key_id=$1 + AND show_id=$2 + AND effective_from IS NOT NULL', + [self::getMetadataKey('player_image'), $this->getID()] + ); + + self::$db->query( + 'UPDATE schedule.show_image_metadata SET effective_from=NOW(), metadata_value=$1 + WHERE show_image_metadata_id=$2', + [$suffix, $result] + ); + + $this->photo_url = Config::$public_media_uri.'/'.$suffix; + self::$db->query('COMMIT'); + $this->updateCacheObject(); + } + + /** + * Sets a metadata key to the specified value. + * + * If any value is the same as an existing one, no action will be taken. + * If the given key has is_multiple, then the value will be added as a new, additional key. + * If the key does not have is_multiple, then any existing values will have effective_to + * set to the effective_from of this value, effectively replacing the existing value. + * This will *not* unset is_multiple values that are not in the new set. + * + * @param string $string_key The metadata key + * @param mixed $value The metadata value. If key is_multiple and value is an array, will create instance + * for value in the array. + * @param int $effective_from UTC Time the metavalue is effective from. Default now. + * @param int $effective_to UTC Time the metadata value is effective to. Default NULL (does not expire). + */ + public function setMeta($string_key, $value, $effective_from = null, $effective_to = null) + { + $r = parent::setMetaBase( + $string_key, + $value, + $effective_from, + $effective_to, + 'schedule.show_metadata', + 'show_id' + ); + $this->updateCacheObject(); + + return $r; + } + + /** + * Sets the Genre, if it hasn't changed. + * + * @param int $genreid + */ + public function setGenre($genreid) + { + if (empty($genreid)) { + throw new MyRadioException('Genre cannot be empty!', 400); + } + if ($genreid != $this->getGenre()) { + self::$db->query( + 'UPDATE schedule.show_genre SET effective_to=NOW() WHERE show_id=$1', + [$this->getID()] + ); + self::$db->query( + 'INSERT INTO schedule.show_genre (show_id, genre_id, effective_from, memberid, approvedid) + VALUES ($1, $2, NOW(), $3, $3)', + [$this->getID(), $genreid, MyRadio_User::getInstance()->getID()] + ); + $this->genres = [$genreid]; + $this->updateCacheObject(); + } + } + + /** + * Sets this show's "Podcast explicit" status + * @param $value bool + */ + public function setPodcastExplicit($value) + { + self::$db->query( + 'UPDATE schedule.show SET podcast_explicit = $2::boolean WHERE show_id = $1', + [$this->getID(), $value ? 1 : 0] + ); + $this->updateCacheObject(); + } + + /** + * Updates the list of Credits. + * + * Existing credits are kept active, ones that are not in the new list are set to effective_to now, + * and ones that are in the new list but not exist are created with effective_from now. + * + * @param MyRadio_User[] $users An array of Users associated. + * @param int[] $credittypes The relevant credittypeid for each User. + */ + public function setCredits($users, $credittypes, $table = null, $pkey = null) + { + $r = parent::setCredits($users, $credittypes, 'schedule.show_credit', 'show_id'); + $this->updateCacheObject(); + + return $r; + } + + /** + * Gets all podcasts linked to this show. + * + * @param bool $include_suspended Whether to include suspended podcasts in the result + * + * @return MyRadio_Podcast[] + */ + public function getAllPodcasts($include_suspended = false) + { + $andSuspend = ""; + + // This makes me sad, but it passes "false" from API, + // which is true because it isn't "". ¯\_(ツ)_/¯ + if (!$include_suspended || $include_suspended == "false") { + $andSuspend = " AND suspended = false"; + } + + $query = "SELECT podcast_id FROM schedule.show_podcast_link + INNER JOIN uryplayer.podcast USING (podcast_id) + WHERE show_id = $1" + . $andSuspend + . " ORDER BY submitted DESC"; + + $ids = self::$db->fetchColumn( + $query, + [$this->getID()] + ); + + $podcasts = []; + foreach ($ids as $id) { + $podcasts[] = MyRadio_Podcast::getInstance($id); + } + + return $podcasts; + } + + /** + * Returns all Shows of the given type. Caches for 1h. + * + * @return MyRadio_Show[] + */ + public static function getAllShows($show_type_id = 1, $current_term_only = false) + { + $key = 'MyRadio_Show_AllShowsFetcher_last_'.$show_type_id.'_'.(int) $current_term_only; + + $keys = self::$cache->get($key); + + if ($keys) { + $results = self::$cache->getAll($keys); + $shows = array_values($results); // Cached results are in different format. + } else { + $sql = self::BASE_SHOW_SQL.' WHERE show_type_id=$1'; + $params = [$show_type_id]; + if ($current_term_only) { + $sql .= ' AND EXISTS ( + SELECT * FROM schedule.show_season + WHERE schedule.show_season.show_id=schedule.show.show_id + AND schedule.show_season.termid=$2 + )'; + $params[] = MyRadio_Term::getActiveApplicationTerm()->getID(); + } + + $result = self::$db->fetchAll($sql, $params); + + $shows = []; + $show_keys = []; + foreach ($result as $row) { + $show = new self($row); + $show->updateCacheObject(); + $shows[] = $show; + $show_keys[] = self::getCacheKey($show->getID()); + } + + self::$cache->set($key, $show_keys); + } + + return $shows; + } + + /** + * Find the most messaged shows. + * + * @param int $date If specified, only messages for timeslots since $date are counted. + * + * @return array An array of 30 Shows that have been put through toDataSource, with the addition of a msg_count key, + * referring to the number of messages sent to that show. + */ + public static function getMostMessaged($date = 0) + { + $result = self::$db->fetchAll( + 'SELECT show.show_id, COUNT(*) as msg_count FROM sis2.messages + LEFT JOIN schedule.show_season_timeslot ON messages.timeslotid=show_season_timeslot.show_season_timeslot_id + LEFT JOIN schedule.show_season ON show_season_timeslot.show_season_id=show_season.show_season_id + LEFT JOIN schedule.show ON show_season.show_id=show.show_id + WHERE show_season_timeslot.start_time > $1 GROUP BY show.show_id ORDER BY msg_count DESC LIMIT 30', + [CoreUtils::getTimestamp($date)] + ); + + $top = []; + foreach ($result as $r) { + $show = self::getInstance($r['show_id'])->toDataSource(); + $show['msg_count'] = intval($r['msg_count']); + $top[] = $show; + } + + return $top; + } + + /** + * Returns the current Show on air, if there is one. + * + * @param int $time Optional integer timestamp + * + * @return MyRadio_Show|null + */ + public static function getCurrentShow($time = null) + { + $timeslot = MyRadio_Timeslot::getCurrentTimeslot($time); + if (empty($timeslot)) { + return; + } else { + return $timeslot->getSeason()->getShow(); + } + } + + /** + * Find the most listened Shows. + * + * @param int $date If specified, only messages for timeslots since $date are counted. + * + * @return array An array of 30 Timeslots that have been put through toDataSource, with the addition of a msg_count + * key, referring to the number of messages sent to that show. + */ + public static function getMostListened($date = 0) + { + $key = 'stats_show_mostlistened'; + if (($top = self::$cache->get($key)) !== false) { + return $top; + } + + $result = self::$db->fetchAll( + 'SELECT show_id, SUM(listeners) AS listeners_sum FROM ( + SELECT show_season_id, ( + SELECT COUNT(*) FROM strm_log + WHERE (starttime < show_season_timeslot.start_time AND endtime >= show_season_timeslot.start_time) + OR ( + starttime >= show_season_timeslot.start_time + AND starttime < show_season_timeslot.start_time + show_season_timeslot.duration + ) + ) AS listeners + FROM schedule.show_season_timeslot + WHERE start_time > $1 + ) AS t1 + LEFT JOIN schedule.show_season ON t1.show_season_id = show_season. show_season_id + GROUP BY show_id ORDER BY listeners_sum DESC LIMIT 30', + [CoreUtils::getTimestamp($date)] + ); + + $top = []; + foreach ($result as $r) { + $show = self::getInstance($r['show_id'])->toDataSource(); + $show['listeners'] = intval($r['listeners_sum']); + $top[] = $show; + } + + self::$cache->set($key, $top, 86400); + + return $top; + } + + /** + * Searches searchable *text* metadata for the specified value. Does not work for image metadata. + * if $q is set, then $path_query must be set to "search". This allows the query to be given in path or in parameters. + * + * @todo effective_from/to not yet implemented + * + * @param string $path_query The query value encoded in the path (DEPRECATED). + * @param string $q The query value as a query string. to use this, $path_query must be set to "search" + * @param array $string_keys The metadata keys to search + * @param int $effective_from UTC Time to search from. + * @param int $effective_to UTC Time to search to. + * + * @return array The shows that match the search terms + */ + public static function searchMeta($path_query, $q="", $string_keys = null, $effective_from = null, $effective_to = null) + { + if($path_query != "search" && $q != "") { + throw new MyRadioException( + "the path_query must be set to 'search' if q is set in the query string", + 400 + ); + } + if ($path_query == "search" && $q != ""){ + $query = $q; + } else { + $query = $path_query; + } + if (is_null($string_keys)) { + $string_keys = ['title', 'description', 'tag']; + } + + $r = parent::searchMetaBase( + $query, + $string_keys, + $effective_from, + $effective_to, + 'schedule.show_metadata', + 'show_id' + ); + return self::resultSetToObjArray($r); + } + + /** + * Generate a podcast RSS feed for this show. + * @return string + */ + public function getPodcastRss() + { + $website = preg_replace( + '(/$)', + '', + 'https:' .Config::$website_url + ); + $media_url = preg_replace( + '(/$)', + '', + $website . '/' . Config::$public_media_uri + ); + + $writer = new \XMLWriter(); + $writer->openMemory(); + $writer->startDocument('1.0', 'UTF-8'); + $writer->setIndent(true); + + $writer->startElement('rss'); + $writer->writeAttribute('xmlns:itunes', 'http://www.itunes.com/dtds/podcast-1.0.dtd'); + $writer->writeAttribute('xmlns:spotify', 'https://www.spotify.com/ns/rss'); + $writer->writeAttribute('version', '2.0'); + + $writer->startElement('channel'); + + $writer->writeElement("title", $this->getMeta("title")); + $writer->writeElement("link", $website . $this->getWebpage()); + + $writer->startElement("description"); + $writer->writeCdata( + str_replace( + ' ', + ' ', + html_entity_decode( + strip_tags($this->getMeta("description"), ['a', 'p']), + ENT_QUOTES | ENT_XML1, + "UTF-8" + ) + ) + ); + $writer->endElement(); + + $writer->writeElement("language", "en"); // TODO + + $writer->writeElementNs("itunes", "author", null, Config::$long_name); + + $writer->startElementNs("itunes", "category", null); + $writer->writeAttribute("text", "Society & Culture"); // TODO + $writer->endElement(); + + $writer->startElementNs("itunes", "image", null); + $writer->writeAttribute( + "href", + $website . $this->getShowPhoto() + ); + $writer->endElement(); + + $writer->startElementNs("itunes", "owner", null); + + $writer->writeElementNs("itunes", "name", null, Config::$long_name); + $writer->writeElementNs("itunes", "email", null, "pc@" . Config::$email_domain); + + $writer->endElement(); + + $writer->writeElementNs( + "itunes", + "explicit", + null, + $this->isPodcastExplicit() ? "true" : "false" + ); + + $writer->writeElement("copyright", "Copyright " . date("Y") . " " . Config::$long_name . ". All rights reserved."); + + foreach ($this->getAllPodcasts() as $episode) { + if (!($episode->isPublished())) { + continue; + } + + $fileSize = filesize($episode->getWebFile()); + + if (empty($fileSize)) { + // that file is in the twilight zone + continue; + } + + $writer->startElement("item"); + + $writer->writeElement("guid", $episode->getGUID()); + $writer->writeElement("title", $episode->getMeta("title")); + + $writer->startElement("description"); + $writer->writeCdata( + $episode->getMeta("description") + ); + $writer->endElement(); + + $writer->writeElement("pubDate", CoreUtils::getRfc2822Timestamp($episode->getSubmitted())); + + if (!empty($episode->getCover())) { + $writer->startElementNs("itunes", "image", null); + $writer->writeAttribute( + "href", + $media_url.$episode->getCover() + ); + $writer->endElement(); + } + + $getID3 = new \getID3(); + $fileInfo = $getID3->analyze($episode->getWebFile()); + + if (isset($fileInfo["playtime_string"])) { + $writer->writeElementNs("itunes", "duration", null, $fileInfo['playtime_string']); + } + + $writer->startElement("enclosure"); + $writer->writeAttribute("url", $website . $episode->getURI()); + $writer->writeAttribute("type", "audio/mpeg"); // TODO + $writer->writeAttribute("length", $fileSize); + $writer->endElement(); + + $writer->endElement(); + } + + $writer->endElement(); + $writer->endElement(); + $writer->endDocument(); + + return $writer->flush(); + } + + + public function toDataSource($mixins = []) + { + $data = [ + 'show_id' => $this->getID(), + 'title' => $this->getMeta('title'), + 'credits_string' => implode(', ', $this->getCreditsNames(false)), + 'credits' => array_map( + function ($x) { + $x['User'] = $x['User']->toDataSource(); + $x['type_name'] = $this->getCreditName($x['type']); + + return $x; + }, + $this->getCredits() + ), + 'description' => $this->getMeta('description'), + 'show_type_id' => $this->show_type, + 'subtype' => array_merge($this->getSubtype()->toDataSource($mixins), [ + // I don't like using html here, but if I use text it adds an unnecessary and ugly
    tag + 'display' => 'html', + 'html' => $this->getSubtype()->getName() + ]), + 'seasons' => [ + 'display' => 'text', + 'value' => $this->getNumberOfSeasons(), + 'title' => 'Click to see Seasons for this show', + 'url' => URLUtils::makeURL('Scheduler', 'listSeasons', ['showid' => $this->getID()]), + ], + 'editlink' => [ + 'display' => 'icon', + 'value' => 'pencil', + 'title' => 'Edit Show', + 'url' => URLUtils::makeURL('Scheduler', 'editShow', ['showid' => $this->getID()]), + ], + 'applylink' => [ + 'display' => 'icon', + 'value' => 'calendar', + 'title' => 'Apply for a new Season', + 'url' => URLUtils::makeURL('Scheduler', 'editSeason', ['showid' => $this->getID()]), + ], + 'uploadlink' => [ + 'display' => 'icon', + 'value' => 'upload', + 'title' => 'upload show art', + 'url' => URLUtils::makeURL('Scheduler', 'showPhoto', ['show_id' => $this->getID()]), + ], + 'micrositelink' => [ + 'display' => 'icon', + 'value' => 'link', + 'title' => 'View Show Microsite', + 'url' => $this->getWebpage(), + ], + 'photo' => $this->getShowPhoto(), + ]; + + return $data; + } } diff --git a/src/Classes/ServiceAPI/MyRadio_ShowSubtype.php b/src/Classes/ServiceAPI/MyRadio_ShowSubtype.php new file mode 100644 index 000000000..f6e94ec7e --- /dev/null +++ b/src/Classes/ServiceAPI/MyRadio_ShowSubtype.php @@ -0,0 +1,136 @@ +show_subtype_id = $data['show_subtype_id']; + $this->name = $data['name']; + $this->class = $data['class']; + $this->description = $data['description']; + } + + public function getID() + { + return $this->show_subtype_id; + } + + + /** + * Get the name of this subtype. + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Get the CSS class of this subtype. + * @return string + */ + public function getClass() + { + return $this->class; + } + + /** + * Get the description of the subtype + * @return string + */ + public function getDescription() + { + return $this->description; + } + + public function toDataSource($mixins = []) + { + return [ + 'id' => $this->getID(), + 'name' => $this->getName(), + 'class' => $this->getClass(), + 'description' => $this->getDescription() + ]; + } + + /** + * Gets all subtypes. + * + * @return MyRadio_ShowSubtype[] + */ + public static function getAll() + { + $sql = 'SELECT show_subtype_id, name, class, description FROM schedule.show_subtypes'; + $rows = self::$db->fetchAll($sql); + + $subtypes = []; + foreach ($rows as $row) { + $subtypes[] = new self($row); + } + + return CoreUtils::setToDataSource($subtypes); + } + + /** + * Get all subtypes in a format suitable for a MyRadioFormField select field. + * @return array + */ + public static function getOptions() + { + return self::$db->fetchAll( + 'SELECT class AS value, name AS text FROM schedule.show_subtypes ORDER BY show_subtype_id ASC' + ); + } + + protected static function factory($itemid) + { + $sql = 'SELECT show_subtype_id, name, class, description FROM schedule.show_subtypes + WHERE show_subtype_id = $1 LIMIT 1'; + $result = self::$db->fetchOne($sql, [$itemid]); + + if (empty($result)) { + throw new MyRadioException('That subtype does not exist.', 404); + } + + return new self($result); + } +} diff --git a/src/Classes/ServiceAPI/MyRadio_Swagger.php b/src/Classes/ServiceAPI/MyRadio_Swagger.php index aed55aa65..a2453ce11 100644 --- a/src/Classes/ServiceAPI/MyRadio_Swagger.php +++ b/src/Classes/ServiceAPI/MyRadio_Swagger.php @@ -1,45 +1,57 @@ - * @package MyRadio_API - * @uses \Database - * + * The Swagger class is an Implementation of https://developers.helloreverb.com/swagger/. + * + * @uses \Database + * * @todo Detect Response Types * @todo Parse docblocks to get variable information */ -class MyRadio_Swagger { - - /** THIS HALF DEALS WITH RESOURCES LISTING * */ - public static function resources() { +class MyRadio_Swagger +{ + /** + * THIS HALF DEALS WITH RESOURCES LISTING *. + */ + public static function resources() + { $data = [ 'apiVersion' => 0.1, 'swaggerVersion' => 1.2, 'basePath' => Config::$api_url, 'authorizations' => ['apiKey' => ['type' => 'api_key', 'passAs' => 'query']], - 'apis' => [] + 'apis' => [], ]; - foreach (self::getApiClasses() as $api => $myury) { - if ($myury == __CLASS__) { + foreach (self::getApiClasses() as $api => $myradio_class) { + if ($myradio_class == __CLASS__) { continue; } - $class = new ReflectionClass($myury); - $data['apis'][] = ['path' => '/resources/' . $api, 'description' => $class->getDocComment()]; + $class = new ReflectionClass($myradio_class); + $meta = self::getClassDoc($class); + $data['apis'][] = ['path' => '/resources/'.$api, 'description' => $meta['short_desc']]; } return $data; } - public static function getApiClasses() { - $data = Database::getInstance()->fetch_all('SELECT class_name, api_name FROM myury.api_class_map ORDER BY api_name'); + public static function getApiClasses() + { + $data = Database::getInstance()->fetchAll( + 'SELECT class_name, api_name FROM myury.api_class_map ORDER BY api_name' + ); $result = []; foreach ($data as $row) { @@ -49,186 +61,267 @@ public static function getApiClasses() { return $result; } - /** THIS HALF DEALS WITH API Declarations * */ - private $class; + /** + * THIS HALF DEALS WITH API Declarations *. + */ + protected $class; - public function __construct($class) { + public function __construct($class) + { $this->class = $class; } - public function toDataSource() { - $blocked_methods = ['getInstance', + protected static function getParamType($param, $meta) + { + $type = empty($meta['params'][$param->getName()]['type']) + ? 'integer' : $meta['params'][$param->getName()]['type']; + switch ($type) { + case 'int': + $type = 'integer'; + break; + case 'float': + case 'double': + $type = 'number'; + break; + case 'char': + $type = 'string'; + break; + case 'bool': + $type = 'boolean'; + break; + } + + return $type; + } + + protected static function getParamDescription($param, $meta) + { + return empty($meta['params'][$param->getName()]['description']) ? + '' : $meta['params'][$param->getName()]['description']; + } + + public function toDataSource($mixins = []) + { + $blocked_methods = [ + 'getInstance', 'wakeup', '__wakeup', 'removeInstance', '__toString', - 'setToDataSource', - '__construct']; + '__construct', + ]; $data = [ - 'apiVersion' => 0.1, 'swaggerVersion' => 1.2, - 'basePath' => Config::$api_url . '/' . $this->class, + 'apiVersion' => 0.2, + 'basePath' => Config::$api_url.'/'.$this->class, 'apis' => [], - 'models' => [] + 'models' => [], ]; - $ref = new ReflectionClass($this->getApiClasses()[$this->class]); + $refClass = new ReflectionClass($this->getApiClasses()[$this->class]); $constructor = new ReflectionMethod($this->getApiClasses()[$this->class], '__construct'); - foreach ($ref->getMethods() as $method) { + foreach ($refClass->getMethods() as $method) { if (!$method->isPublic() or in_array($method->getName(), $blocked_methods)) { continue; } - $meta = $this->getMethodDoc($method); - /** + $meta = self::getMethodDoc($method); + /* * Add the custom @api docblock option * @api may be GET, POST... */ - $comment = preg_replace('/^.*\@api ([A-Z]+)?.*$/s', '$1', $method->getDocComment(), 1, $count); + $comment = preg_replace('/^.*\@api\s+([A-Z]+)?.*$/s', '$1', $method->getDocComment(), 1, $count); if ($count === 1) { $meta['api'] = $comment; } else { - $meta['api'] = (substr($method->getName(), 0, 3) === 'set' or $method->getName() === 'create') ? 'POST' : 'GET'; + $meta['api'] = (substr($method->getName(), 0, 3) === 'set' || $method->getName() === 'create') ? + 'POST' : 'GET'; } //Build the API URL $path = '/'; - if (!$method->isStatic() && - !($constructor->isPublic() && $constructor->getParameters() == null)) { + if (!$method->isStatic() + && !($constructor->isPublic() && $constructor->getParameters() == null) + ) { $path .= '{id}/'; } - + $params = []; if ($method->getName() !== 'toDataSource') { - $path .= $method->getName() . '/'; + $path .= $method->getName().'/'; + } else { //toDataSource has a full option $params[] = [ - "paramType" => "query", - "name" => 'full', - "description" => "Some objects can optionally return a small or large response. By default, a full response is on, although it is intended for this to change.", - "dataType" => "boolean", - "required" => false, - "allowMultiple" => false, - "defaultValue" => true + 'paramType' => 'query', + 'name' => 'mixins', + 'description' => 'Some objects can optionally extra data as strings to the api call', + 'type' => 'Array', + 'required' => false, + 'allowMultiple' => false, + 'defaultValue' => [], ]; } //Build the parameters list //id is a parameter if the method is not static //unless the constructor is public and takes no args - if (!$method->isStatic() && - !($constructor->isPublic() && $constructor->getParameters() == null)) { + if (!$method->isStatic() + && !($constructor->isPublic() && $constructor->getParameters() == null) + ) { $params[] = [ - "paramType" => "path", - "name" => "id", - "description" => "The unique identifier of the $this->class to be acted on. An int for most Objects, but some are Strings.", - "dataType" => "int", - "required" => true, - "allowMultiple" => false + 'paramType' => 'path', + 'name' => 'id', + 'description' => "The unique identifier of the $this->class to be acted on. " + . "An int for most Objects, but some are Strings.", + 'type' => 'int', + 'required' => true, + 'allowMultiple' => false, ]; } //now do the ones for the specific method foreach ($method->getParameters() as $param) { $params[] = [ - "paramType" => "query", - "name" => $param->getName(), - "description" => (empty($meta['params'][$param->getName()]['description']) ? : $meta['params'][$param->getName()]['description']), - "dataType" => (empty($meta['params'][$param->getName()]['type']) ? 'int' : $meta['params'][$param->getName()]['type']), - "required" => !$param->isOptional(), - "allowMultiple" => false, - "defaultValue" => $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null + 'paramType' => 'query', + 'name' => $param->getName(), + 'description' => self::getParamDescription($param, $meta), + 'type' => self::getParamType($param, $meta), + 'required' => !$param->isOptional(), + 'allowMultiple' => false, + 'defaultValue' => $param->isDefaultValueAvailable() ? $param->getDefaultValue() : null, ]; } //cool, now add the method in $data['apis'][] = [ - "path" => $path, - "description" => $method->getDocComment(), - "operations" => [ + 'path' => $path, + 'description' => $meta['short_desc'], + 'operations' => [ [ - "httpMethod" => $meta['api'], - "nickname" => $method->getName(), - "responseClass" => $meta['return_type'], - "parameters" => $params, - "summary" => $meta['short_desc'], - "notes" => $meta['long_desc'] - ] - ] + 'method' => $meta['api'], + 'nickname' => $method->getName(), + '$ref' => $meta['return_type'], + 'parameters' => $params, + 'summary' => $meta['short_desc'], + 'notes' => $meta['long_desc'], + ], + ], ]; } return $data; } - private function getMethodDoc(ReflectionMethod $method) { - $doc = $method->getDocComment(); - - $lines = explode("\n", trim(preg_replace('/(\/\*\*)|(\n\s+\*\/?\s?)/', "\n", $doc), " \n")); + public static function parseDoc($doc) + { + $raw = explode( + "\n", + trim(preg_replace('/(\/\*\*)|(\n\s+\*\/?[^\S\r\n]?)/', "\n", $doc->getDocComment()), " \n") + ); - //Parse for short description. This is up to the first blank line. - $i = 0; - $short_desc = ''; - while (isset($lines[$i]) && !empty($lines[$i]) && substr($lines[$i], 0, 1) !== '@') { - $short_desc .= $lines[$i] . ' '; - $i++; + $lines = ['']; + $keys = []; + foreach ($raw as $line) { + if (empty($raw)) { + $lines[] = ''; + } elseif (substr($line, 0, 1) === '@') { + $key = preg_replace('/^\@([a-zA-Z]+)(.*)$/', '$1', $line); + $keys[] = ['type' => $key, 'data' => trim(preg_replace('/^\@([a-zA-Z]+) (.*)$/', '$2', $line))]; + } else { + $lines[sizeof($lines) - 1] .= $line.' '; + } } + return ['lines' => $lines, 'keys' => $keys]; + } + + protected static function getClassDoc(ReflectionClass $class) + { + $doc = self::parseDoc($class); + + $short_desc = array_shift($doc['lines']); + //Parse for long description. This is until the first @ - $long_desc = ''; - while (isset($lines[$i]) && substr($lines[$i], 0, 1) !== '@') { - $long_desc .= $lines[$i] . ' '; - $i++; - } - - //We append the auth requirements to the long description - $requirements = MyRadio_APIKey::getCallRequirements( - $this->getApiClasses()[$this->class], $method->getName()); - if ($requirements === null) { - $long_desc .= '
    This API Call requires a Full API Access Key.'; - } elseif (empty($requirements)) { - $long_desc .= '
    Any API Key can Call this method.'; - } else { - $long_desc .= '
    The following permissions enable access to this method:'; - foreach ($requirements as $typeid) { - $long_desc .= '
    - ' . CoreUtils::getAuthDescription($typeid); - } - } + $long_desc = implode('
    ', $doc['lines']); //Now parse for docblock things $params = []; $return_type = 'Set'; - while (isset($lines[$i])) { - //Skip ones that are out of place. - if (substr($lines[$i], 0, 1) !== '@') { - $i++; - continue; - } - $key = preg_replace('/^\@([a-zA-Z]+)(.*)$/', '$1', $lines[$i]); - if (empty($key)) - continue; - switch ($key) { + foreach ($doc['keys'] as $key) { + switch ($key['type']) { //Deal with $params case 'param': - /** + /* * info[0] should be "@param" * info[1] should be data type * info[2] should be parameter name * info[3] should be the description */ - $info = explode(' ', $lines[$i], 4); + $info = explode(' ', $key['data'][0], 4); $arg = str_replace('$', '', $info[2]); //Strip the $ from variable name - $params[$arg] = ['type' => $info[1], 'description' => empty($info[3]) ? : $info[3]]; - //For any following lines, if they don't start with @, assume it's a continuation of the description - $i++; - while (isset($lines[$i]) && substr($lines[$i], 0, 1) !== '@') { - if (empty($lines[$i])) - $params[$arg]['description'] .= '
    '; - $params[$arg]['description'] .= ' ' . $lines[$i]; - $i++; + $params[$arg] = ['type' => $info[1], 'description' => empty($info[3]) ?: $info[3]]; + break; + } + } + + return [ + 'short_desc' => trim($short_desc), + 'long_desc' => trim($long_desc), + 'params' => $params, + 'return_type' => $return_type, + ]; + } + + protected static function getMethodDoc(ReflectionMethod $method) + { + $doc = self::parseDoc($method); + + $short_desc = array_shift($doc['lines']); + + //Parse for long description. This is until the first @ + $long_desc = implode('
    ', $doc['lines']); + + //Now parse for docblock things + $params = []; + $mixins = []; + $return_type = 'Set'; + $deprecated = false; + $ignore = false; + $method = 'auto'; + foreach ($doc['keys'] as $key) { + switch ($key['type']) { + //Deal with $params + case 'param': + /* + * info[0] should be data type + * info[1] should be parameter name + * info[2] should be the description + */ + $info = preg_split('/\s+/', $key['data'], 3); + if (sizeof($info) > 1) { + $arg = str_replace('$', '', $info[1]); //Strip the $ from variable name + $params[$arg] = [ + 'type' => $info[0], + 'description' => $info[2] ?? '' + ]; + } + break; + case 'mixin': + /* + * info[0] should be the mixin name + * info[1] should be a description of what the mixin does + */ + foreach ($key['data'] as $value) { + $info = explode(' ', $value, 2); + $mixins[$info[0]] = $info[1]; } break; - default: - $i++; + case 'deprecated': + $deprecated = true; + break; + case 'swagger': + if ($key['data'][0] === 'ignore') { + $ignore = true; + } break; } } @@ -237,8 +330,142 @@ private function getMethodDoc(ReflectionMethod $method) { 'short_desc' => trim($short_desc), 'long_desc' => trim($long_desc), 'params' => $params, - 'return_type' => $return_type + 'mixins' => $mixins, + 'return_type' => $return_type, + 'deprecated' => $deprecated, + 'ignore' => $ignore ]; } -} \ No newline at end of file + /** + * Return the methods this endpoint allows. + * + * Specify these with one or more @api decorators. + * Defaults to GET only. + * Defaults to POST if the method begins with 'set' (e.g. setIntro) + */ + public static function getOptionsAllow(ReflectionMethod $method) + { + $info = self::parseDoc($method); + foreach ($info['keys'] as $key) { + if ($key['type'] === 'api') { + return array_merge(['OPTIONS'], $key['data']); + } + } + + if (strncmp($method->getName(), 'set', strlen('set')) === 0) { + return ['OPTIONS', 'POST']; + } else { + return ['OPTIONS', 'GET']; + } + } + + /** + * Get the permissions that are needed to access this API Call. + * + * If the return values is null, this method cannot be called. + * If the return value is an empty array, no permissions are needed. + * + * @param string $class The class the method belongs to (actual, not API Alias) + * @param string $method The method being called + * + * @return int[] + */ + public static function getCallRequirements($class, $method) + { + $result = Database::getInstance()->fetchColumn( + 'SELECT typeid FROM myury.api_method_auth WHERE class_name=$1 AND + (method_name=$2 OR method_name IS NULL)', + [$class, $method] + ); + + if (empty($result)) { + return; + } + + foreach ($result as $row) { + if (empty($row)) { + return []; //There's a global auth option + } + } + + return $result; + } + + /** + * Get the permissions that are needed to access this API Call with the given mixin. + * + * If the return values is null, this method cannot be called. + * If the return value is an empty array, no permissions are needed. + * + * @param string $class The class the method belongs to (actual, not API Alias) + * @param string $mixin The mixin being called + * + * @return int[] + */ + public static function getMixinRequirements($class, $mixin) + { + $result = Database::getInstance()->fetchColumn( + 'SELECT typeid FROM myury.api_mixin_auth WHERE class_name=$1 AND + (mixin_name=$2 OR mixin_name IS NULL)', + [$class, $mixin] + ); + + if (empty($result)) { + return; + } + + foreach ($result as $row) { + if (empty($row)) { + return []; //There's a global auth option + } + } + + return $result; + } + + /** + * Identifies who's calling this. + * + * @return \MyRadio\Iface\APICaller The APICaller authorising against the request + */ + public static function getAPICaller() + { + if (isset($_REQUEST['apiKey'])) { + $_REQUEST['api_key'] = $_REQUEST['apiKey']; + } + if (empty($_REQUEST['api_key'])) { + /* + * Attempt to use user session + * By not using session handler, and resetting $_SESSION after + * We are ensuring there are no session-based side effects + */ + $api_key = self::getCurrentUserWithoutMessingWithSession(); + } else { + $api_key = MyRadio_APIKey::getInstance($_REQUEST['api_key']); + # If the API key has been revoked, it doesn't exist anymore. + if (isset($api_key)) { + $api_key = $api_key->isRevoked() ? false : $api_key; + } + } + + return $api_key; + } + + /** + * I really, really hope that the brief method name tells you what's going on here. + * + * @return MyRadio_User|null + */ + protected static function getCurrentUserWithoutMessingWithSession() + { + $dummysession = unserialize((new MyRadioSession())->read(session_id())); + if (!isset($dummysession['memberid'])) { + $user = null; + } else { + $user = MyRadio_User::getInstance($dummysession['memberid']); + } + + return $user; + } +} diff --git a/src/Classes/ServiceAPI/MyRadio_Swagger2.php b/src/Classes/ServiceAPI/MyRadio_Swagger2.php new file mode 100644 index 000000000..c55ea7ebf --- /dev/null +++ b/src/Classes/ServiceAPI/MyRadio_Swagger2.php @@ -0,0 +1,572 @@ +canCall($class, $method) && + ($method !== 'toDataSource' || $auth->canMixin($class, $mixins)); + } + + private static function getArgs($op, $method, $arg0) + { + $args = []; + + switch ($op) { + case 'get': + $args = $_GET; + break; + case 'post': + if (substr_count($_SERVER['CONTENT_TYPE'], 'application/json')) { + $args = json_decode(file_get_contents('php://input'), true); + if ($method->getNumberOfParameters() === 1) { + //Support the case where the entire body is the parameter + // This is the more likely case, but I think the other scenario is used somewhere... + $args = [$args]; + } + } else { + $args = $_POST; + } + break; + case 'put': + if (substr_count($_SERVER['CONTENT_TYPE'], 'application/json')) { + $args = json_decode(file_get_contents('php://input'), true); + } else { + parse_str(file_get_contents('php://input'), $args); + } + break; + } + + // Check mixins too + if (isset($args['mixins'])) { + $args['mixins'] = array_filter(explode(',', $args['mixins'])); + } + + $parameters = $method->getParameters(); + + if (self::isOptionInPathForMethod($method)) { + $args[$parameters[0]->getName()] = $arg0; + } + + return $args; + } + + /** + * Identify if this method should put its option in its path + * i.e. as /class/method/option + * + * @param ReflectionMethod The method + * @return bool + */ + private static function isOptionInPathForMethod($method) + { + $doc = self::getMethodDoc($method); + return self::getMethodOpType($method) === 'get' && + $method->getNumberOfRequiredParameters() === 1 && + self::getParamType($method->getParameters()[0], $doc) !== 'array'; + } + + /** + * Identify if the class/method combination given is valid. Useful for routing when paths are ambiguous. + * + * @param string $class The URI-name of the class to check + * @param string $method The URI-name of the method to check + * @return boolean + */ + public static function isValidClassMethodCombination($class, $method) + { + $classes = array_flip(self::getApis()); + if (!isset($classes[$class])) { + return false; + } + + $refClass = new self($classes[$class]); + return in_array($method, $refClass->getClassMethodsPublicNames()); + } + + /** + * Process an /api/v2 request. + * + * @param string $op The HTTP request method (GET/PUT/POST/DELETE...) + * @param string $class The URI-name of the class being acted on + * @param string $method The URI-name of the method being acted on + * @param mixed $id The ID of the item being acted on, if the method is non-static + * @param mixed $arg0 The value of the parameter after the method name, if there is one + */ + public static function handleRequest($op, $class, $method, $id = null, $arg0 = null) + { + $classes = array_flip(self::getApis()); + + if (!isset($classes[$class])) { + throw new MyRadioException("$class endpoint does not exist.", 404); + } + + $refClass = new self($classes[$class]); + $paths = $refClass->getClassInfo()['children']; + + // @todo: This could probably be refactored to be friendlier now isValidClassMethodCombination exists. + $path = '/'; + if ($id) { + $path = $path.'{id}/'; + } + + if ($method) { + $path .= $method; + } + + if ($arg0) { + // array_filter($paths, func, ARRAY_FILTER_USE_KEY) is not running func for me... + $options = []; + foreach (array_keys($paths) as $key) { + if (strpos($key, $path.'/{') === 0) { + $options[] = $key; + break; + } + } + + if (sizeof($options) > 1) { + throw new MyRadioException('Ambiguous path.', 404); + } + + $path = $options[0]; + } + + if (!isset($paths[$path])) { + throw new MyRadioException("$class has no child $method.", 404); + } + + $options = strtoupper(implode(', ', array_keys($paths[$path]))).', OPTIONS'; + if ($op === 'options') { + header('Access-Control-Allow-Methods: '.$options); // This is for CORS in browser + URLUtils::nocontent(); + } elseif (!isset($paths[$path][$op])) { + header('Allow: '.$options); // This is reference for HTTP 405 + throw new MyRadioException("$path does not have a valid $op handler.", 405); + } + + // Note: May not contain a 'mixins' key. Don't add it either, or the empty array will get passed to functions + $args = self::getArgs($op, $paths[$path][$op], $arg0); + + if ($id) { + if (method_exists($classes[$class], 'getInstance')) { + $object = $classes[$class]::getInstance($id); + } else { + $object = new $classes[$class]($id); + } + } else { + $object = null; + } + + //Cool, it's valid. Can they get at it? + $caller = self::getAPICaller(); + + if (!$caller) { + throw new MyRadioException('No valid authentication data provided.', 401); + } elseif (self::validateRequest( + $caller, + $classes[$class], + $paths[$path][$op]->getName(), + $args['mixins'] ?? [] + ) + ) { + $status = '200 OK'; + if ($paths[$path][$op]->getName() === 'create') { + $status = '201 Created'; + } + + // Don't send the API key or mixins to the function (mixins are handled later) + unset($args['api_key']); + $mixins = $args['mixins']; + unset($args['mixins']); + + $data = [ + 'status' => $status, + 'content' => invokeArgsNamed($paths[$path][$op], $object, $args), + // We need this key so it gets pinged back to dataSourceParser in v2.php + 'mixins' => $mixins ?? [] + ]; + + // If this returns a datasourceable array of objects, validate any mixins + $sample_obj = null; + if (is_array($data['content']) + && sizeof($data['content']) > 0 + && is_subclass_of(array_values($data['content'])[0], 'MyRadio::ServiceAPI::ServiceAPI') + ) { + $sample_obj = array_values($data['content'])[0]; + } elseif (is_subclass_of($data['content'], 'MyRadio::ServiceAPI::ServiceAPI')) { + $sample_obj = $data['content']; + } + + if ($sample_obj && !$caller->canMixin(get_class($sample_obj), $mixins ?? [])) { + throw new MyRadioException('Caller cannot access this method.', 403); + } + + return $data; + } else { + throw new MyRadioException('Caller cannot access this method.', 403); + } + } + + /** + * THIS HALF DEALS WITH RESOURCES LISTING. + */ + public static function resources() + { + $apis = self::getApis(); + $data = [ + 'swagger' => '2.0', + 'basePath' => Config::$api_uri.'v2', + 'host' => $_SERVER['HTTP_HOST'], + 'info' => [ + 'title' => 'MyRadio API', + 'description' => 'The MyRadio API provides vaguely RESTful access to many of the internal workings ' + . 'of your friendly local radio station.', + 'termsOfService' => 'The use of this API is permitted only for applications which have been issued an ' + . 'API key, and only then within the additional Terms of Service issued with that ' + . 'application\'s key. Any other use is strictly prohibited. The MyRadio API may be ' + . 'used for good, but not evil. The lighter 15 of the 50 grey areas are also ' + . 'permitted for all authorised applications.', + 'version' => '2.0', + ], + 'schemes' => ['https'], + 'consumes' => [], + 'produces' => ['application/json'], + 'paths' => self::getPaths($apis), + 'tags' => self::getTags($apis), + 'parameters' => [ + 'idParam' => [ + 'name' => 'id', + 'in' => 'path', + 'description' => 'The ID of the item to work with.', + 'required' => true, + 'type' => 'integer', + ], + 'dataSourceFull' => [ + 'name' => 'full', + 'in' => 'query', + 'description' => 'Deprecated. Used to return more details in object GETs.', + 'required' => false, + 'type' => 'boolean', + 'default' => false, + ], + ], + 'responses' => [ + 'invalidInput' => [ + 'description' => 'Invalid input for this operation.', + ], + ], + 'definitions' => self::getApiConfig()['specs'] + ]; + + return $data; + } + + private static function getApis() + { + return self::getApiConfig()["classes"]; + } + + private static function getApiConfig() + { + if (!self::$api_config) { + self::$api_config = json_decode(file_get_contents(__DIR__.'/../../../schema/api.json'), true); + } + return self::$api_config; + } + + private static function getParameters($method, $doc, $op, $public_name) + { + $parameters = []; + + if (!$method->isStatic()) { + $parameters[] = [ + '$ref' => '#/parameters/idParam', + ]; + } + + if ($method->name === 'toDataSource' && $method->getNumberOfParameters() === 1) { + if (!empty($doc['mixins'])) { + $description = 'A list of mixins to provide additional information in the response. Possible values:'; + foreach ($doc['mixins'] as $mixin => $desc) { + $description .= "
    $mixin: $desc"; + } + $parameters[] = [ + 'name' => 'mixins', + 'in' => 'query', + 'description' => $description, + 'required' => false, + 'type' => 'array', + 'items' => [ + 'type' => 'string', + 'format' => 'string', + ], + 'collectionFormat' => 'csv', + 'default' => [], + ]; + } else { + $parameters[] = [ + '$ref' => '#/parameters/dataSourceFull', + ]; + } + } elseif ($method->name === 'create' + && $method->getNumberOfParameters() === 1 + && $op === 'post' + && self::getApiConfig()['specs'][$public_name]) { + //This endpoint can have JSON POSTed at it + $parameters[] = [ + 'name' => $public_name, + 'in' => 'body', + 'required' => true, + 'schema' => [ + '$ref' => '#/definitions/' . $public_name + ] + ]; + } else { + $startIdx = 0; + $paramReflectors = $method->getParameters(); + + if (self::isOptionInPathForMethod($method)) { + //If only one GET is required, make it URL + $param = $method->getParameters()[0]; + $parameters[] = [ + 'name' => $param->getName(), + 'in' => 'path', + 'description' => self::getParamDescription($param, $doc), + 'required' => true, + 'type' => self::getParamType($param, $doc), + ]; + ++$startIdx; + } + + for ($i = $startIdx; $i < sizeof($paramReflectors); ++$i) { + $param = $paramReflectors[$i]; + $definition = [ + 'name' => $param->getName(), + 'in' => $op === 'get' ? 'query' : 'form', + 'description' => self::getParamDescription($param, $doc), + 'required' => !$param->isOptional(), + 'type' => self::getParamType($param, $doc) + ]; + // SwaggerUI converts a null to the string "null", which is confusing. + if ($param->isDefaultValueAvailable() && $param->getDefaultValue() !== null) { + $definition['default'] = $param->getDefaultValue(); + } + $parameters[] = $definition; + } + } + + return $parameters; + } + + private static function getPaths($apis) + { + $cache_class = Config::$cache_provider; + $cache = $cache_class::getInstance(); + + $paths = $cache->get('api_pathmap_v2'); + if (!$paths) { + $paths = []; + + foreach ($apis as $class => $public_name) { + $api = new self($class); + + foreach ($api->getClassInfo()['children'] as $method_name => $child) { + foreach ($child as $op => $reflector) { + $data = self::getMethodDoc($reflector); + + // Skip methods set to be skipped + if ($data['ignore']) { + continue; + } + + $paths['/'.$public_name.$method_name][$op] = [ + 'summary' => $data['short_desc'], + 'description' => $data['long_desc'], + 'tags' => [$public_name], + 'operationId' => $class.':'.$reflector->getName(), + 'parameters' => self::getParameters($reflector, $data, $op, $public_name), + 'responses' => [ + '400' => ['$ref' => '#/responses/invalidInput'], + ], + 'deprecated' => $data['deprecated'], + 'security' => [], + ]; + } + } + } + + $cache->set('api_pathmap_v2', $paths, 600); + } + + return $paths; + } + + private static function getTags($apis) + { + $tags = []; + + foreach ($apis as $class => $public_name) { + $api = new self($class); + + $tag = [ + 'name' => $public_name, + 'description' => $api->getClassInfo()['description'], + ]; + + $tags[] = $tag; + } + + return $tags; + } + + private static function getMethodOpType($method) + { + $name = $method->getName(); + + //Note the ordering is important - create is static! + // setUserSelectedTimeslot is here too because it's a static + // method setting a 'global' (in the session) variable + if ($name === 'testCredentials' + || $name === 'setUserSelectedTimeslot' + || CoreUtils::startsWith($name, 'create') + || CoreUtils::startsWith($name, 'add') + ) { + return 'post'; + } + + if ($name === 'toDataSource' + || CoreUtils::startsWith($name, 'get') + || CoreUtils::startsWith($name, 'is') + || $method->isStatic() + ) { + return 'get'; + } + + return 'put'; + } + + private static function getMethodPublicName($method) + { + $name = $method->getName(); + + if ($name === 'toDataSource' || $name === 'create') { + return ''; + } + + if (CoreUtils::startsWith($name, 'set') || CoreUtils::startsWith($name, 'get')) { + return strtolower(substr($name, 3)); + } + + return strtolower($name); + } + + /** + * Returns an array of ReflectionMethod objects for each method this class should have exposed. + * @return ReflectionMethod[] + */ + private function getReflectedMethods() + { + $methods = []; + + $blocked_methods = [ + 'getInstance', + 'wakeup', + '__wakeup', + 'removeInstance', + '__toString', + '__construct', + 'resultSetToObjArray', + '__destruct' + ]; + + $refClass = new ReflectionClass($this->class); + + foreach ($refClass->getMethods() as $method) { + if ((!$method->isPublic()) + || in_array($method->getName(), $blocked_methods) + || substr($method->getName(), strlen($method->getName()) - 4) === 'Form' + ) { + continue; + } + + $methods[] = $method; + } + + return $methods; + } + + /** + * Gets a list of all the public method names for this class. + * @return String[] + */ + public function getClassMethodsPublicNames() + { + $names = []; + foreach ($this->getReflectedMethods() as $method) { + $names[] = self::getMethodPublicName($method); + } + + return $names; + } + + public function getClassInfo() + { + $data = [ + 'description' => '', + 'children' => [], + ]; + + $refClass = new ReflectionClass($this->class); + $data['description'] = self::getClassDoc($refClass)['short_desc']; + + foreach ($this->getReflectedMethods() as $method) { + $op = self::getMethodOpType($method); + $public_name = '/' . self::getMethodPublicName($method); + + if (!$method->isStatic()) { + $public_name = '/{id}'.$public_name; + } + + if (self::isOptionInPathForMethod($method)) { + $public_name .= '/{'.$method->getParameters()[0]->getName().'}'; + } + + $data['children'][$public_name][$op] = $method; + } + + return $data; + } +} diff --git a/src/Classes/ServiceAPI/MyRadio_Team.php b/src/Classes/ServiceAPI/MyRadio_Team.php index 852e3e515..db543d05a 100644 --- a/src/Classes/ServiceAPI/MyRadio_Team.php +++ b/src/Classes/ServiceAPI/MyRadio_Team.php @@ -1,261 +1,402 @@ - * @package MyRadio_Core - * @uses \Database - * + * + * @uses \Database */ -class MyRadio_Team extends ServiceAPI { - /** - * The ID of the Team - * @var int - */ - private $teamid; - - /** - * Team name e.g. "Computing Team" - * @var String - */ - private $name; - /** - * Officer email alias e.g. "computing" - * @var String - */ - private $alias; - /** - * The weight of the Team, when listing on a page. - * @var int - */ - private $ordering; - /** - * A description of the Team. - * @var String - */ - private $description; - /** - * (c)urrent or (h)istorical. - * @var char - */ - private $status; - /** - * Officer positions in this team - * @var int[] - */ - private $officers; - - protected function __construct($id) { - $result = self::$db->fetch_one('SELECT * FROM public.team ' - . 'WHERE teamid=$1', [$id]); - - if (empty($result)) { - throw new MyRadioException('Team '.$id.' does not exist!', 404); - } else { - $this->teamid = (int)$id; - $this->name = $result['team_name']; - $this->alias = $result['local_alias']; - $this->ordering = (int)$result['ordering']; - $this->description = $result['descr']; - $this->status = $result['status']; - $this->officers = array_map(function($x){return (int)$x;}, - self::$db->fetch_column('SELECT officerid FROM officer ' - . 'WHERE teamid=$1 ORDER BY ordering', [$id])); +class MyRadio_Team extends ServiceAPI +{ + /** + * The ID of the Team. + * + * @var int + */ + private $teamid; + + /** + * Team name e.g. "Computing Team". + * + * @var string + */ + private $name; + /** + * Officer email alias e.g. "computing". + * + * @var string + */ + private $alias; + /** + * The weight of the Team, when listing on a page. + * + * @var int + */ + private $ordering; + /** + * A description of the Team. + * + * @var string + */ + private $description; + /** + * (c)urrent or (h)istorical. + * + * @var char + */ + private $status; + /** + * Officer positions in this team. + * + * @var int[] + */ + private $officers; + + protected function __construct($id) + { + $result = self::$db->fetchOne( + 'SELECT * FROM public.team + WHERE teamid=$1', + [$id] + ); + + if (empty($result)) { + throw new MyRadioException('Team '.$id.' does not exist!', 404); + } else { + $this->teamid = (int) $id; + $this->name = $result['team_name']; + $this->alias = $result['local_alias']; + $this->ordering = (int) $result['ordering']; + $this->description = $result['descr']; + $this->status = $result['status']; + $this->officers = array_map( + function ($x) { + return (int) $x; + }, + self::$db->fetchColumn( + 'SELECT officerid FROM officer + WHERE teamid=$1 ORDER BY ordering', + [$id] + ) + ); + } + } + + /** + * Returns all the Teams available. + * + * @return array + */ + public static function getAllTeams() + { + return self::resultSetToObjArray( + self::$db->fetchColumn('SELECT teamid FROM public.team') + ); + } + + /** + * Returns all the current Teams. + * + * @return array + */ + public static function getCurrentTeams() + { + return self::resultSetToObjArray( + self::$db->fetchColumn( + 'SELECT teamid FROM public.team + WHERE status = \'c\' + ORDER BY ordering ASC' + ) + ); + } + + /** + * Returns teamids and names for use in select boxes. + * + * @return array + */ + public static function getTeamSelect() + { + return self::$db->fetchAll( + 'SELECT teamid AS value, team_name AS text FROM public.team + WHERE status = \'c\' + ORDER BY ordering ASC' + ); + } + + /** + * Get the ID for this Team. + * + * @return int + */ + public function getID() + { + return $this->teamid; + } + + /** + * Get the Name of this Team. + * + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Gets the Team primary email alias. + * + * @todo Database discrepancy - the actual lists themselves are defined + * manually. Need to discuss what to do about this. + * + * @return string + */ + public function getAlias() + { + return $this->alias; + } + + /** + * Returns the weight of the Team when listing them. + * + * @return int + */ + public function getOrdering() + { + return $this->ordering; + } + + /** + * Get a description of the Team. + * + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * (c)urrent or (h)istorical. + * + * @return char + */ + public function getStatus() + { + return $this->status; + } + + /** + * (o)fficer, (a)ssistant head of team, (h)ead of team + * or (m)ember (not actually an Officer, just in team). + * + * @return char + */ + public function getType() + { + return $this->type; + } + + /** + * Return all Officer Positions in the team. + * + * @return MyRadio_Officer[] + */ + public function getOfficers() + { + return MyRadio_Officer::resultSetToObjArray($this->officers); } - } - - /** - * Returns all the Teams available. - * @return array - */ - public static function getAllTeams($full = true) { - return self::resultSetToObjArray(self::$db->fetch_column( - 'SELECT teamid FROM public.team'), $full); - } - - /** - * Get the ID for this Team - * @return int - */ - public function getID() { - return $this->teamid; - } - - /** - * Get the Name of this Team - * @return String - */ - public function getName() { - return $this->name; - } - - /** - * Gets the Team primary email alias. - * - * @todo Database discrepancy - the actual lists themselves are defined - * manually. Need to discuss what to do about this. - * @return String - */ - public function getAlias() { - return $this->alias; - } - - /** - * Returns the weight of the Team when listing them. - * @return int - */ - public function getOrdering() { - return $this->ordering; - } - - /** - * Get a description of the Team - * @return String - */ - public function getDescription() { - return $this->description; - } - - /** - * (c)urrent or (h)istorical. - * @return char - */ - public function getStatus() { - return $this->status; - } - - /** - * (o)fficer, (a)ssistant head of team, (h)ead of team - * or (m)ember (not actually an Officer, just in team) - * @return char - */ - public function getType() { - return $this->type; - } - - /** - * Return all Officer Positions in the team - * @return MyRadio_Officer[] - */ - public function getOfficers() { - return MyRadio_Officer::resultSetToObjArray($this->officers); - } - - /** - * Return all Users who held positions in this Team - * @return Array {'User':User, 'from':time, 'to':time|null, - * 'memberofficerid': int, 'position': MyRadio_Officer} - */ - public function getHistory() { - $data = []; - foreach ($this->getOfficers() as $officer) { - $data = array_merge($data, array_map(function($x) use ($officer) { - $x['position'] = $officer; - return $x; - }, $officer->getHistory())); + + /** + * Return all Users who held positions in this Team. + * + * @return array {'User':User, 'from':time, 'to':time|null, + * 'memberofficerid': int, 'position': MyRadio_Officer} + */ + public function getHistory() + { + $data = []; + foreach ($this->getOfficers() as $officer) { + $data = array_merge( + $data, + array_map( + function ($x) use ($officer) { + $x['position'] = $officer; + + return $x; + }, + $officer->getHistory() + ) + ); + } + + usort( + $data, + function ($a, $b) { + return $b['from'] - $a['from']; + } + ); + + return $data; + } + + public static function getGraphQLTypeName() + { + return 'Team'; + } + + /** + * Get Users currently in the Team. + * + * @return array {'User':User, 'from':time, + * 'memberofficerid': int, 'position': MyRadio_Officer} + */ + public function getCurrentHolders() + { + $i = $this->getHistory(); + $result = []; + + foreach ($i as $o) { + if (empty($o['to']) or $o['to'] >= time()) { + unset($o['to']); + $result[] = $o; + } + } + + return $result; + } + + /** + * Returns Officer positions that are the Assistant Head of Team. + * + * @return Officer[] + */ + public function getAssistantHeadPositions() + { + return $this->getMembersOfType('a'); + } + + /** + * Returns Officer positions that are the Head of Team. + * + * @return Officer[] + */ + public function getHeadPositions() + { + return $this->getMembersOfType('h'); } - - usort($data, function($a, $b) { - return $b['from']-$a['from']; - }); - - return $data; - } - - /** - * Get Users currently in the Team - * @return Array {'User':User, 'from':time, - * 'memberofficerid': int, 'position': MyRadio_Officer} - */ - public function getCurrentHolders() { - $i = $this->getHistory(); - $result = array(); - - foreach ($i as $o) { - if (empty($o['to']) or $o['to'] >= time()) { - unset($o['to']); - $result[] = $o; - } + + /** + * Returns Officer positions that are an Officer member. + * + * @return Officer[] + */ + public function getOfficerPositions() + { + return $this->getMembersOfType('o'); } - return $result; - } - - /** - * Returns Officer positions that are the Assistant Head of Team - * @return Officer[] - */ - public function getAssistantHeadPositions() { - return $this->getMembersOfType('a'); - } - - /** - * Returns Officer positions that are the Head of Team - * @return Officer[] - */ - public function getHeadPositions() { - return $this->getMembersOfType('h'); - } - - /** - * Returns Officer positions that are an Officer member - * @return Officer[] - */ - public function getOfficerPositions() { - return $this->getMembersOfType('o'); - } - - /** - * Returns Officer positions that are a non-committee member - * @return Officer[] - */ - public function getMemberPositions() { - return $this->getMembersOfType('m'); - } - - /** - * Returns Officer positions that are the given type - * @param char $type - * @return Officer[] - */ - private function getMembersOfType($type) { - $data = []; - foreach ($this->getCurrentHolders() as $holder) { - if ($holder['position']->getType() == $type) { - $data[] = $holder; - } + + /** + * Returns Officer positions that are a non-committee member. + * + * @return Officer[] + */ + public function getMemberPositions() + { + return $this->getMembersOfType('m'); } - - return $data; - } - - /** - * Returns data about the Team. - * - * @param bool $full If true, includes info about Officers in the Team - * @return Array - */ - public function toDataSource($full = false) { - $data = [ - 'teamid' => $this->getID(), - 'name' => $this->getName(), - 'alias' => $this->getAlias(), - 'ordering' => $this->getOrdering(), - 'description' => $this->getDescription(), - 'status' => $this->getStatus() - ]; - - if ($full) { - $data['officers'] = CoreUtils::dataSourceParser($this->getCurrentHolders(), false); - $data['history'] = CoreUtils::dataSourceParser($this->getHistory(), false); + + /** + * Returns Officer positions that are the given type. + * + * @param char $type + * + * @return MyRadio_Officer[] + */ + private function getMembersOfType($type) + { + $data = []; + foreach ($this->getCurrentHolders() as $holder) { + if ($holder['position']->getType() == $type) { + $data[] = $holder; + } + } + + return $data; + } + + /** + * Returns the team with the given local_alias. + * + * @param string $alias + * + * @return MyRadio_Team + */ + public static function getByAlias($alias) + { + return self::getInstance( + self::$db->fetchColumn('SELECT teamid FROM public.team WHERE local_alias=$1', [$alias])[0] + ); } - - return $data; - } + /** + * Create a new Team with the given paramaters. + * + * @param string $name The name of the new Team + * @param string $descr A friendly description of the new Team + * @param string $alias /[a-z]+/ used for the mailing list name + * @param int $ordering The larger this number, the further down this Team + * + * @return MyRadio_Team The new Team + */ + public static function createTeam($name, $descr, $alias, $ordering) + { + return self::getInstance( + self::$db->fetchColumn( + 'INSERT INTO public.team (team_name, descr, local_alias, ordering) + VALUES ($1, $2, $3, $4) RETURNING teamid', + [$name, $descr, $alias, $ordering] + )[0] + ); + } + + /** + * Returns data about the Team. + * + * @mixin officers Provides officers for the team. + * @mixin history Provides historic data for the team. + * + * @return array + */ + public function toDataSource($mixins = []) + { + $mixin_funcs = [ + 'officers' => function (&$data) { + $data['officers'] = CoreUtils::dataSourceParser($this->getCurrentHolders()); + }, + 'history' => function (&$data) { + $data['history'] = CoreUtils::dataSourceParser($this->getHistory()); + }, + ]; + + $data = [ + 'teamid' => $this->getID(), + 'name' => $this->getName(), + 'alias' => $this->getAlias(), + 'ordering' => $this->getOrdering(), + 'description' => $this->getDescription(), + 'status' => $this->getStatus(), + ]; + + $this->addMixins($data, $mixins, $mixin_funcs); + + return $data; + } } diff --git a/src/Classes/ServiceAPI/MyRadio_Term.php b/src/Classes/ServiceAPI/MyRadio_Term.php new file mode 100644 index 000000000..453812a1b --- /dev/null +++ b/src/Classes/ServiceAPI/MyRadio_Term.php @@ -0,0 +1,218 @@ +term_id = (int) $term_id; + + self::initDB(); + $result = self::$db->fetchOne( + "SELECT * FROM public.terms WHERE termid=$1;", + [$this->term_id] + ); + + if (empty($result)) { + throw new MyRadioException("The specified term " . $this->term_id . " doesn't exist" . json_encode($result)); + return; + } + + $this->start_date = $result['start']; + $this->descr = $result['descr'] . date(" Y", strtotime($this->start_date)); + $this->num_weeks = (int) $result['weeks']; + $this->week_names = json_decode($result['week_names']); + if ($this->week_names == null) { + $this->week_names = []; + } + + } + + /** + * Create a new term. + * + * @param int $start The term start date + * @param string $descr Term description e.g. Autumn 2036 + * + * @return int The new termid + */ + public static function addTerm($start, $descr, $num_weeks) + { + if (date('D', $start) !== 'Mon') { + throw new MyRadioException('Terms must start on a Monday.', 400); + } + if (!is_numeric($num_weeks)){ + throw new MyRadioException('Weeks must be an integer.', 400); + } + $num_weeks = (int)$num_weeks; + // Let's make this a GMT thing, exactly midnight + $ts = gmdate('Y-m-d 00:00:00+00', $start); + $end = $start + (86400 * ((7*$num_weeks)-2)); // to Friday of the final week + $te = gmdate('Y-m-d 00:00:00+00', $end); + + return self::$db->fetchColumn( + 'INSERT INTO terms (start, finish, descr, weeks) VALUES ($1, $2, $3, $4) RETURNING termid', + [$ts, $te, $descr, $num_weeks] + )[0]; + } + + /** + * Returns a list of terms in the present or future. + * + * @param bool $currentOnly If only the present term should be output (if term time) + * @return Array[Array] an array of arrays of terms + * @TODO caching + */ + public static function getAllTerms($currentOnly = false) + { + if ($currentOnly == "false") { + $currentOnly = false; + } + + $query = 'SELECT termid, EXTRACT(EPOCH FROM start) AS start FROM terms '; + $query .= $currentOnly ? 'WHERE start <= now() AND finish > now() ' : ''; + $query .= 'ORDER BY start ASC'; + $result = self::$db->fetchAll($query); + + $terms = []; + foreach ($result as $row) { + $term = new self($row['termid']); + $terms[] = $term; + } + + return $terms; + } + + /** + * Returns if we are currently in term time. + * + * @return Boolean + */ + public static function isTerm() + { + return true; + return (!empty(self::getAllTerms(true))); + } + + public function getID() { + return $this->term_id; + } + + public function getTermDescr() { + return $this->descr; + } + + public function getTermWeeks() { + return $this->num_weeks; + } + + /** + Return an array of week names in a term + Stored as a JSON array in the DB. + + i.e. ['Week 0 Sem 1', 'Week 1 Sem 1'] etc + + @return array of strings, the week names + } + */ + public function getTermWeekNames() { + return $this->week_names; + } + + public function getTermStartDate() { + return strtotime('Midnight '.gmdate('d-m-Y', strtotime($this->start_date)).' GMT'); + } + + /** + * Returns the Term currently available for Season applications. + * Users can only apply to the current term, or 28 days before the next one + * starts. + */ + public static function getActiveApplicationTerm() + { + $return = self::$db->fetchColumn( + 'SELECT termid FROM terms + WHERE start <= $1 AND finish >= NOW() LIMIT 1', + [CoreUtils::getTimestamp(strtotime('+28 Days'))] + ); + + if (empty($return)) { + return; + } + + return new self($return[0]); + } + + public static function getTermForm() + { + return ( + new MyRadioForm( + 'sched_term', + 'Scheduler', + 'editTerm', + [ + 'title' => 'Scheduler', + 'subtitle' => 'Create Term', + ] + ) + )->addField( + new MyRadioFormField( + 'descr', + MyRadioFormField::TYPE_TEXT, + [ + 'explanation' => 'Name the term. A value of "Autumn" denotes that this ' + . 'term represents the start of a new membership year.', + 'label' => 'Term description', + 'options' => ['maxlength' => 10], + ] + ) + )->addField( + new MyRadioFormField( + 'numweeks', + MyRadioFormField::TYPE_NUMBER, + [ + 'explanation' => 'How many weeks will there be in the term?', + 'label' => 'Term weeks', + ] + ) + )->addField( + new MyRadioFormField( + 'start', + MyRadioFormField::TYPE_DATE, + [ + 'explanation' => 'Select a term start date. This must be a Monday.', + 'label' => 'Start date', + ] + ) + ); + + } + + public function toDataSource($mixins = []) { + return [ + "term_id" => $this->getID(), + "start" => $this->getTermStartDate(), + "descr" => $this->getTermDescr(), + "num_weeks" => $this->getTermWeeks(), + "week_names" => $this->getTermWeekNames() + ]; + } +} diff --git a/src/Classes/ServiceAPI/MyRadio_Timeslot.php b/src/Classes/ServiceAPI/MyRadio_Timeslot.php index 51e66f987..998ea053a 100644 --- a/src/Classes/ServiceAPI/MyRadio_Timeslot.php +++ b/src/Classes/ServiceAPI/MyRadio_Timeslot.php @@ -1,83 +1,133 @@ - * @package MyRadio_Scheduler + * The Timeslot class is used to view and manupulate Timeslot within the new MyRadio Scheduler Format. + * + * @todo Generally creation of bulk Timeslots is currently handled by the Season/Show classes, but this should change + * * @uses \Database * @uses \MyRadio_Show - * */ -class MyRadio_Timeslot extends MyRadio_Metadata_Common { - +class MyRadio_Timeslot extends MyRadio_Metadata_Common +{ private $timeslot_id; private $start_time; private $duration; private $season_id; - private $owner; private $timeslot_num; + protected $owner; + protected $playout; protected $credits; - protected function __construct($timeslot_id) { - $this->timeslot_id = $timeslot_id; + protected function __construct($timeslot_id) + { + if (empty($timeslot_id)) { + throw new MyRadioException('Timeslot ID must be provided.'); + } + + $this->timeslot_id = (int)$timeslot_id; //Init Database self::initDB(); - //Get the basic info about the season - $result = self::$db->fetch_one('SELECT show_season_timeslot_id, - show_season_id, start_time, duration, memberid, - (SELECT array(SELECT metadata_key_id FROM schedule.timeslot_metadata - WHERE show_season_timeslot_id=$1 AND effective_from <= NOW() AND - (effective_to IS NULL OR effective_to >= NOW()) - ORDER BY effective_from, show_season_timeslot_id)) AS metadata_types, - (SELECT array(SELECT metadata_value FROM schedule.timeslot_metadata - WHERE show_season_timeslot_id=$1 AND effective_from <= NOW() AND - (effective_to IS NULL OR effective_to >= NOW()) - ORDER BY effective_from, show_season_timeslot_id)) AS metadata, - (SELECT COUNT(*) FROM schedule.show_season_timeslot - WHERE show_season_id=(SELECT show_season_id FROM schedule.show_season_timeslot WHERE show_season_timeslot_id=$1) - AND start_time<=(SELECT start_time FROM schedule.show_season_timeslot WHERE show_season_timeslot_id=$1)) - AS timeslot_num, - (SELECT array(SELECT creditid FROM schedule.show_credit - WHERE show_id=( - SELECT show_id FROM schedule.show_season_timeslot - JOIN schedule.show_season USING (show_season_id) - WHERE show_season_timeslot_id=$1 - ) - AND effective_from <= NOW() AND (effective_to IS NULL OR effective_to >= NOW()) AND approvedid IS NOT NULL - ORDER BY show_credit_id)) AS credits, - (SELECT array(SELECT credit_type_id FROM schedule.show_credit - WHERE show_id=( - SELECT show_id FROM schedule.show_season_timeslot - JOIN schedule.show_season USING (show_season_id) - WHERE show_season_timeslot_id=$1 - ) AND effective_from <= NOW() AND (effective_to IS NULL OR effective_to >= NOW()) AND approvedid IS NOT NULL - ORDER BY show_credit_id)) AS credit_types - FROM schedule.show_season_timeslot WHERE show_season_timeslot_id=$1', array($timeslot_id)); + //Get the basic info about the timeslot + // Note that credits have different metadata timeranges to text + // This is annoying, but needs to be this way. + $result = self::$db->fetchOne( + 'SELECT show_season_timeslot_id, show_season_id, start_time, duration, memberid, playout::boolean::text, ( + SELECT array_to_json(array( + SELECT metadata_key_id FROM schedule.timeslot_metadata + WHERE show_season_timeslot_id=$1 + AND effective_from < NOW() + AND (effective_to IS NULL OR effective_to > NOW()) + ORDER BY effective_from, show_season_timeslot_id + )) + ) AS metadata_types, ( + SELECT array_to_json(array( + SELECT metadata_value FROM schedule.timeslot_metadata + WHERE show_season_timeslot_id=$1 + AND effective_from < NOW() + AND (effective_to IS NULL OR effective_to > NOW()) + ORDER BY effective_from, show_season_timeslot_id + )) + ) AS metadata, ( + SELECT COUNT(*) FROM schedule.show_season_timeslot + WHERE show_season_id=( + SELECT show_season_id FROM schedule.show_season_timeslot + WHERE show_season_timeslot_id=$1 + ) + AND start_time<=(SELECT start_time FROM schedule.show_season_timeslot WHERE show_season_timeslot_id=$1) + ) AS timeslot_num, ( + SELECT array_to_json(array( + SELECT creditid FROM schedule.show_credit + WHERE show_id=( + SELECT show_id FROM schedule.show_season_timeslot + JOIN schedule.show_season USING (show_season_id) + WHERE show_season_timeslot_id=$1 + ) + AND effective_from < (start_time + duration) + AND (effective_to IS NULL OR effective_to > start_time) + AND approvedid IS NOT NULL + ORDER BY show_credit_id + )) + ) AS credits, ( + SELECT array_to_json(array( + SELECT credit_type_id FROM schedule.show_credit + WHERE show_id=( + SELECT show_id FROM schedule.show_season_timeslot + JOIN schedule.show_season USING (show_season_id) + WHERE show_season_timeslot_id=$1 + ) + AND effective_from < (start_time + duration) + AND (effective_to IS NULL OR effective_to > start_time) + AND approvedid IS NOT NULL + ORDER BY show_credit_id + )) + ) AS credit_types + FROM schedule.show_season_timeslot + WHERE show_season_timeslot_id=$1', + [$timeslot_id] + ); if (empty($result)) { //Invalid Season - throw new MyRadioException('The MyRadio_Timeslot with instance ID #' . $timeslot_id . ' does not exist.'); + throw new MyRadioException( + 'The MyRadio_Timeslot with instance ID #' . $timeslot_id . ' does not exist.', + 404 + ); } //Deal with the easy bits - $this->timeslot_id = (int) $result['show_season_timeslot_id']; - $this->season_id = (int) $result['show_season_id']; + $this->timeslot_id = (int)$result['show_season_timeslot_id']; + $this->season_id = (int)$result['show_season_id']; $this->start_time = strtotime($result['start_time']); $this->duration = $result['duration']; $this->owner = MyRadio_User::getInstance($result['memberid']); - $this->timeslot_num = (int) $result['timeslot_num']; + $this->playout = $result['playout'] == "true"; + $this->timeslot_num = (int)$result['timeslot_num']; - $metadata_types = self::$db->decodeArray($result['metadata_types']); - $metadata = self::$db->decodeArray($result['metadata']); + $metadata_types = json_decode($result['metadata_types']); + $metadata = json_decode($result['metadata']); //Deal with the metadata - for ($i = 0; $i < sizeof($metadata_types); $i++) { + for ($i = 0; $i < sizeof($metadata_types); ++$i) { if (self::isMetadataMultiple($metadata_types[$i])) { $this->metadata[$metadata_types[$i]][] = $metadata[$i]; } else { @@ -86,19 +136,22 @@ protected function __construct($timeslot_id) { } //Deal with the Credits arrays - $credit_types = self::$db->decodeArray($result['credit_types']); - $credits = self::$db->decodeArray($result['credits']); + $credit_types = json_decode($result['credit_types']); + $credits = json_decode($result['credits']); - for ($i = 0; $i < sizeof($credits); $i++) { + for ($i = 0; $i < sizeof($credits); ++$i) { if (empty($credits[$i])) { continue; } - $this->credits[] = array('type' => (int) $credit_types[$i], 'memberid' => $credits[$i], - 'User' => MyRadio_User::getInstance($credits[$i])); + $this->credits[] = [ + 'type' => (int)$credit_types[$i], 'memberid' => $credits[$i], + 'User' => MyRadio_User::getInstance($credits[$i]), + ]; } } - public function getMeta($meta_string) { + public function getMeta($meta_string) + { $key = self::getMetadataKey($meta_string); if (isset($this->metadata[$key])) { return $this->metadata[$key]; @@ -107,70 +160,143 @@ public function getMeta($meta_string) { } } - public function getID() { + public function getID() + { return $this->timeslot_id; } - public function getSeason() { + public function getSeason() + { return MyRadio_Season::getInstance($this->season_id); } - public function getWebpage() { - $season = $this->getSeason(); - return 'http://ury.org.uk/show/' . $season->getShow()->getID() . '/' . $season->getSeasonNumber() . '/' . $this->getTimeslotNumber(); + /** + * Get the microsite URI. + * + * @return string + */ + public function getWebpage() + { + return '/schedule/shows/timeslots/' . $this->timeslot_id; } - public function getPhoto() { + public function getPhoto() + { return $this->getSeason()->getShow()->getShowPhoto(); } /** * Get the Timeslot number - for the first Timeslot of a Season, this is 1, for the second it's 2 etc. + * * @return int */ - public function getTimeslotNumber() { + public function getTimeslotNumber() + { return $this->timeslot_num; } /** - * Get the start time of the Timeslot as an integer since epoch + * Get the start time of the Timeslot as an integer since epoch. + * * @return int */ - public function getStartTime() { + public function getStartTime() + { return $this->start_time; } - public function getDuration() { + public function getDuration() + { return $this->duration; } /** - * Returns when the timeslot ends in epoch number form + * Returns when the timeslot ends in epoch number form. + * * @return int */ - public function getEndTime() { + public function getEndTime() + { $duration = strtotime('1970-01-01 ' . $this->getDuration() . '+00'); + return $this->getStartTime() + $duration; } + /** + * Returns whether the user has selected the timeslot for automatic playout + * + * @return bool + */ + public function getPlayout() + { + return $this->playout; + } + /** * Gets the Timeslot that is on after this. - * @param MyRadio_Timeslot $timeslot + * + * @param $filter defines a filter of show_type ids + * * @return MyRadio_Timeslot|null If null, Jukebox is next. */ - public function getTimeslotAfter() { - $result = self::$db->fetch_column('SELECT show_season_timeslot_id' - . ' FROM schedule.show_season_timeslot' - . ' WHERE start_time >= $1 AND start_time <= $2' - . ' ORDER BY start_time ASC LIMIT 1', [CoreUtils::getTimestamp($this->getEndTime() - 300), - CoreUtils::getTimestamp($this->getEndTime() + 300)]); + public function getTimeslotAfter($filter = [1]) + { + // lolphp http://php.net/manual/en/function.pg-query-params.php#71912 + $filter = '{' . implode(', ', $filter) . '}'; + + $result = self::$db->fetchColumn( + 'SELECT show_season_timeslot_id + FROM schedule.show_season_timeslot + INNER JOIN schedule.show_season USING (show_season_id) + INNER JOIN schedule.show USING (show_id) + WHERE start_time >= $1 AND start_time <= $2 + AND show_type_id = ANY ($3) + ORDER BY start_time ASC LIMIT 1', + [ + CoreUtils::getTimestamp($this->getEndTime() - 300), + CoreUtils::getTimestamp($this->getEndTime() + 300), + $filter, + ] + ); if (empty($result)) { - return null; + return; } else { return self::getInstance($result[0]); } } + /** + * Returns the currently selected timeslot (from the navbar). + * + * @return MyRadio_Timeslot|null If null, user has no selected timeslot. + */ + public static function getUserSelectedTimeslot() + { + if (isset($_SESSION['timeslotid'])) { + $timeslot = self::getInstance($_SESSION['timeslotid']); + + return $timeslot; + } + return null; + } + + /** + * Sets the current user selected timeslot. NOTE: No auth checking here. + * + * @param MyRadioTimeslot|null $timeslot The timeslot (or none) to set the current user timeslot to. + * + * @return MyRadio_Timeslot|null If null, user has no selected timeslot. + */ + public static function setUserSelectedTimeslot($timeslot = null) { + if ($timeslot) { + $_SESSION['timeslotid'] = $timeslot->getID(); + $_SESSION['timeslotname'] = CoreUtils::happyTime($timeslot->getStartTime()); + } else { + $_SESSION['timeslotid'] = null; + $_SESSION['timeslotname'] = null; + } + } + /** * Sets a metadata key to the specified value. * @@ -180,50 +306,117 @@ public function getTimeslotAfter() { * set to the effective_from of this value, effectively replacing the existing value. * This will *not* unset is_multiple values that are not in the new set. * - * @param String $string_key The metadata key + * @param string $string_key The metadata key * @param mixed $value The metadata value. If key is_multiple and value is an array, will create instance - * for value in the array. + * for value in the array. * @param int $effective_from UTC Time the metavalue is effective from. Default now. * @param int $effective_to UTC Time the metadata value is effective to. Default NULL (does not expire). - * @param null $table No action. Used for compatibility with parent. - * @param null $pkey No action. Used for compatibility with parent. */ - public function setMeta($string_key, $value, $effective_from = null, $effective_to = null, $table = null, $pkey = null) { - $r = parent::setMeta($string_key, $value, $effective_from, $effective_to, 'schedule.timeslot_metadata', 'show_season_timeslot_id'); + public function setMeta($string_key, $value, $effective_from = null, $effective_to = null) + { + $r = parent::setMetaBase( + $string_key, + $value, + $effective_from, + $effective_to, + 'schedule.timeslot_metadata', + 'show_season_timeslot_id' + ); $this->updateCacheObject(); + return $r; } - public function toDataSource() { - return array_merge($this->getSeason()->toDataSource(), array( - 'id' => $this->getID(), - 'timeslot_num' => $this->getTimeslotNumber(), - 'title' => $this->getMeta('title'), - 'description' => $this->getMeta('description'), - 'tags' => $this->getMeta('tag'), - 'start_time' => CoreUtils::happyTime($this->getStartTime()), - 'duration' => $this->getDuration(), - 'mixcloud_status' => $this->getMeta('upload_state'), - 'rejectlink' => array( - 'display' => 'icon', - 'value' => 'trash', - 'title' => 'Cancel Episode', - 'url' => CoreUtils::makeURL('Scheduler', 'cancelEpisode', array('show_season_timeslot_id' => $this->getID()))) - )); + /** + * Searches searchable *text* metadata for the specified value. Does not work for image metadata. + * + * @param string $query The query value. + * @param array $string_keys The metadata keys to search + * @param int $effective_from UTC Time to search from. + * @param int $effective_to UTC Time to search to. + * + * @return array The shows that match the search terms + * @todo effective_from/to not yet implemented + * + */ + public static function searchMeta($query, $string_keys = null, $effective_from = null, $effective_to = null) + { + if (is_null($string_keys)) { + $string_keys = ['title', 'description', 'tag']; + } + + $r = parent::searchMetaBase( + $query, + $string_keys, + $effective_from, + $effective_to, + 'schedule.timeslot_metadata', + 'show_season_timeslot_id' + ); + return self::resultSetToObjArray($r); } /** - * Find the most messaged Timeslots - * @param int $date If specified, only messages for timeslots since $date are counted. - * @return array An array of 30 Timeslots that have been put through toDataSource, with the addition of a msg_count key, - * referring to the number of messages sent to that show. + * Serialises the timeslot. Merges with the parent season object as well. */ - public static function getMostMessaged($date = 0) { - $result = self::$db->fetch_all('SELECT messages.timeslotid, count(*) as msg_count FROM sis2.messages - LEFT JOIN schedule.show_season_timeslot ON messages.timeslotid = show_season_timeslot.show_season_timeslot_id - WHERE show_season_timeslot.start_time > $1 GROUP BY messages.timeslotid ORDER BY msg_count DESC LIMIT 30', array(CoreUtils::getTimestamp($date))); + public function toDataSource($mixins = []) + { + return array_merge( + $this->getSeason()->toDataSource($mixins), + [ + 'timeslot_id' => $this->getID(), + 'timeslot_num' => $this->getTimeslotNumber(), + 'title' => $this->getMeta('title'), + 'description' => $this->getMeta('description'), + 'tags' => $this->getMeta('tag'), + 'time' => $this->getStartTime(), + 'start_time' => CoreUtils::happyTime($this->getStartTime()), + 'duration' => $this->getDuration(), + 'mixcloud_status' => $this->getMeta('upload_state'), + 'mixcloud_starttime' => $this->getMeta('upload_starttime'), + 'mixcloud_endtime' => $this->getMeta('upload_endtime'), + 'rejectlink' => [ + 'display' => 'icon', + 'value' => 'trash', + 'title' => 'Cancel Episode', + 'url' => URLUtils::makeURL( + 'Scheduler', + 'cancelEpisode', + ['show_season_timeslot_id' => $this->getID()] + ), + ], + 'movelink' => [ + 'display' => 'icon', + 'value' => 'transfer', + 'title' => 'Move Episode', + 'url' => URLUtils::makeURL( + 'Scheduler', + 'moveEpisode', + ['show_season_timeslot_id' => $this->getID()] + ), + ] + ] + ); + } - $top = array(); + /** + * Find the most messaged Timeslots. + * + * @param int $date If specified, only messages for timeslots since $date are counted. + * + * @return array An array of 30 Timeslots that have been put through toDataSource, with the addition of a msg_count + * key, referring to the number of messages sent to that show. + */ + public static function getMostMessaged($date = 0) + { + $result = self::$db->fetchAll( + 'SELECT messages.timeslotid, count(*) as msg_count FROM sis2.messages + LEFT JOIN schedule.show_season_timeslot ON messages.timeslotid=show_season_timeslot.show_season_timeslot_id + WHERE show_season_timeslot.start_time > $1 GROUP BY messages.timeslotid ORDER BY msg_count DESC LIMIT 30', + [CoreUtils::getTimestamp($date)] + ); + + $top = []; foreach ($result as $r) { $show = self::getInstance($r['timeslotid'])->toDataSource(); $show['msg_count'] = intval($r['msg_count']); @@ -234,24 +427,34 @@ public static function getMostMessaged($date = 0) { } /** - * Find the most listened Timeslots + * Find the most listened Timeslots. + * * @param int $date If specified, only messages for timeslots since $date are counted. - * @return array An array of 30 Timeslots that have been put through toDataSource, with the addition of a msg_count key, - * referring to the number of messages sent to that show. + * + * @return array An array of 30 Timeslots that have been put through toDataSource, with the addition of a msg_count + * key, referring to the number of messages sent to that show. */ - public static function getMostListened($date = 0) { + public static function getMostListened($date = 0) + { $key = 'stats_timeslot_mostlistened'; if (($top = self::$cache->get($key)) !== false) { return $top; } - $result = self::$db->fetch_all('SELECT show_season_timeslot_id, - (SELECT COUNT(*) FROM strm_log WHERE (starttime < start_time AND endtime >= start_time) - OR (starttime >= start_time AND starttime < start_time + duration)) AS listeners - FROM schedule.show_season_timeslot WHERE start_time > $1 - ORDER BY listeners DESC LIMIT 30', array(CoreUtils::getTimestamp($date))); - - $top = array(); + $result = self::$db->fetchAll( + 'SELECT show_season_timeslot_id, + ( + SELECT COUNT(*) FROM strm_log + WHERE (starttime < start_time + AND endtime >= start_time) + OR (starttime >= start_time AND starttime < start_time + duration) + ) AS listeners + FROM schedule.show_season_timeslot WHERE start_time > $1 + ORDER BY listeners DESC LIMIT 30', + [CoreUtils::getTimestamp($date)] + ); + + $top = []; foreach ($result as $r) { $show = self::getInstance($r['show_season_timeslot_id'])->toDataSource(); $show['listeners'] = intval($r['listeners']); @@ -259,68 +462,267 @@ public static function getMostListened($date = 0) { } self::$cache->set($key, $top, 86400); + return $top; } /** * Returns the current Timeslot on air, if there is one. + * * @param int $time Optional integer timestamp + * @param $filter defines a filter of show_type ids * * @return MyRadio_Timeslot|null */ - public static function getCurrentTimeslot($time = null) { + public static function getCurrentTimeslot($time = null, $filter = [1]) + { self::initDB(); //First DB access for Timelord if ($time === null) { $time = time(); } - $result = self::$db->fetch_column('SELECT show_season_timeslot_id FROM - schedule.show_season_timeslot WHERE start_time <= $1 AND - start_time + duration >= $1', [CoreUtils::getTimestamp($time)]); + $filter = '{' . implode(', ', $filter) . '}'; // http://php.net/manual/en/function.pg-query-params.php#71912 + + $result = self::$db->fetchColumn( + 'SELECT show_season_timeslot_id + FROM schedule.show_season_timeslot + INNER JOIN schedule.show_season USING (show_season_id) + INNER JOIN schedule.show USING (show_id) + WHERE start_time <= $1 + AND start_time + duration >= $1 + AND show_type_id = ANY ($2)', + [CoreUtils::getTimestamp($time), $filter] + ); if (empty($result)) { - return null; + return; } else { - return MyRadio_Timeslot::getInstance($result[0]); + return self::getInstance($result[0]); + } + } + + /** + * Gets the previous Timeslots before $time, in reverse chronological order. + * + * @param int $time + * @param int $n defines the number of timeslots you want before this time. + * @param $filter defines a filter of show_type ids + * + * @return Array of MyRadio_Timeslots + */ + public static function getPreviousTimeslots($time = null, $n = 1, $filter = [1]) + { + // lolphp http://php.net/manual/en/function.pg-query-params.php#71912 + $filter = '{' . implode(', ', $filter) . '}'; + + $result = self::$db->fetchAll( + 'SELECT show_season_timeslot_id + FROM schedule.show_season_timeslot + INNER JOIN schedule.show_season USING (show_season_id) + INNER JOIN schedule.show USING (show_id) + WHERE start_time < $1 + AND show_type_id = ANY ($3) + ORDER BY start_time DESC + LIMIT $2', + [CoreUtils::getTimestamp($time), $n, $filter] + ); + + $timeslots = []; + foreach ($result as $r) { + $timeslots[] = self::getInstance($r['show_season_timeslot_id']); } + return $timeslots; } /** - * Gets the next Timeslot to start after $time + * Gets the next Timeslot to start after $time. + * * @param int $time + * @param $filter defines a filter of show_type ids + * * @return MyRadio_Timeslot */ - public static function getNextTimeslot($time = null) { - $result = self::$db->fetch_column('SELECT show_season_timeslot_id FROM - schedule.show_season_timeslot WHERE start_time >= $1 - ORDER BY start_time ASC - LIMIT 1', [CoreUtils::getTimestamp($time)]); + public static function getNextTimeslot($time = null, $filter = [1]) + { + // lolphp http://php.net/manual/en/function.pg-query-params.php#71912 + $filter = '{' . implode(', ', $filter) . '}'; + + $result = self::$db->fetchColumn( + 'SELECT show_season_timeslot_id + FROM schedule.show_season_timeslot + INNER JOIN schedule.show_season USING (show_season_id) + INNER JOIN schedule.show USING (show_id) + WHERE start_time >= $1 + AND show_type_id = ANY ($2) + ORDER BY start_time ASC + LIMIT 1', + [CoreUtils::getTimestamp($time), $filter] + ); if (empty($result)) { - return null; + return; } else { return self::getInstance($result[0]); } } + /** + * Returns Timeslots scheduled for the given week number. + * + * "Weeks" are nine days - From the Sunday preceeding the week to the Monday + * after the week ends, i.e. Sun/Mon/Tue/Wed/Thu/Fri/Sat/Sun/Mon. + * A Timeslot that starts before the start of the period but ends during + * will be included. The same is true for ones that end after the period.
    + * It is guaranteed that the results will be in order of start time. + * + * @param int $weekno An ISO-8601 Week Number (http://en.wikipedia.org/wiki/ISO_8601#Week_dates) + * @param int $year Default to current Calendar year. + * + * @return MyRadio_Timeslot[] + */ + public static function get9DaySchedule($weekno, $year = null) + { + self::wakeup(); + if ($year === null) { + $year = (int)gmdate('Y'); + } + + if ($weekno < 10) { + $weekno = '0' . $weekno; + } + + $key = 'MyRadio9DayScheduleFor' . $year . 'W' . $weekno; + $cache = self::$cache->get($key); + if (!$cache) { + $startOfWeek = strtotime($year . 'W' . $weekno); + $sundayBefore = $startOfWeek - 86400; // 60 * 60 * 24 + $endOfMondayAfter = $startOfWeek + (86400 * 8) - 1; //Monday 23:59:59 + + $startTimestamp = CoreUtils::getTimestamp($sundayBefore); + $endTimestamp = CoreUtils::getTimestamp($endOfMondayAfter); + + $result = self::$db->fetchColumn( + 'SELECT show_season_timeslot_id + FROM schedule.show_season_timeslot + INNER JOIN schedule.show_season USING (show_season_id) + INNER JOIN schedule.show USING (show_id) + WHERE ( + (start_time + duration >= $1 AND start_time + duration <= $2) OR + (start_time >= $1 AND start_time <= $2) + ) + AND show_type_id = 1 + ORDER BY start_time ASC', + [$startTimestamp, $endTimestamp] + ); + + $cache = self::resultSetToObjArray($result); + + self::$cache->set($key, $cache, 3600); + } + + return $cache; + } + + /** + * Returns Timeslots scheduled for the given week number. + * + * Weeks are from Monday - Sunday (URY days start at 6am) + * A Timeslot that starts before the start of the period but ends during + * will be included. The same is true for ones that end after the period.
    + * It is guaranteed that the results will be in order of start time. + * + * @param int $weekno An ISO-8601 Week Number (http://en.wikipedia.org/wiki/ISO_8601#Week_dates) + * @param int $year Default to current Calendar year. + * + * @return MyRadio_Timeslot[] + */ + public static function getWeekSchedule($weekno, $year = null) + { + self::wakeup(); + if ($year === null) { + $year = (int)gmdate('Y'); + } + + if ($weekno < 10) { + $weekno = '0' . $weekno; + } + + $key = 'MyRadioWeekScheduleFor' . $year . 'W' . $weekno; + $cache = self::$cache->get($key); + if (!$cache) { + $startOfWeek = strtotime($year . 'W' . $weekno) + (60 * 60 * 6); // Monday 06:00:00 + $endOfWeek = $startOfWeek + (86400 * 7) - 1; //Next Monday 05:59:59 + + $startTimestamp = CoreUtils::getTimestamp($startOfWeek); + $endTimestamp = CoreUtils::getTimestamp($endOfWeek); + + $result = self::$db->fetchAll( + 'SELECT show_season_timeslot_id, EXTRACT(ISODOW FROM (start_time - interval \'6 hours\')) as day + FROM schedule.show_season_timeslot + INNER JOIN schedule.show_season USING (show_season_id) + INNER JOIN schedule.show USING (show_id) + WHERE ( + (start_time + duration >= $1 AND start_time + duration <= $2) OR + (start_time >= $1 AND start_time <= $2) + ) + AND show_type_id = 1 + ORDER BY start_time ASC', + [$startTimestamp, $endTimestamp] + ); + + $schedule = []; + + foreach ($result as $item) { + $schedule[$item['day']][] = self::getInstance($item['show_season_timeslot_id']); + } + + $cache = $schedule; + + self::$cache->set($key, $cache, 3600); + } + + return $cache; + } + /** * Returns the current timeslot, and the n after it, in a simplified * datasource format. Mainly intended for API use. + * * @param int $time + * @param int $n number of next shows to return + * @param $filter defines a filter of show_type ids */ - public static function getCurrentAndNext($time = null, $n = 1) { - $timeslot = self::getCurrentTimeslot($time); - $next = self::getNextTimeslot($time); + public static function getCurrentAndNext($time = null, $n = 1, $filter = [1]) + { + $timeslot = self::getCurrentTimeslot($time, $filter); + $next = self::getNextTimeslot($time, $filter); + //Still display a show if there's one scheduled for whatever reason. if (empty($timeslot)) { - //There's currently not a show on. - $response = [ - 'current' => [ - 'title' => Config::$short_name . ' Jukebox', - 'desc' => 'Non-stop Music', - 'photo' => Config::$default_show_uri, - 'end_time' => $next->getStartTime()] - ]; + // Checking if it's term time according to the schedule is unreliable. Instead, check which selector source + // is on. + $source = MyRadio_Selector::getStudioAtTime(); + if ($source === MyRadio_Selector::SEL_OFFAIR) { + $response = [ + 'current' => [ + 'title' => 'Off Air', + 'desc' => 'We\'re not broadcasting right now, we\'ll be back next term.', + 'photo' => Config::$offair_uri, + 'end_time' => $next ? $next->getStartTime() : 'The End of Time', + ], + ]; + } else { + $response = [ + 'current' => [ + 'title' => Config::$short_name . ' Jukebox', + 'desc' => 'There are currently no shows on right now, even our presenters + need a break. But it\'s okay, ' . Config::$short_name . + ' Jukebox has got you covered, playing the best music for your ears!', + 'photo' => Config::$default_show_uri, + 'end_time' => $next ? $next->getStartTime() : 'The End of Time', + ], + ]; + } } else { //There's a show on! $response = [ @@ -330,25 +732,30 @@ public static function getCurrentAndNext($time = null, $n = 1) { 'photo' => $timeslot->getPhoto(), 'start_time' => $timeslot->getStartTime(), 'end_time' => $timeslot->getEndTime(), - 'presenters' => $timeslot->getPresenterString()] + 'presenters' => $timeslot->getPresenterString(), + 'url' => $timeslot->getWebpage(), + 'id' => $timeslot->getID(), + ], ]; - $next = $timeslot->getTimeslotAfter(); + $next = $timeslot->getTimeslotAfter($filter); } $lastnext = $timeslot; - for ($i = 0; $i < $n; $i++) { + for ($i = 0; $i < $n; ++$i) { if (empty($next)) { - if ($lastnext instanceof MyRadio_Timeslot) { + if ($lastnext instanceof self) { //There's not a next show, but there might be one later - $nextshow = self::getNextTimeslot($lastnext->getEndTime()); + $nextshow = self::getNextTimeslot($lastnext->getEndTime(), $filter); $response['next'][] = [ 'title' => Config::$short_name . ' Jukebox', - 'desc' => 'Non-stop Music', + 'desc' => 'There are currently no shows on right now, even our presenters + need a break. But it\'s okay, ' . Config::$short_name . + ' Jukebox has got you covered, playing the best music for your ears!', 'photo' => Config::$default_show_uri, 'start_time' => $lastnext->getEndTime(), - 'end_time' => $nextshow ? $nextshow->getStartTime() : 'The End of Time' + 'end_time' => $nextshow ? $nextshow->getStartTime() : 'The End of Time', ]; } } else { @@ -359,17 +766,19 @@ public static function getCurrentAndNext($time = null, $n = 1) { 'photo' => $next->getPhoto(), 'start_time' => $next->getStartTime(), 'end_time' => $next->getEndTime(), - 'presenters' => $next->getPresenterString() + 'presenters' => $next->getPresenterString(), + 'url' => $next->getWebpage(), + 'id' => $next->getID(), ]; } - if ($next instanceof MyRadio_Timeslot) { + if ($next instanceof self) { $lastnext = $next; - $next = $next->getTimeslotAfter(); + $next = $next->getTimeslotAfter($filter); } else { - if ($lastnext instanceof MyRadio_Timeslot) { + if ($lastnext instanceof self) { $last = $next; - $next = self::getNextTimeslot($lastnext->getEndTime()); + $next = self::getNextTimeslot($lastnext->getEndTime(), $filter); $lastnext = $last; } else { $lastnext = $next; @@ -378,42 +787,168 @@ public static function getCurrentAndNext($time = null, $n = 1) { } } - if (sizeof($response['next']) === 1) { + if (isset($response['next']) && sizeof($response['next']) === 1 && $n == 1) { $response['next'] = $response['next'][0]; } return $response; } + /** + * Returns the current timeslot, and the n after it - returning the Timeslot objects for real shows, + * or simplified dataSource-ish arrays for non-shows. + * + * Note that, unlike getCurrentAndNext, $result['next'] will always be an array - either of Timeslots or + * of arrays. + * + * @param int|null $time time to check, defaults to current time + * @param int $n number of next shows to return + * @param int[] $filter defines a filter of show_type ids + * @return array + */ + public static function getCurrentAndNextObjects($time = null, $n = 1, $filter = [1]) + { + $value = self::getCurrentAndNext($time, $n, $filter); + if (isset($value['current']['id'])) { + $value['current'] = MyRadio_Timeslot::getInstance($value['current']['id']); + } + + // next can be either an array, or an array of arrays, and because PHP, count($assoc_array) returns the number + // of keys. So we check the number of non-string keys to decide. + if (count(array_filter(array_keys($value['next']), 'is_string')) === 0) { + $value['next'] = array_map(function ($show) { + return isset($show['id']) ? MyRadio_Timeslot::getInstance($show['id']) : $show; + }, $value['next']); + } else { + if (isset($value['next']['id'])) { + $value['next'] = [MyRadio_Timeslot::getInstance($value['next']['id'])]; + } else { + $value['next'] = [$value['next']]; + } + } + + return $value; + } + + /** + * Returns the next $n timeslots for the given user. + * @param int $memberid + * @param int $n + * @return self[] + */ + public static function getUserNextTimeslots(int $memberid, int $n = 10): array + { + $ids = self::$db->fetchColumn( + 'select show_season_timeslot_id from schedule.show_season_timeslot ts + where (memberid = $1 + or $1 in ( + select creditid from schedule.show_credit + where show_id = ( + SELECT show_id FROM schedule.show_season_timeslot + JOIN schedule.show_season USING (show_season_id) + WHERE show_season_timeslot_id=ts.show_season_timeslot_id + ) + AND effective_from < (start_time + duration) + AND (effective_to IS NULL OR effective_to > start_time) + AND approvedid IS NOT NULL + )) + and start_time >= NOW() + order by start_time + limit $2', + [$memberid, $n] + ); + $results = []; + foreach ($ids as $id) { + $results[] = self::getInstance($id); + } + return $results; + } + + /** + * Returns the next $n timeslots for the current user. + * @param int $n + * @return self[] + */ + public static function getCurrentUserNextTimeslots(int $n = 10): array + { + return self::getUserNextTimeslots($_SESSION['memberid'], $n); + } + + /** + * Returns the last $n timeslots for the given user. + * @param int $n + * @return self[] + */ + public static function getUserPreviousTimeslots(int $memberid, int $n = 10): array + { + $ids = self::$db->fetchColumn( + 'select show_season_timeslot_id from schedule.show_season_timeslot ts + where (memberid = $1 + or $1 in ( + select creditid from schedule.show_credit + where show_id = ( + SELECT show_id FROM schedule.show_season_timeslot + JOIN schedule.show_season USING (show_season_id) + WHERE show_season_timeslot_id=ts.show_season_timeslot_id + ) + AND effective_from < (start_time + duration) + AND (effective_to IS NULL OR effective_to > start_time) + AND approvedid IS NOT NULL + )) + and start_time < NOW() + order by start_time desc + limit $2', + [$memberid, $n] + ); + $results = []; + foreach ($ids as $id) { + $results[] = self::getInstance($id); + } + return $results; + } + + /** + * Returns the last $n timeslots for the current user. + * @param int $n + * @return self[] + */ + public static function getCurrentUserPreviousTimeslots(int $n = 10): array + { + return self::getUserPreviousTimeslots($_SESSION['memberid'], $n); + } + /** * Deletes this Timeslot from the Schedule, and everything associated with it. - * - * + * * This is a proxy for several other methods, depending on the User and the current time:
    * (1) If the User has Cancel Show Privileges, then they can remove it at any time, notifying Creditors - * + * * (2) If the User is a Show Credit, and there are 48 hours or more until broadcast, they can remove it, * notifying the PC - * - * (3) If the User is a Show Credit, and there are less than 48 hours until broadcast, they can send a request to the - * PC for removal, and it will be flagged as hidden from the Schedule - it will still count as a noshow unless (1) occurs - * - * @param string $reason, Why the episode was cancelled. - * - * @todo Make the smarter - check if it's a programming team person, in which case just do this, if it's not - * then if >48hrs away just do it but email programming, but <48hrs should hide it but tell prog to confirm reason + * + * (3) If the User is a Show Credit, and there are less than 48 hours until broadcast, they can send a request to + * the PC for removal, and it will be flagged as hidden from the Schedule - it will still count as a noshow + * unless (1) occurs + * + * @param string $reason , Why the episode was cancelled. + * + * @todo Make the smarter - check if it's a programming team person, in which case just do this, if it's not then if + * >48hrs away just do it but email programming, but <48hrs should hide it but tell prog to confirm reason * @todo Response codes? i.e. error/db or error/403 etc */ - public function cancelTimeslot($reason) { - + public function cancelTimeslot($reason) + { + // If no active session we must have come through API so use admin + if (MyRadio_User::getCurrentUser() === null) { + $r = $this->cancelTimeslotAdmin($reason); + return $r; + } //Get if the User has permission to drop the episode if (MyRadio_User::getInstance()->hasAuth(AUTH_DELETESHOWS)) { //Yep, do an administrative drop $r = $this->cancelTimeslotAdmin($reason); - } - - //Get if the User is a Creditor - elseif ($this->getSeason()->getShow()->isCurrentUserAnOwner()) { + } elseif ($this->getSeason()->getShow()->isCurrentUserAnOwner()) { + //Get if the User is a Creditor //Yaay, depending on time they can do an self-service drop or cancellation request if ($this->getStartTime() > time() + (48 * 3600)) { //Self-service cancellation @@ -424,82 +959,149 @@ public function cancelTimeslot($reason) { } } else { //They can't do this. - return $r = false; + return false; } + return $r; } - private function cancelTimeslotAdmin($reason) { + private function cancelTimeslotAdmin($reason) + { $r = $this->deleteTimeslot(); - if (!$r) + if (!$r) { return false; + } - $email = "Hi #NAME, \r\n\r\n Please note that an episode of your show, " . $this->getMeta('title') . - ' has been cancelled by our Programming Team. The affected episode was at ' . CoreUtils::happyTime($this->getStartTime()); - $email .= "\r\n\r\nReason: $reason\r\n\r\nRegards\r\n" . Config::$long_name . " Programming Team"; + $email = "Hi #NAME, \r\n\r\n Please note that an episode of your show, " . $this->getMeta('title') + . ' has been cancelled by our Programming Team. The affected episode was at ' + . CoreUtils::happyTime($this->getStartTime()) + . "\r\n\r\n"; + $email .= "Reason: $reason\r\n\r\nRegards\r\n" . Config::$long_name . ' Programming Team'; self::$cache->purge(); - MyRadioEmail::sendEmailToUserSet($this->getSeason()->getShow()->getCreditObjects(), 'Episode of ' . $this->getMeta('title') . ' Cancelled', $email); - + MyRadioEmail::sendEmailToUserSet( + $this->getSeason()->getShow()->getCreditObjects(), + 'Episode of ' . $this->getMeta('title') . ' Cancelled', + $email + ); return true; } - private function cancelTimeslotSelfService($reason) { - + private function cancelTimeslotSelfService($reason) + { $r = $this->deleteTimeslot(); - if (!$r) + if (!$r) { return false; + } - $email1 = "Hi #NAME, \r\n\r\n You have requested that an episode of " . $this->getMeta('title') . - ' is cancelled. The affected episode was at ' . CoreUtils::happyTime($this->getStartTime()); - $email1 .= "\r\n\r\nReason: $reason\r\n\r\nRegards\r\n" . Config::$long_name . " Scheduler Robot"; - - $email2 = $this->getMeta('title') . ' on ' . CoreUtils::happyTime($this->getStartTime()) . ' was cancelled by a presenter because ' . $reason; - $email2 .= "\r\n\r\nIt was cancelled automatically as more than required notice was given."; - - MyRadioEmail::sendEmailToUserSet($this->getSeason()->getShow()->getCreditObjects(), 'Episode of ' . $this->getMeta('title') . ' Cancelled', $email1); - MyRadioEmail::sendEmailToList(MyRadio_List::getByName('programming'), 'Episode of ' . $this->getMeta('title') . ' Cancelled', $email2); - + $email1 = "Hi #NAME, \r\n\r\n You have requested that an episode of " . $this->getMeta('title') + . ' is cancelled. The affected episode was at ' . CoreUtils::happyTime($this->getStartTime()) + . "\r\n\r\n"; + $email1 .= "Reason: $reason\r\n\r\nRegards\r\n" . Config::$long_name . ' Scheduler Robot'; + + $email2 = $this->getMeta('title') + . ' on ' . CoreUtils::happyTime($this->getStartTime()) + . ' was cancelled by a presenter because ' . $reason + . "\r\n\r\n"; + $email2 .= "It was cancelled automatically as more than required notice was given."; + + MyRadioEmail::sendEmailToUserSet( + $this->getSeason()->getShow()->getCreditObjects(), + 'Episode of ' . $this->getMeta('title') . ' Cancelled', + $email1 + ); + MyRadioEmail::sendEmailToList( + MyRadio_List::getByName('programming'), + 'Episode of ' . $this->getMeta('title') . ' Cancelled', + $email2 + ); return true; } - private function cancelTimeslotRequest($reason) { - $email = $this->getMeta('title') . ' on ' . CoreUtils::happyTime($this->getStartTime()) . ' has requested cancellation because ' . $reason; - $email .= "\r\n\r\nDue to the short notice, it has been passed to you for consideration. To cancel the timeslot, visit "; - $email .= CoreUtils::makeURL('Scheduler', 'cancelEpisode', array('show_season_timeslot_id' => $this->getID(), 'reason' => base64_encode($reason))); - - MyRadioEmail::sendEmailToList(MyRadio_List::getByName('programming'), 'Show Cancellation Request', $email); - + private function cancelTimeslotRequest($reason) + { + $email = $this->getMeta('title') + . ' on ' . CoreUtils::happyTime($this->getStartTime()) + . ' has requested cancellation because ' . $reason + . "\r\n\r\n"; + $email .= "Due to the short notice, it has been passed to you for consideration. " + . "To cancel the timeslot, visit "; + $email .= URLUtils::makeURL( + 'Scheduler', + 'cancelEpisode', + ['show_season_timeslot_id' => $this->getID(), 'reason' => base64_encode($reason)] + ); + + MyRadioEmail::sendEmailToList(MyRadio_List::getByName('presenting'), 'Show Cancellation Request', $email); return true; } /** * Deletes the timeslot. Nothing else. See the cancelTimeslot... methods for recommended removal usage. + * * @return bool success/fail */ - private function deleteTimeslot() { - $r = self::$db->query('DELETE FROM schedule.show_season_timeslot WHERE show_season_timeslot_id=$1', array($this->getID())); + private function deleteTimeslot() + { + $r = self::$db->query( + 'DELETE FROM schedule.show_season_timeslot WHERE show_season_timeslot_id=$1', + [$this->getID()] + ); - /** - * @todo This is massively overkill, isn't it? - */ - $m = new Memcached(); - $m->addServer(Config::$django_cache_server, 11211); - $m->flush(); + $this->updateCacheObject(); + return $r; + } + /** + * Move this Timeslot to a new time. + * @param $newStart + * @param $newEnd + */ + public function moveTimeslot($newStart, $newEnd) + { + $oldStart = $this->getStartTime(); + $oldEnd = $this->getEndTime(); + + $r = self::$db->query( + 'UPDATE schedule.show_season_timeslot + SET start_time = $1, duration = $2 + WHERE show_season_timeslot_id = $3', + [ + CoreUtils::getTimestamp($newStart), + CoreUtils::makeInterval($newStart, $newEnd), + $this->getID() + ] + ); + + $email = "Hi #NAME, \r\n\r\n Please note that an episode of your show, " . $this->getMeta('title') + . ' has been moved by our Programming Team. The affected episode was at ' + . CoreUtils::happyTime($oldStart) . ' until ' . CoreUtils::happyTime($oldEnd) + . "\r\n" + . "It has been moved to " . CoreUtils::happyTime($newStart) . " until " . CoreUtils::happyTime($newEnd) + . "\r\n\r\n"; + $email .= "Regards\r\n" . Config::$long_name . ' Programming Team'; + + self::$cache->purge(); + MyRadioEmail::sendEmailToUserSet( + $this->getSeason()->getShow()->getCreditObjects(), + 'Episode of ' . $this->getMeta('title') . ' Moved', + $email + ); return $r; } /** - * This is the server-side implementation of the JSONON system for tracking Show Planner alterations - * @param array $set A JSONON operation set + * This is the server-side implementation of the JSONON system for tracking Show Planner alterations. + * + * @param array[] $set A JSONON operation set */ - public function updateShowPlan($set) { - $result = array(); + public function updateShowPlan($set) + { + $result = []; //Being a Database Transaction - this all succeeds, or none of it does self::$db->query('BEGIN'); - foreach ($set['ops'] as $op) { + foreach ($set as $op) { switch ($op['op']) { case 'AddItem': try { @@ -507,34 +1109,47 @@ public function updateShowPlan($set) { $parts = explode('-', $op['id']); if ($parts[0] === 'ManagedDB') { //This is a managed item - $i = NIPSWeb_TimeslotItem::create_managed($this->getID(), $parts[1], $op['channel'], $op['weight']); + $i = NIPSWeb_TimeslotItem::createManaged( + $this->getID(), + $parts[1], + $op['channel'], + $op['weight'] + ); } else { //This is a rec database track - $i = NIPSWeb_TimeslotItem::create_central($this->getID(), $parts[1], $op['channel'], $op['weight']); + $i = NIPSWeb_TimeslotItem::createCentral( + $this->getID(), + $parts[1], + $op['channel'], + $op['weight'] + ); } } catch (MyRadioException $e) { - $result[] = array('status' => false); + $result[] = ['status' => false]; self::$db->query('ROLLBACK'); + return $result; } - $result[] = array('status' => true, 'timeslotitemid' => $i->getID()); + $result[] = ['status' => true, 'timeslotitemid' => $i->getID()]; break; case 'MoveItem': if (!is_numeric($op['timeslotitemid'])) { - $result[] = array('status' => false); + $result[] = ['status' => false]; self::$db->query('ROLLBACK'); + return $result; } $i = NIPSWeb_TimeslotItem::getInstance($op['timeslotitemid']); if ($i->getChannel() != $op['oldchannel'] or $i->getWeight() != $op['oldweight']) { - $result[] = array('status' => false); + $result[] = ['status' => false]; self::$db->query('ROLLBACK'); + return $result; } else { $i->setLocation($op['channel'], $op['weight']); - $result[] = array('status' => true); + $result[] = ['status' => true]; } break; @@ -544,20 +1159,18 @@ public function updateShowPlan($set) { } $i = NIPSWeb_TimeslotItem::getInstance($op['timeslotitemid']); if ($i->getChannel() != $op['channel'] or $i->getWeight() != $op['weight']) { - $result[] = array('status' => false); + $result[] = ['status' => false]; self::$db->query('ROLLBACK'); + return $result; } else { $i->remove(); - $result[] = array('status' => true); + $result[] = ['status' => true]; } break; } } - self::$db->query('INSERT INTO bapsplanner.timeslot_change_ops (client_id, change_ops) - VALUES ($1, $2)', array($set['clientid'], json_encode($set['ops']))); - self::$db->query('COMMIT'); //Update the legacy baps show plans database @@ -566,68 +1179,204 @@ public function updateShowPlan($set) { return $result; } - private function updateLegacyShowPlan() { + private function updateLegacyShowPlan() + { NIPSWeb_BAPSUtils::saveListingsForTimeslot($this); } /** - * Returns the tracks etc. and their associated channels as planned for this show. Mainly used by NIPSWeb + * Returns the tracks etc. and their associated channels as planned for this show. Mainly used by NIPSWeb. */ - public function getShowPlan() { - /** + public function getShowPlan() + { + // Check we can access it, if not, require permission + if (!($this->isCurrentUserAnOwner())) { + AuthUtils::requirePermission(AUTH_VIEWMEMBERSHOWS); + } + + /* * Find out if there's a NIPSWeb Schema listing for this timeslot. * If not, throw back an empty array */ - $r = self::$db->query('SELECT timeslot_item_id, channel_id FROM bapsplanner.timeslot_items WHERE timeslot_id=$1 - ORDER BY weight ASC', array($this->getID())); + $q = 'SELECT timeslot_item_id, channel_id FROM bapsplanner.timeslot_items + WHERE timeslot_id=$1 + ORDER BY weight ASC'; - if (!$r or pg_num_rows($r) === 0) { + $r = self::$db->query($q, [$this->getID()]); + + if (!$r or self::$db->numRows($r) === 0) { //No show planned yet - return array(); + return []; } else { - $tracks = array(); - foreach (self::$db->fetch_all($r) as $track) { - $tracks[$track['channel_id']][] = NIPSWeb_TimeslotItem::getInstance($track['timeslot_item_id'])->toDataSource(); + $tracks = []; + foreach (self::$db->fetchAll($q, [$this->getID()]) as $track) { + $tracks[$track['channel_id']][] = + NIPSWeb_TimeslotItem::getInstance($track['timeslot_item_id'])->toDataSource(); } return $tracks; } } + /** + * Updates whether the user wants the timeslot to be played out automagically + * + * @param bool $playout + */ + public function setPlayout($playout) + { + self::$db->query( + "UPDATE schedule.show_season_timeslot SET playout = $1 WHERE show_season_timeslot_id = $2", + [$playout, $this->getID()] + ); + $this->playout = $playout; + $this->updateCacheObject(); + } + + public function getAutoVizConfig() + { + return MyRadio_AutoVizConfiguration::getConfigForTimeslot($this->timeslot_id); + } + + public function getAutoViz(): bool + { + return MyRadio_AutoVizConfiguration::getConfigForTimeslot($this->timeslot_id) !== null; + } + + public static function getNextAutovizTimeslots() + { + $data = self::getCurrentAndNextObjects(); + /** @type MyRadio_Timeslot[] */ + $timeslots = [$data['current'], ...($data['next'])]; + $timeslots = array_filter($timeslots, function ($ts) { + return $ts !== null && (!is_array($ts)); + }); + $result = []; + /** @var MyRadio_Timeslot $ts */ + foreach ($timeslots as $ts) { + $cfg = MyRadio_AutoVizConfiguration::getConfigForTimeslot($ts->timeslot_id); + if ($cfg !== null) { + $result[] = $cfg->toTask(); + } + } + return $result; + } + + /** + * Return location name + * + * @param $locationid int - The ID of the location + * @return string Location Name + * @throws MyRadioException + */ + public static function getLocationName($locationid) + { + self::wakeup(); + $result = self::$db->fetchOne( + "SELECT location_name FROM schedule.location + WHERE location_id = $1", + [$locationid] + ); + if (isset($result['location_name'])) { + return $result['location_name']; + } else { + throw new MyRadioException("The location with location_id $locationid doesn't exist.", 400); + } + } + /** * Get information about the Users signed into this Timeslot. + * + * @return array with the following keys: + * * signedby - who signed in the user - MyRadio_User|null + * * location - the name of the sign-in location + * * time - the sign-in time, as a UNIX timestamp + * * EITHER user - the member signed in (for members) + * * OR guest_info - the guest details given at sign-in (for guests) + * * @todo Cache this data? */ - public function getSigninInfo() { - $result = self::$db->fetch_all('SELECT * FROM (SELECT creditid AS memberid ' - . 'FROM schedule.show_credit WHERE show_id IN ' - . '(SELECT show_id FROM schedule.show_season WHERE show_season_id IN ' - . '(SELECT show_season_id FROM schedule.show_season_timeslot' - . ' WHERE show_season_timeslot_id=$1))' - . ' AND effective_from <= NOW()' - . ' AND (effective_to IS NULL OR effective_to > NOW())) AS t1 ' - . 'LEFT JOIN (SELECT memberid, signerid FROM sis2.member_signin ' - . 'WHERE show_season_timeslot_id=$1) AS t2 USING (memberid)', [$this->getID()]); - - return array_map(function($x) { - return ['user' => MyRadio_User::getInstance($x['memberid']), - 'signedby' => $x['signerid'] ? MyRadio_User::getInstance($x['signerid']) : null]; - }, $result); - } - - public function getMessages($offset = 0) { - $result = self::$db->fetch_all('SELECT c.commid AS id, - commtypeid AS type, - EXTRACT (EPOCH FROM date) AS time, - subject AS title, - content AS body, - (statusid = 2) AS read, - comm_source AS source - FROM sis2.messages c - INNER JOIN schedule.show_season_timeslot ts ON (c.timeslotid = ts.show_season_timeslot_id) - WHERE statusid <= 2 AND c.timeslotid = $1 - AND c.commid > $2 - ORDER BY c.commid ASC', [$this->getID(), $offset]); + public function getSigninInfo() + { + $result = self::$db->fetchAll( + 'SELECT * FROM ( + SELECT DISTINCT creditid AS memberid + FROM schedule.show_credit WHERE show_id IN ( + SELECT show_id FROM schedule.show_season + WHERE show_season_id IN ( + SELECT show_season_id FROM schedule.show_season_timeslot + WHERE show_season_timeslot_id=$1 + ) + ) + AND effective_from <= NOW() + AND (effective_to IS NULL OR effective_to > NOW()) + ) AS t1 + LEFT JOIN ( + SELECT memberid, signerid, location, sign_time FROM sis2.member_signin + WHERE show_season_timeslot_id=$1 + ) AS t2 USING (memberid) + LEFT JOIN schedule.location ON t2.location = location.location_id', + [$this->getID()] + ); + + $data = array_map( + function ($x) { + return [ + 'user' => MyRadio_User::getInstance($x['memberid']), + 'signedby' => $x['signerid'] ? MyRadio_User::getInstance($x['signerid']) : null, + 'location' => $x['location_name'], + 'time' => $x['sign_time'] !== null ? strtotime($x['sign_time']) : null + ]; + }, + $result + ); + + // Now handle guests + $result = self::$db->fetchAll( + 'SELECT signerid, guest_info, sign_time, location_id, location_name FROM sis2.guest_signin + INNER JOIN schedule.location ON guest_signin.location = location.location_id + WHERE show_season_timeslot_id=$1', + [$this->getID()] + ); + + $data = array_merge( + $data, + array_map( + function ($x) { + return [ + 'signedby' => $x['signerid'] ? MyRadio_User::getInstance($x['signerid']) : null, + 'location' => $x['location_name'], + 'guest_info' => $x['guest_info'], + 'time' => strtotime($x['sign_time']) + ]; + }, + $result + ) + ); + + return $data; + } + + public function getMessages($offset = 0) + { + if (!($this->getSeason()->getShow()->isCurrentUserAnOwner())) { + AuthUtils::requirePermission(AUTH_ANY_SHOW_MESSAGES); + } + $result = self::$db->fetchAll( + 'SELECT c.commid AS id, + commtypeid AS type, + EXTRACT (EPOCH FROM date) AS time, + subject AS title, + content AS body, + (statusid = 2) AS read, + comm_source AS source + FROM sis2.messages c + INNER JOIN schedule.show_season_timeslot ts ON (c.timeslotid = ts.show_season_timeslot_id) + WHERE statusid <= 2 AND c.timeslotid = $1 + AND c.commid > $2 + ORDER BY c.commid ASC', + [$this->getID(), $offset] + ); foreach ($result as $k => $v) { $result[$k]['read'] = ($v['read'] === 't'); @@ -637,19 +1386,173 @@ public function getMessages($offset = 0) { if ($v['type'] == 3) { $result[$k]['location'] = SIS_Utils::ipLookup($v['source']); } + $result[$k]['title'] = htmlspecialchars($v['title']); + $result[$k]['body'] = htmlspecialchars($v['body']); } + return $result; } /** - * Signs the given user into the timeslot to say they were on air at this time. - * + * Sends a message to the timeslot for display in SIS. + * + * @param string $message the message to be sent + * @return MyRadio_Timeslot + */ + public function sendMessage($message) + { + $message = trim($message); + + if (empty($message)) { + throw new MyRadioException('Message is empty.', 400); + } + + $junk = SIS_Utils::checkMessageSpam($message); + $warning = SIS_Utils::checkMessageSocialEngineering($message); + + if ($warning !== false) { + $prefix = '

    ' . $warning . '

    '; + } else { + $prefix = ''; + } + + $source = $_SERVER['REMOTE_ADDR']; + + self::$db->query( + 'INSERT INTO sis2.messages (timeslotid, commtypeid, sender, subject, content, statusid, comm_source) + VALUES ($1, $2, $3, $4, $5, $6, $7)', + [ + $this->getID(), // timeslot + 3, // commtypeid : website + 'MyRadio', // sender + substr($message, 0, 144), // subject : trancated message + $prefix . $message, // content : message with prefix + $junk ? 4 : 1, // statusid : junk or unread + $source, // comm_source : IP + ] + ); + + return $this; + } + + /** + * Signs the given user into the timeslot to say they were + * on air at this time, if they haven't been signed in already. + * * @param MyRadio_User $member + * @param int|string $locationid */ - public function signIn(MyRadio_User $member) { - self::$db->query('INSERT INTO sis2.member_signin' - . ' (show_season_timeslot_id, memberid, signerid)' - . ' VALUES ($1, $2, $3)', [$this->getID(), $member->getID(), MyRadio_User::getInstance()->getID()]); + public function signIn(MyRadio_User $member, $locationid) + { + // If member already signed in for whatever reason, don't bother trying again. + $signedIn = !empty(self::$db->fetchOne( + 'SELECT * FROM sis2.member_signin + WHERE show_season_timeslot_id=$1 AND memberid=$2', + [$this->getID(), $member->getID()] + )); + if (!$signedIn) { + self::$db->query( + 'INSERT INTO sis2.member_signin (show_season_timeslot_id, memberid, signerid, location) + VALUES ($1, $2, $3, $4)', + [$this->getID(), $member->getID(), MyRadio_User::getInstance()->getID(), $locationid] + ); + } + } + + public function signInGuests(string $guestInfo, $locationid) + { + self::$db->query( + 'INSERT INTO sis2.guest_signin (show_season_timeslot_id, signerid, location, guest_info) + VALUES ($1, $2, $3, $4)', + [ + $this->getID(), + MyRadio_User::getInstance()->getID(), + $locationid, + htmlspecialchars($guestInfo, ENT_QUOTES) + ] + ); + } + + public function toIcalEvent(): Event + { + return Event::create($this->getMeta('title')) + ->startsAt((new DateTime())->setTimestamp($this->getStartTime())) + ->endsAt((new DateTime())->setTimestamp($this->getEndTime())) + ->description(html_entity_decode(strip_tags($this->getMeta('description')))); } + public static function getCancelForm() + { + return (new MyRadioForm( + 'sched_cancel', + 'Scheduler', + 'cancelEpisode', + [ + 'debug' => false, + 'title' => 'Cancel Episode', + ] + ) + )->addField( + new MyRadioFormField( + 'reason', + MyRadioFormField::TYPE_BLOCKTEXT, + ['label' => 'Please explain why this Episode should be removed from the Schedule'] + ) + )->addField( + new MyRadioFormField( + 'show_season_timeslot_id', + MyRadioFormField::TYPE_HIDDEN, + ['value' => $_REQUEST['show_season_timeslot_id']] + ) + ); + } + + public function getMoveForm() + { + $title = $this->getMeta('title') . ' - ' . CoreUtils::happyTime($this->getStartTime()); + return (new MyRadioForm( + 'sched_move', + 'Scheduler', + 'moveEpisode', + [ + 'debug' => false, + 'title' => 'Move Episode', + 'subtitle' => "Moving $title" + ] + ))->addField(new MyRadioFormField( + 'grp_info', + MyRadioFormField::TYPE_SECTION, + [ + 'label' => 'New Time', + 'explanation' => 'Enter the new time to move the episode to. Take care with the end time.' + ] + ))->addField(new MyRadioFormField( + 'new_start_time', + MyRadioFormField::TYPE_DATETIME, + [ + 'label' => 'New Start Time', + 'value' => date('d/m/Y H:i', $this->getStartTime()) + ] + ))->addField(new MyRadioFormField( + 'new_end_time', + MyRadioFormField::TYPE_DATETIME, + [ + 'label' => 'New End Time', + 'value' => date('d/m/Y H:i', $this->getEndTime()) + ] + ))->addField(new MyRadioFormField( + 'grp_info_close', + MyRadioFormField::TYPE_SECTION_CLOSE, + [] + ))->addField(new MyRadioFormField( + 'show_season_timeslot_id', + MyRadioFormField::TYPE_HIDDEN, + ['value' => $this->getID()] + )); + } + + public static function getGraphQLTypeName() + { + return 'Timeslot'; + } } diff --git a/src/Classes/ServiceAPI/MyRadio_Track.php b/src/Classes/ServiceAPI/MyRadio_Track.php index 5a33cb6cc..12d2a4dc6 100644 --- a/src/Classes/ServiceAPI/MyRadio_Track.php +++ b/src/Classes/ServiceAPI/MyRadio_Track.php @@ -1,322 +1,803 @@ - * @package MyRadio_Core - * @uses \Database - * @todo Cache this + * The MyRadio_Track class provides and stores information about a Track. + * + * @uses \Database */ -class MyRadio_Track extends ServiceAPI { +class MyRadio_Track extends ServiceAPI +{ /** - * The number of the Track on a Record + * The number of the Track on a Record. + * * @var int */ private $number; /** - * The title of the Track - * @var String + * The title of the Track. + * + * @var string */ private $title; /** - * The Artist of the Track + * The Artist of the Track. + * * @var int */ private $artist; /** - * The length of the Track, in seconds + * The length of the Track, in seconds. + * * @var int */ private $length; /** * Don't use this. + * * @deprecated - * @var String + * + * @var string */ private $duration; /** - * The genreid of the Track + * The genreid of the Track. + * * @var char */ private $genre; /** - * How long the intro (non-vocal) part of the track is, in seconds + * How long the intro (non-vocal) part of the track is, in seconds. + * * @var int */ private $intro; + /** + * The start time of an ending segment to the track, in seconds. + * + * @var int + */ + private $outro; + /** * Whether the track is clean:
    * y: The track is verified as clean
    * n: The track is verified as unclean
    - * u: This track has not been checked for cleanliness - * @var String + * u: This track has not been checked for cleanliness. + * + * @var string */ private $clean; /** - * The Unique ID of this Track + * The Unique ID of this Track. + * * @var int */ private $trackid; /** - * The Record this track belongs to + * The Record this track belongs to. + * * @var int */ private $record; /** - * Whether or not there is a digital version of this track stored in the Central Database + * Whether or not there is a digital version of this track stored in the Central Database. + * * @var bool */ private $digitised; /** - * The member who digitised this track + * The member who digitised this track. + * * @var int */ private $digitisedby; + /** + * The time when this track was last edited. + * + * @var int + */ + private $last_edited_time; + + /** + * The member who last edited this track. + * + * @var int + */ + private $last_edited_memberid; + /** * Caches Last.fm's Track.getSimilar response. - * @var Array + * + * @var array */ private $lastfm_similar; /** - * Whether this track is iTones blacklisted + * Whether this track is iTones blacklisted. */ private $itones_blacklist = null; /** - * Initiates the Track variables - * @param int $trackid The ID of the track to initialise + * Initiates the Track variables. + * + * @param array $result + * artist string + * clean char y/n/u + * digitised bool + * digitisedby int + * genre int + * intro string HH:ii:ss + * outro string HH:ii:ss + * length string HH:ii:ss + * duration int + * number int + * recordid int + * title string + * * @todo Genre class * @todo Artist normalisation */ - protected function __construct($trackid) { - - $this->trackid = (int) $trackid; - $result = self::$db->fetch_one('SELECT * FROM public.rec_track WHERE trackid=$1 LIMIT 1', array($this->trackid)); - if (empty($result)) { - throw new MyRadioException('The specified Track does not seem to exist'); - return; - } - + protected function __construct($result) + { + $this->trackid = (int) $result['trackid']; $this->artist = $result['artist']; $this->clean = $result['clean']; $this->digitised = ($result['digitised'] == 't') ? true : false; - $this->digitisedby = empty($result['digitisedby']) ? null : (int) $result['digitisedby']; + $this->digitisedby = empty($result['digitisedby']) ? + null : (int) $result['digitisedby']; + $this->last_edited_time = empty($result['last_edited_time']) ? + null : $result['last_edited_time']; + $this->last_edited_memberid = empty($result['last_edited_memberid']) ? + null : (int) $result['last_edited_memberid']; $this->genre = $result['genre']; - $this->intro = strtotime('1970-01-01 ' . $result['intro'] . '+00'); + $this->intro = strtotime('1970-01-01 '.$result['intro'].'+00'); + $this->outro = strtotime('1970-01-01 '.$result['outro'].'+00'); $this->length = $result['length']; $this->duration = (int) $result['duration']; - $this->number = (int) $result['intro']; + $this->number = (int) $result['number']; $this->record = (int) $result['recordid']; $this->title = $result['title']; } - private function updateCachedObject() { - self::$cache->set(self::getCacheKey($this->getID()), $this, Config::$cache_track_timeout); + /** + * @throws MyRadioException if the track does not exist + * + * @return MyRadio_Track + */ + protected static function factory($trackid) + { + $sql = 'SELECT * FROM public.rec_track WHERE trackid=$1 LIMIT 1'; + $result = self::$db->fetchOne($sql, [$trackid]); + + if (empty($result)) { + throw new MyRadioException('The specified Track does not seem to exist', 404); + } + + return new self($result); } + public static function getForm() + { + return ( + new MyRadioForm( + 'lib_edittrack', + 'Library', + 'editTrack', + [ + 'title' => 'Edit Track', + ] + ) + )->addField( + new MyRadioFormField('title', MyRadioFormField::TYPE_TEXT, ['label' => 'Title']) + )->addField( + new MyRadioFormField('artist', MyRadioFormField::TYPE_TEXT, ['label' => 'Artist']) + )->addField( + new MyRadioFormField( + 'album', + MyRadioFormField::TYPE_ALBUM, + [ + 'label' => 'Album', + 'explanation' => 'This must be an existing album in our system.' + ] + ) + )->addField( + new MyRadioFormField( + 'position', + MyRadioFormField::TYPE_NUMBER, + [ + 'label' => 'Position', + 'explanation' => 'The track number on the album.' + ] + ) + )->addField( + new MyRadioFormField( + 'intro', + MyRadioFormField::TYPE_NUMBER, + [ + 'label' => 'Intro', + 'explanation' => 'The track intro end time in seconds.' + ] + ) + )->addField( + new MyRadioFormField( + 'outro', + MyRadioFormField::TYPE_NUMBER, + [ + 'label' => 'Outro', + 'explanation' => 'The track outro start time in seconds.' + ] + ) + )->addField( + new MyRadioFormField( + 'clean', + MyRadioFormField::TYPE_SELECT, + [ + 'options' => array_merge( + [['text' => 'Please select...', 'disabled' => true]], + self::getCleanOptions() + ), + 'label' => 'Clean/Explicit/Unknown' + ] + ) + )->addField( + new MyRadioFormField( + 'genre', + MyRadioFormField::TYPE_SELECT, + [ + 'options' => array_merge( + [['text' => 'Please select...', 'disabled' => true]], + self::getGenres() + ), + 'label' => 'Genre' + ] + ) + )->addField( + new MyRadioFormField( + 'digitised', + MyRadioFormField::TYPE_CHECK, + [ + 'label' => 'Digitised', + 'required' => false + ] + ) + )->addField( + new MyRadioFormField( + 'digitisedby', + MyRadioFormField::TYPE_MEMBER, + [ + 'label' => 'Digitised By', + 'explanation' => 'The person who uploaded the track.', + 'enabled' => false, + 'required' => false + ] + ) + )->addField( + new MyRadioFormField( + 'blacklisted', + MyRadioFormField::TYPE_CHECK, + [ + 'label' => 'Blacklisted', + 'required' => false, + 'explanation' => 'If the track is banned from playing on Jukebox.' + ] + ) + )->addField( + new MyRadioFormField( + 'last_edited_separator', + MyRadioFormField::TYPE_SECTION, + [ + 'label' => 'Edit History' + ] + ) + )->addField( + new MyRadioFormField( + 'last_edited_time', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Last Edited', + 'required' => false, + 'explanation' => 'The time someone last submitted this form for this track.', + 'enabled' => false + ] + ) + )->addField( + new MyRadioFormField( + 'last_edited_memberid', + MyRadioFormField::TYPE_MEMBER, + [ + 'label' => 'Last Edited By', + 'required' => false, + 'explanation' => 'The member that last submitted this form for this track.', + 'enabled' => false + ] + ) + ); + } + public function getEditForm() + { + return self::getForm() + ->editMode( + $this->getID(), + [ + 'title' => $this->getTitle(), + 'artist' => $this->getArtist(), + 'album' => $this->getAlbum(), + 'position' => $this->getPosition(), + 'intro' => $this->getIntro(), + 'outro' => $this->getOutro(), + 'clean' => $this->getClean(), + 'genre' => $this->getGenre(), + 'digitised' => $this->getDigitised(), + 'digitisedby' => $this->getDigitisedBy(), + 'blacklisted' => $this->isBlacklisted(), + 'last_edited_time' => $this->getLastEditedTime() === null ? null : + CoreUtils::happyTime($this->getLastEditedTime()), + 'last_edited_memberid' => $this->getLastEditedMemberID(), + ] + ); + } + + /** * Returns a "summary" string - the title and artist seperated with a dash. - * @return String + * + * @return string */ - public function getSummary() { - return $this->getTitle() . ' - ' . $this->getArtist(); + public function getSummary() + { + return $this->getTitle().' - '.$this->getArtist(); } /** - * Get the Title of the Track - * @return String + * Get the Title of the Track. + * + * @return string */ - public function getTitle() { + public function getTitle() + { return $this->title; } /** - * Get the Artist of the Track - * @return String + * Get the Artist of the Track. + * + * @return string */ - public function getArtist() { + public function getArtist() + { return $this->artist; } /** - * Get the Album of the Track; + * Get the Album of the Track. + * * @return Album */ - public function getAlbum() { + public function getAlbum() + { return MyRadio_Album::getInstance($this->record); } /** - * Get whether the track is clean + * Get the intro duration of the Track, in seconds. + * + * @return int + */ + public function getIntro() + { + return $this->intro; + } + + /** + * Get the outro start-time of the Track, in seconds. + * + * @return int + */ + public function getOutro() + { + return $this->outro; + } + + /** + * Get whether the track is clean. + * * @return char */ - public function getClean() { + public function getClean() + { return $this->clean; } /** - * Get the unique trackid of the Track + * Get the unique trackid of the Track. + * * @return int */ - public function getID() { + public function getID() + { return $this->trackid; } /** - * Get the length of the Track, in hours:minutes:seconds + * Get the length of the Track, in hours:minutes:seconds. + * * @return string */ - public function getLength() { + public function getLength() + { return $this->length; } /** - * Get the duration of the Track, in seconds + * Get the duration of the Track, in seconds. + * * @return int */ - public function getDuration() { + public function getDuration() + { return $this->duration; } /** - * Get whether or not the track is digitised + * Get the genre of the Track. + * + * @return char + */ + public function getGenre() + { + return $this->genre; + } + + /** + * Get whether or not the track is digitised. + * * @return bool */ - public function getDigitised() { + public function getDigitised() + { return $this->digitised; } - public function getDigitisedBy() { - if ($this->digitisedby === null) - return null; - else + /** + * Get the user who digitised the track + * @param MyRadio_User $digitisedby The user who digitised the track. + */ + public function getDigitisedBy() + { + if ($this->digitisedby === null) { + return; + } else { return MyRadio_User::getInstance($this->digitisedby); + } + } + + /** + * Get the last time a user edited the track. + * + * @return bool + */ + public function getLastEditedTime() + { + return $this->last_edited_time; + } + + /** + * Get the user who last edited the track. + * + * @return MyRadio_User $last_edited_memberid The user who last edited the track. + */ + public function getLastEditedMemberID() + { + if ($this->last_edited_memberid === null) { + return; + } else { + return MyRadio_User::getInstance($this->last_edited_memberid); + } } /** - * Update whether or not the track is digitised + * Update whether or not the track is digitised. + * + * @param bool $digitised */ - public function setDigitised($digitised) { + public function setDigitised($digitised) + { $this->digitised = $digitised; - self::$db->query('UPDATE rec_track SET digitised=$1, digitisedby=$2 WHERE trackid=$3', $digitised ? array( - 't', $_SESSION['memberid'], $this->getID() - ) : array( - 'f', null, $this->getID() - ) + self::$db->query( + 'UPDATE rec_track SET digitised=$1 WHERE trackid=$2', + $digitised ? ['t', $this->getID()] : ['f', $this->getID()] + ); + $this->updateCacheObject(); + } + + /** + * Update the user who digitised the track + * @param MyRadio_User $digitisedby The user who digitised the track. + */ + public function setDigitisedBy($digitisedby) + { + $this->digitisedby = $digitisedby->getID(); + self::$db->query( + 'UPDATE rec_track SET digitisedby=$1 WHERE trackid=$2', + [$digitisedby->getID(), $this->getID()] ); - $this->updateCachedObject(); + $this->updateCacheObject(); } /** - * Update whether or not the track is clean + * Update when a user last edited the track info. + * + * @param bool $digitised */ - public function setClean($clean) { + public function setLastEdited() + { + $this->last_edited_time = CoreUtils::getTimestamp(); + $this->last_edited_memberid = $_SESSION['memberid']; + self::$db->query( + 'UPDATE rec_track SET last_edited_time=$1, last_edited_memberid=$2 WHERE trackid=$3', + [ + $this->last_edited_time, + $this->last_edited_memberid, + $this->getID() + ] + ); + $this->updateCacheObject(); + } + + /** + * Update whether or not the track is clean. + */ + public function setClean($clean) + { $this->clean = $clean; self::$db->query('UPDATE rec_track SET clean=$1 WHERE trackid=$2', [$clean, $this->getID()]); - $this->updateCachedObject(); + $this->updateCacheObject(); } /** - * Returns an array of key information, useful for Twig rendering and JSON requests + * Returns an array of key information, useful for Twig rendering and JSON requests. + * @param array $mixins Mixins. Currently unused. + * @return array * @todo Expand the information this returns - * @return Array */ - public function toDataSource() { - return array( + public function toDataSource($mixins = []) + { + return [ 'title' => $this->getTitle(), 'artist' => $this->getArtist(), 'type' => 'central', //Tells NIPSWeb Client what this item type is 'album' => $this->getAlbum()->toDataSource(), 'trackid' => $this->getID(), 'length' => $this->getLength(), + 'intro' => $this->getIntro(), + 'outro' => $this->getOutro(), 'clean' => $this->clean !== 'n', 'digitised' => $this->getDigitised(), - 'editlink' => array( + 'editlink' => [ 'display' => 'icon', - 'value' => 'script', + 'value' => 'pencil', 'title' => 'Edit Track', - 'url' => CoreUtils::makeURL('Library', 'editTrack', array('trackid' => $this->getID())) - ), - 'deletelink' => array( + 'url' => URLUtils::makeURL('Library', 'editTrack', ['trackid' => $this->getID()]), + ], + 'deletelink' => [ 'display' => 'icon', 'value' => 'trash', 'title' => 'Delete (Undigitise) Track', - 'url' => CoreUtils::makeURL('Library', 'deleteTrack', array('trackid' => $this->getID())) - ) + 'url' => URLUtils::makeURL('Library', 'deleteTrack', ['trackid' => $this->getID()]), + ], + ]; + } + + /** + * Report this track as explicit. + * + * Do not confuse with {@link setClean} - this method will set it as explicit, and send + * an email to various people informing them of it. If you don't want that to happen, + * don't use this. + */ + public function reportExplicit() + { + if ($this->getClean() === 'n') { + throw new MyRadioException('This is already marked explicit.', 400); + } + + $this->setClean('n'); + + $currentUser = MyRadio_User::getCurrentOrSystemUser(); + + $title = htmlspecialchars($this->getTitle()); + $artist = htmlspecialchars($this->getArtist()); + $userName = htmlspecialchars($currentUser->getFName() . ' ' . $currentUser->getSName()); + $editUrl = URLUtils::makeURL('Library', 'editTrack', ['trackid' => $this->getID()]); + MyRadioEmail::sendEmailToList( + MyRadio_List::getByName('playlisting'), + 'Track Reported Explicit', + <<here
    . + +Thanks, +MyRadio Music Library Robot +EOF + , + null ); } /** - * Returns an Array of Tracks matching the given partial title - * @param String $title A partial or total title to search for - * @param String $artist a partial or total title to search for - * @param int $limit The maximum number of tracks to return - * @param bool $digitised Whether the track must be digitised. Default false. - * @param bool $exact Only return Exact matches (i.e. no %) - * @return Array of Track objects + * Returns an Array of Tracks matching the given partial title. + * + * @param string $title A partial or total title to search for + * @param string $artist a partial or total title to search for + * @param int $limit The maximum number of tracks to return + * @param bool $digitised Whether the track must be digitised. Default false. + * @param bool $exact Only return Exact matches (i.e. no %) + * + * @return array of Track objects */ - private static function findByNameArtist($title, $artist, $limit, $digitised = false, $exact = false) { - $result = self::$db->fetch_column('SELECT trackid - FROM rec_track, rec_record WHERE rec_track.recordid=rec_record.recordid - AND rec_track.title ' . ($exact ? '=$1' : 'ILIKE \'%\' || $1 || \'%\'') . - 'AND rec_track.artist ' . ($exact ? '=$2' : 'ILIKE \'%\' || $1 || \'%\'') . - ($digitised ? ' AND digitised=\'t\'' : '') . ' - LIMIT $3', array($title, $artist, $limit)); + private static function findByNameArtist($title, $artist, $limit, $digitised = false, $exact = false) + { + if ($exact) { + $result = self::$db->fetchColumn( + 'SELECT trackid + FROM rec_track + WHERE title=$1 AND artist=$2' + .($digitised ? ' AND digitised=\'t\'' : '') + .' LIMIT $3', + [$title, $artist, $limit] + ); + } else { + $opts = [$title, $limit]; + if ($artist) { + $opts[] = $artist; + } + $result = self::$db->fetchColumn( + 'SELECT trackid FROM ( + SELECT DISTINCT trackid, priority FROM + ( + ( + SELECT trackid, 1 AS priority + FROM rec_track WHERE title ILIKE $1' + .($artist ? ' AND artist=$3' : '') + .($digitised ? ' AND digitised=\'t\'' : '').' + ) UNION ( + SELECT trackid, 2 AS priority + FROM rec_track WHERE title ILIKE $1 || \'%\'' + .($artist ? ' AND artist ILIKE $3 || \'%\'' : '') + .($digitised ? ' AND digitised=\'t\'' : '').' + ) UNION ( + SELECT trackid, 3 AS priority + FROM rec_track WHERE title ILIKE \'%\' || $1 || \'%\'' + .($artist ? ' AND artist ILIKE \'%\' || $3 || \'%\'' : '') + .($digitised ? ' AND digitised=\'t\'' : '').' + ) + ) AS t1 + ) As t2 + ORDER BY priority LIMIT $2', + $opts + ); + } - $response = array(); - foreach ($result as $trackid) { - $response[] = new MyRadio_Track($trackid); + return self::resultSetToObjArray(array_unique($result)); + } + + /** + * Search for tracks in the library. + * + * @param string $title Only return tracks matching this title + * @param string $artist Only return tracks matching this artist + * @param integer $recordid Only return tracks in this album + * @param boolean $digitised Only return tracks that are digitised. If false, return any. Default true. + * @param enum $clean Only return tracks with the given cleanliness (y = clean, n = explicit, u = unknown) + * @param boolean $precise Only return exact matches for title and artist. Defaults to fuzzy search. + * @param integer $limit Search only returns the default config number of results by default, this overrides that. + * @param enum $sort Sort order. Possible values: "id" (default), "title", "random". Random will not paginate well. + * @param string $itonesplaylistid Managed playlist id to return, + * for example 'breakfast' will return all tracks from the breakfast playlist. + */ + public static function search( + $title = null, + $artist = null, + $recordid = null, + $digitised = true, + $clean = null, + $precise = false, + $limit = null, + $sort = null, + $itonesplaylistid = null + ) { + if ($clean !== null && $clean !== 'u' && $clean !== 'y' && $clean !== 'n') { + throw new MyRadioException('Valid values for clean are u, y and n.'); + } + + if ($sort !== null && $sort !== 'id' && $sort !== 'title' && $sort !== 'random') { + throw new MyRadioException('Valid values for sort are id, title and random.'); } - return $response; + $options = [ + 'title' => $title, + 'artist' => $artist, + 'recordid' => empty($recordid) ? null : (int)$recordid, + 'digitised' => filter_var($digitised, FILTER_VALIDATE_BOOLEAN), + 'clean' => $clean, + 'precise' => filter_var($precise, FILTER_VALIDATE_BOOLEAN), + 'itonesplaylistid' => $itonesplaylistid + ]; + if ($limit != null) { + $options['limit'] = $limit; + } + if ($sort === 'id') { + $options['idsort'] = true; + } + if ($sort === 'title') { + $options['titlesort'] = true; + } + if ($sort === 'random') { + $options['random'] = true; + } + + return self::findByOptions($options); } /** - * - * @param Array $options One or more of the following: - * title: String title of the track - * artist: String artist name of the track - * digitised: If true, only return digitised tracks. If false, return any. - * itonesplaylistid: Tracks that are members of the iTones_Playlist id - * limit: Maximum number of items to return. 0 = No Limit - * recordid: int Record id - * lastfmverified: Boolean whether or not verified with Last.fm Fingerprinter. Default any. - * random: If true, sort randomly - * idsort: If true, sort by trackid - * custom: A custom SQL WHERE clause - * precise: If true, will only return exact matches for artist/title - * nocorrectionproposed: If true, will only return items with no correction proposed. - * clean: Default any. 'y' for clean tracks, 'n' for dirty, 'u' for unknown. - * + * Not for use via the Swagger API. See /track/search instead. + * + * @swagger ignore + * @param array $options One or more of the following: + * title: String title of the track + * artist: String artist name of the track + * digitised: If true, only return digitised tracks. If false, return any. + * itonesplaylistid: Tracks that are members of the iTones_Playlist id + * limit: Maximum number of items to return. 0 = No Limit. start,limit can also be used. + * recordid: int Record id + * lastfmverified: Boolean whether or not verified with Last.fm Fingerprinter. Default any. + * random: If true, sort randomly + * idsort: If true, sort by trackid (default) + * titlesort: If true, sort by title + * custom: A custom SQL WHERE clause + * precise: If true, will only return exact matches for artist/title(/album if specified) + * nocorrectionproposed: If true, will only return items with no correction proposed. + * clean: Default any. 'y' for clean tracks, 'n' for dirty, 'u' for unknown. + * * @todo Limit not accurate for itonesplaylistid queries */ - public static function findByOptions($options) { + public static function findByOptions($options) + { self::wakeup(); //Shortcircuit - if itonesplaylistid is the only not-default value, just return the playlist $conflict = false; - foreach (array('title', 'artist', 'digitised') as $k) { + foreach (['title', 'artist', 'digitised'] as $k) { if (!empty($options[$k])) { $conflict = true; break; @@ -326,7 +807,25 @@ public static function findByOptions($options) { if (!$conflict && !empty($options['itonesplaylistid'])) { return iTones_Playlist::getInstance($options['itonesplaylistid'])->getTracks(); } - + if (isset($options['random']) && isset($options['titlesort'])) { + if (!$options['random'] && !$options['titlesort']) { + $options['idsort'] = true; + } + } elseif (isset($options['random'])) { + if (!$options['random']) { + $options['idsort'] = true; + $options['titlesort'] = false; + } + } elseif (isset($options['titlesort'])) { + if (!$options['titlesort']) { + $options['idsort'] = true; + $options['random'] = false; + } + } else { + $options['idsort'] = true; + $options['random'] = false; + $options['titlesort'] = false; + } if (empty($options['title'])) { $options['title'] = ''; } @@ -360,6 +859,9 @@ public static function findByOptions($options) { if (empty($options['idsort'])) { $options['idsort'] = null; } + if (empty($options['titlesort'])) { + $options['titlesort'] = null; + } if (empty($options['custom'])) { $options['custom'] = null; } @@ -373,234 +875,368 @@ public static function findByOptions($options) { $options['clean'] = false; } + //Shortcircuit - there's a far simpler and more accurate method + // if there's only title/artist/digitised/limit + if (!$options['itonesplaylistid'] + && !$options['recordid'] + && !$options['lastfmverified'] + && !$options['random'] + && $options['idsort'] + && !$options['custom'] + && !$options['nocorrectionproposed'] + && !$options['clean'] + ) { + return self::findByNameArtist( + $options['title'], + $firstop === 'OR' ? null : $options['artist'], + $options['limit'], + $options['digitised'], + $options['precise'] + ); + } + //Prepare paramaters - $sql_params = array($options['title'], $options['artist'], $options['album'], $options['precise'] ? '' : '%'); - $count = 4; + $sql_params = [$options['precise'] ? '' : '%', $options['title'], $options['artist']]; + $count = 3; + if ($options['album']) { + $sql_params[] = $options['album']; + ++$count; + $album_param = $count; + } if ($options['limit'] != 0) { $sql_params[] = $options['limit']; - $count++; + ++$count; $limit_param = $count; } if ($options['clean']) { $sql_params[] = $options['clean']; - $count++; + ++$count; $clean_param = $count; } //Do the bulk of the sorting with SQL - $result = self::$db->fetch_all('SELECT trackid, rec_track.recordid - FROM rec_track, rec_record WHERE rec_track.recordid=rec_record.recordid - AND (rec_track.title ILIKE $4 || $1 || $4 - ' . $firstop . ' rec_track.artist ILIKE $4 || $2 || $4) - AND rec_record.title ILIKE $4 || $3 || $4 - ' . ($options['digitised'] ? ' AND digitised=\'t\'' : '') . ' - ' . ($options['lastfmverified'] === true ? ' AND lastfm_verified=\'t\'' : '') - . ($options['lastfmverified'] === false ? ' AND lastfm_verified=\'f\'' : '') - . ($options['nocorrectionproposed'] === true ? ' AND trackid NOT IN ( - SELECT trackid FROM public.rec_trackcorrection WHERE state=\'p\' - )' : '') - . ($options['clean'] != null ? ' AND clean=$' . $clean_param : '') - . ($options['custom'] !== null ? ' AND ' . $options['custom'] : '') - . ($options['random'] ? ' ORDER BY RANDOM()' : '') - . ($options['idsort'] ? ' ORDER BY trackid' : '') - . ($options['limit'] == 0 ? '' : ' LIMIT $' . $limit_param), $sql_params); - - $response = array(); + $result = self::$db->fetchAll( + 'SELECT trackid, rec_track.recordid + FROM rec_track, rec_record WHERE rec_track.recordid=rec_record.recordid + AND (rec_track.title ILIKE $1 || $2 || $1' + .' ' .$firstop + .' rec_track.artist ILIKE $1 || $3 || $1)' + .($options['album'] ? ' AND rec_record.title ILIKE $1 || $'.$album_param.' || $1' : '') + .($options['digitised'] ? ' AND digitised=\'t\'' : '') + .($options['lastfmverified'] === true ? ' AND lastfm_verified=\'t\'' : '') + .($options['lastfmverified'] === false ? ' AND lastfm_verified=\'f\'' : '') + .($options['nocorrectionproposed'] === true ? ' AND trackid NOT IN ( + SELECT trackid FROM public.rec_trackcorrection WHERE state=\'p\')' : '') + .($options['clean'] != null ? ' AND clean=$'.$clean_param : '') + .($options['custom'] !== null ? ' AND '.$options['custom'] : '') + .($options['random'] ? ' ORDER BY RANDOM()' : '') + .($options['idsort'] ? ' ORDER BY trackid' : '') + .($options['titlesort'] ? ' ORDER BY rec_track.title' : '') + .($options['limit'] == 0 ? '' : ' LIMIT $'.$limit_param), + $sql_params + ); + + $response = []; foreach ($result as $trackid) { if ($options['recordid'] !== null && $trackid['recordid'] != $options['recordid']) { continue; } - $response[] = new MyRadio_Track($trackid['trackid']); + $response[] = self::getInstance($trackid['trackid']); } //Intersect with iTones if necessary, then return - return empty($options['itonesplaylistid']) ? $response : - array_intersect($response, iTones_Playlist::getInstance($options['itonesplaylistid']) - ->getTracks()); + return empty($options['itonesplaylistid']) ? + $response : + array_intersect($response, iTones_Playlist::getInstance($options['itonesplaylistid'])->getTracks()); } /** * This method processes an unknown mp3 file that has been uploaded, storing a temporary copy of the file in /tmp/, * then attempting to identify the track by querying it against the last.fm database. - * + * * @param type $tmp_path */ - public static function cacheAndIdentifyUploadedTrack($tmp_path) { - if (!isset($_SESSION['myury_nipsweb_file_cache_counter'])) { - $_SESSION['myury_nipsweb_file_cache_counter'] = 0; + public static function cacheAndIdentifyUploadedTrack($tmp_path) + { + if (!isset($_SESSION['myradio_nipsweb_file_cache_counter'])) { + $_SESSION['myradio_nipsweb_file_cache_counter'] = 0; } if (!is_dir(Config::$audio_upload_tmp_dir)) { mkdir(Config::$audio_upload_tmp_dir); } - $filename = session_id() . '-' . ++$_SESSION['myury_nipsweb_file_cache_counter'] . '.mp3'; + $filename = session_id().'-'.++$_SESSION['myradio_nipsweb_file_cache_counter'].'.mp3'; - move_uploaded_file($tmp_path, Config::$audio_upload_tmp_dir . '/' . $filename); + if (!move_uploaded_file($tmp_path, Config::$audio_upload_tmp_dir.'/'.$filename)) { + throw new MyRadioException('Failed to move uploaded track to tmp directory.', 500); + } - $getID3 = new getID3; - $fileInfo = $getID3->analyze(Config::$audio_upload_tmp_dir . '/' . $filename); + $getID3 = new \getID3(); + $fileInfo = $getID3->analyze(Config::$audio_upload_tmp_dir.'/'.$filename); + $getID3_lib = new \getID3_lib(); + $getID3_lib->CopyTagsToComments($fileInfo); // File quality checks if ($fileInfo['audio']['bitrate'] < 192000) { - return array('status' => 'FAIL', 'error' => 'Bitrate is below 192kbps.', 'fileid' => $filename, 'bitrate' => $fileInfo['audio']['bitrate']); + return [ + 'status' => 'FAIL', + 'message' => 'Bitrate is below 192kbps', + 'fileid' => $filename, + 'bitrate' => $fileInfo['audio']['bitrate'] + ]; } if (strpos($fileInfo['audio']['channelmode'], 'stereo') === false) { - return array('status' => 'FAIL', 'error' => 'Item is not stereo.', 'fileid' => $filename, 'channelmode' => $fileInfo['audio']['channelmode']); + return [ + 'status' => 'FAIL', + 'message' => 'Item is not stereo', + 'fileid' => $filename, + 'channelmode' => $fileInfo['audio']['channelmode'] + ]; } - return array( - 'fileid' => $filename, - 'analysis' => self::identifyUploadedTrack(Config::$audio_upload_tmp_dir . '/' . $filename) - ); + $analysis['status'] = 'INFO'; + $analysis['message'] = 'Currently editing track information for'; + $analysis['submittable'] = true; + $analysis['fileid'] = $filename; + $analysis['analysis']['title'] = $fileInfo['comments_html']['title']; + $analysis['analysis']['artist'] = $fileInfo['comments_html']['artist']; + $analysis['analysis']['album'] = $fileInfo['comments_html']['album']; + + //Remove total tracks in album from the track_number tag. + $trackNo = explode("/", $fileInfo['comments_html']['track_number'][0], 2)[0]; + $analysis['analysis']['position'] = (string)$trackNo; + + $trackName = implode("", $fileInfo['comments_html']['title']); + $analysis['analysis']['explicit'] = !!stripos($trackName, 'explicit'); + + return $analysis; } /** * Attempts to identify an MP3 file against the last.fm database. - * + * * !This method requires the external lastfm-fpclient application to be installed on the server. A FreeBSD build * with URY's API key and support for -json can be found in the fpclient.git URY Git repository. - * - * @param String $path The location of the MP3 file - * @return Array A parsed array version of the JSON lastfm response + * + ***** Since LastFM was removed from the central track uploader, this code MAY not be used anymore. + * + * @param string $path The location of the MP3 file + * + * @return array A parsed array version of the JSON lastfm response */ - public static function identifyUploadedTrack($path) { + public static function identifyUploadedTrack($path) + { //Syspath is set by Daemons or where $PATH is not sufficent. - $response = shell_exec((empty($GLOBALS['syspath']) ? '' : $GLOBALS['syspath']) . 'lastfm-fpclient -json ' . $path); - //echo (empty($GLOBALS['syspath']) ? '' : $GLOBALS['syspath']).'lastfm-fpclient -json ' . $path; + $response = shell_exec((empty($GLOBALS['syspath']) ? '' : $GLOBALS['syspath']).'lastfm-fpclient -json '.$path); + + if (!trim($response)) { + return ['status' => 'LASTFM_ERROR', + 'error' => 'Last.FM doesn\'t seem to be working right now.', ]; + } $lastfm = json_decode($response, true); if (empty($lastfm)) { - return array('FAIL' => 'This track could not be identified. Please email the track to track.requests@ury.org.uk.'); + return ['status' => 'NO_LASTFM_MATCH', + 'error' => 'Track not found in Last FM.', ]; } else { if (isset($lastfm['tracks']['track']['mbid'])) { //Only one match - return array( - array('title' => $lastfm['tracks']['track']['name'], - 'artist' => $lastfm['tracks']['track']['artist']['name'], - 'rank' => $lastfm['tracks']['track']['@attr']['rank']) - ); + return [[ + 'title' => $lastfm['tracks']['track']['name'], + 'artist' => $lastfm['tracks']['track']['artist']['name'], + 'rank' => $lastfm['tracks']['track']['@attr']['rank'], + ]]; } - $tracks = array(); - if (empty($lastfm['tracks']['track'])) - return array(); + $tracks = []; + if (empty($lastfm['tracks']['track'])) { + return []; + } foreach ($lastfm['tracks']['track'] as $track) { - $tracks[] = array('title' => $track['name'], 'artist' => $track['artist']['name'], 'rank' => $track['@attr']['rank']); + $tracks[] = [ + 'title' => $track['name'], + 'artist' => $track['artist']['name'], + 'rank' => $track['@attr']['rank'] + ]; } + return $tracks; } } - public static function identifyAndStoreTrack($tmpid, $title, $artist) { - //Get the album info - $ainfo = self::getAlbumDurationAndPositionFromLastfm($title, $artist); + /** + * Pay special attention to the tri-state value of explicit. False and null are different things. + */ + public static function identifyAndStoreTrack($tmpid, $title, $artist, $album, $position, $explicit = null) + { + // We need to rollback if something goes wrong later + self::$db->query('BEGIN'); + + $track = self::findByNameArtist($title, $artist, 1, false, true); + + $ainfo = null; + if ($album == 'FROM_LASTFM') { + // Get the album info if we're getting it from lastfm + $ainfo = self::getAlbumDurationAndPositionFromLastfm($title, $artist); + } else { + if (!empty($track)) { + $myradio_album = $track[0]->getAlbum(); + } else { + // Use the album title the user has provided. Use an existing album + // if we already have one of that title. If not, create one. + $myradio_album = MyRadio_Album::findOrCreate($album, $artist); + } + $ainfo = array('duration' => null, 'position' => intval($position), 'album' => $myradio_album); + } + + // Get the track duration from the file if it isn't already set if (empty($ainfo['duration'])) { - $getID3 = new getID3; - $ainfo['duration'] = intval($getID3->analyze(Config::$audio_upload_tmp_dir . '/' . $tmpid)['playtime_seconds']); + $getID3 = new \getID3(); + $ainfo['duration'] = intval($getID3->analyze(Config::$audio_upload_tmp_dir.'/'.$tmpid)['playtime_seconds']); } - $track = self::findByNameArtist($title, $artist, 1, false, true); + + // See if the explicit is set, and set the value for the DB accordingly - if not set unknown + if (!is_null($explicit)) { + if ($explicit === true) { + $clean = 'n'; + } elseif ($explicit === false) { + $clean = 'y'; + } + } else { + $clean = 'u'; + } + + // Check if the track is already in the library and create it if not if (empty($track)) { //Create the track - $track = self::create(array( + $track = self::create( + [ 'title' => $title, 'artist' => $artist, 'digitised' => true, 'duration' => $ainfo['duration'], 'recordid' => $ainfo['album']->getID(), - 'number' => $ainfo['position'] - )); + 'number' => $ainfo['position'], + 'clean' => $clean, + ] + ); } else { $track = $track[0]; //If it's set to digitised, throw an error if ($track->getDigitised()) { - return array('status' => 'FAIL', 'error' => 'This track is already in our library.'); + return ['status' => 'FAIL', 'error' => 'This track is already in our library.']; } else { - //Mark it as digitised + //Mark it as digitised/explicit $track->setDigitised(true); + $track->setDigitisedBy(MyRadio_User::getInstance($_SESSION['memberid'])); + $track->setClean($clean); } } - /** + /* * Store three versions of the track: * 1- 192kbps MP3 for BAPS and Chrome/IE * 2- 192kbps OGG for Safari/Firefox * 3- Original file for potential future conversions */ - $tmpfile = Config::$audio_upload_tmp_dir . '/' . $tmpid; - $dbfile = $ainfo['album']->getFolder() . '/' . $track->getID(); + $tmpfile = Config::$audio_upload_tmp_dir.'/'.$tmpid; + $dbfile = $ainfo['album']->getFolder().'/'.$track->getID(); + try { + CoreUtils::encodeTrack($tmpfile, $dbfile); + } catch (MyRadioException $e) { + return ['status' => 'FAIL', 'error' => $e->getMessage()]; + } - shell_exec("nice -n 15 ffmpeg -i '$tmpfile' -ab 192k -f mp3 - >'{$dbfile}.mp3'"); - shell_exec("nice -n 15 ffmpeg -i '$tmpfile' -acodec libvorbis -ab 192k '{$dbfile}.ogg'"); - rename($tmpfile, $dbfile . '.mp3.orig'); + self::$db->query('COMMIT'); - return array('status' => 'OK'); + return ['status' => 'OK']; } /** - * Create a new MyRadio_Track with the provided options - * @param Array $options - * title (required): Title of the track. - * artist (required): (string) Artist of the track. - * recordid (required): (int)Album of track. - * duration (required): Duration of the track, in seconds - * number: Position of track on album - * genre: Character code genre of track - * intro: Length of track intro, in seconds - * clean: 'y' yes, 'n' no, 'u' unknown lyric cleanliness status - * digitised: boolean digitised status + * Create a new MyRadio_Track with the provided options. + * + * @param array $options + * title (required): Title of the track. + * artist (required): (string) Artist of the track. + * recordid (required): (int) Album of track. + * duration (required): Duration of the track, in seconds + * number: Position of track on album + * genre: Character code genre of track + * intro: Length of track intro, in seconds + * outro: Start time of track outro, in seconds + * clean: 'y' yes, 'n' no, 'u' unknown lyric cleanliness status + * digitised: boolean digitised status + * * @return MyRadio_Track a shiny new MyRadio_Track with the provided options + * * @throws MyRadioException */ - public static function create($options) { + public static function create($options) + { self::wakeup(); - $required = array('title', 'artist', 'recordid', 'duration'); + $required = ['title', 'artist', 'recordid', 'duration']; foreach ($required as $require) { - if (empty($options[$require])) - throw new MyRadioException($require . ' is required to create a Track.', 400); + if (empty($options[$require])) { + throw new MyRadioException($require.' is required to create a Track.', 400); + } } -//Number 0 - if (empty($options['number'])) + //Number 0 + if (empty($options['number'])) { $options['number'] = 0; -//Other Genre - if (empty($options['genre'])) + } + //Other Genre (can be automatically updated later on the weekly genres updater) + if (empty($options['genre'])) { $options['genre'] = 'o'; -//No intro - if (empty($options['intro'])) + } + //No intro + if (empty($options['intro'])) { $options['intro'] = 0; -//Clean unknown - if (empty($options['clean'])) + } + //No outro + if (empty($options['outro'])) { + $options['outro'] = 0; + } + //Clean unknown + if (empty($options['clean'])) { $options['clean'] = 'u'; -//Not digitised, and formate to t/f - if (empty($options['digitised'])) + } + //Not digitised, and format to t/f + if (empty($options['digitised'])) { $options['digitised'] = 'f'; - else + } else { $options['digitised'] = $options['digitised'] ? 't' : 'f'; + } - $result = self::$db->query('INSERT INTO rec_track (number, title, artist, length, genre, intro, clean, recordid, - digitised, digitisedby, duration) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11) RETURNING trackid', array( - $options['number'], - trim($options['title']), - trim($options['artist']), - CoreUtils::intToTime($options['duration']), - $options['genre'], - CoreUtils::intToTime($options['intro']), - $options['clean'], - $options['recordid'], - $options['digitised'], - $_SESSION['memberid'], - $options['duration'] - )); - - $id = self::$db->fetch_all($result); + $data = self::$db->fetchOne( + 'INSERT INTO rec_track (number, title, artist, length, genre, intro, outro, + clean, recordid, digitised, digitisedby, duration, last_edited_time, last_edited_memberid) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $11) RETURNING *', + [ + $options['number'], + trim($options['title']), + trim($options['artist']), + CoreUtils::intToTime($options['duration']), + $options['genre'], + CoreUtils::intToTime($options['intro']), + CoreUtils::intToTime($options['outro']), + $options['clean'], + $options['recordid'], + $options['digitised'], + $_SESSION['memberid'], + $options['duration'], + CoreUtils::getTimestamp() + ] + ); - return self::getInstance($id[0]['trackid']); + return new self($data); } - public function updateInfoFromLastfm() { + public function updateInfoFromLastfm() + { $details = self::getAlbumDurationAndPositionFromLastfm($this->title, $this->artist); $this->setAlbum($details['album']); @@ -608,7 +1244,8 @@ public function updateInfoFromLastfm() { $this->setDuration($details['duration']); } - public function setAlbum(MyRadio_Album $album) { + public function setAlbum(MyRadio_Album $album) + { if ($album->getID() === $this->getAlbum()->getID()) { return; } @@ -617,207 +1254,359 @@ public function setAlbum(MyRadio_Album $album) { if (!file_exists($this->getPath($ext))) { continue; } - $new_dir = Config::$music_central_db_path . '/records/' . $album->getID(); + $new_dir = Config::$music_central_db_path.'/records/'.$album->getID(); if (!is_dir($new_dir)) { mkdir($new_dir); } - $new_path = $new_dir . '/' . $this->getID() . '.' . $ext; + $new_path = $new_dir.'/'.$this->getID().'.'.$ext; if (!copy($this->getPath($ext), $new_path)) { - throw new MyRadioException('Failed to move file from ' . $this->getPath($ext) . ' to ' . $new_path); + throw new MyRadioException('Failed to move file from '.$this->getPath($ext).' to '.$new_path); } unlink($this->getPath($ext)); } $this->record = $album->getID(); - self::$db->query('UPDATE rec_track SET recordid=$1 WHERE trackid=$2', array($album->getID(), $this->getID())); + self::$db->query('UPDATE rec_track SET recordid=$1 WHERE trackid=$2', [$album->getID(), $this->getID()]); - $this->updateCachedObject(); + $this->updateCacheObject(); } - public function setTitle($title) { + public function setTitle($title) + { if (empty($title)) { - throw new MyRadioException('Track title must not be empty!'); + throw new MyRadioException('Track title must not be empty!', 400); } $this->title = $title; - self::$db->query('UPDATE rec_track SET title=$1 WHERE trackid=$2', array($title, $this->getID())); - $this->updateCachedObject(); + self::$db->query('UPDATE rec_track SET title=$1 WHERE trackid=$2', [$title, $this->getID()]); + $this->updateCacheObject(); } - public function setArtist($artist) { - if (empty($artist)) + public function setArtist($artist) + { + if (empty($artist)) { throw new MyRadioException('Track artist must not be empty!'); + } $this->artist = $artist; - self::$db->query('UPDATE rec_track SET artist=$1 WHERE trackid=$2', array($artist, $this->getID())); + self::$db->query('UPDATE rec_track SET artist=$1 WHERE trackid=$2', [$artist, $this->getID()]); - $this->updateCachedObject(); + $this->updateCacheObject(); } - public function setPosition($position) { - $this->position = (int) $position; - self::$db->query('UPDATE rec_track SET number=$1 WHERE trackid=$2', array($this->getPosition(), $this->getID())); - $this->updateCachedObject(); + public function setPosition($position) + { + $this->number = (int) $position; + self::$db->query('UPDATE rec_track SET number=$1 WHERE trackid=$2', [$this->getPosition(), $this->getID()]); + $this->updateCacheObject(); } - public function getPosition() { - return $this->position; + public function getPosition() + { + return $this->number; } - public function setDuration($duration) { + public function setDuration($duration) + { $this->duration = (int) $duration; - self::$db->query('UPDATE rec_track SET length=$1, duration=$2 WHERE trackid=$3', array( + self::$db->query( + 'UPDATE rec_track SET length=$1, duration=$2 WHERE trackid=$3', + [ CoreUtils::intToTime($this->getDuration()), $this->getDuration(), - $this->getID() - )); - $this->updateCachedObject(); + $this->getID(), + ] + ); + $this->updateCacheObject(); + } + + public function setGenre($genre) + { + $this->genre = $genre; + self::$db->query( + 'UPDATE rec_track SET genre=$1 WHERE trackid=$2', + [ + $genre, + $this->getID() + ] + ); + $this->updateCacheObject(); + } + + /** + * Set the length of the track intro, in seconds. + * + * @param int $duration Duration of the intro + * + * @api POST + */ + public function setIntro($duration) + { + $this->intro = (int) $duration; + self::$db->query( + 'UPDATE rec_track SET intro=$1 WHERE trackid=$2', + [ + CoreUtils::intToTime($this->intro), + $this->getID(), + ] + ); + $this->updateCacheObject(); + } + + /** + * Set the start-time of the track outro, in seconds. + * + * @param int $start_time Start-time of the outro + * + * @api POST + */ + public function setOutro($start_time) + { + $this->outro = (int) $start_time; + self::$db->query( + 'UPDATE rec_track SET outro=$1 WHERE trackid=$2', + [ + CoreUtils::intToTime($this->outro), + $this->getID(), + ] + ); + $this->updateCacheObject(); } /** - * Returns all Tracks that are marked as digitsed in the library - * + * Returns all Tracks that are marked as digitsed in the library. + * * @return MyRadio_Track[] An array of digitised Tracks */ - public static function getAllDigitised() { + public static function getAllDigitised() + { self::initDB(); - $ids = self::$db->fetch_column('SELECT trackid FROM rec_track WHERE digitised=\'t\''); + $result = self::$db->fetchColumn('SELECT trackid FROM public.rec_track WHERE digitised=\'t\''); - $tracks = array(); - foreach ($ids as $id) { - $tracks[] = self::getInstance($id); + $tracks = []; + foreach ($result as $row) { + $tracks[] = self::getInstance($row); } return $tracks; } /** - * Returns the physical path to the Track - * @param String $format Optional file extension - at time of writing this could me "mp3", "ogg" or "mp3.orig" - * @return String path to Track file + * Returns the physical path to the Track. + * + * @param string $format Optional file extension - at time of writing this could me "mp3", "ogg" or "mp3.orig" + * + * @return string path to Track file */ - public function getPath($format = 'mp3') { - return Config::$music_central_db_path . '/records/' . $this->getAlbum()->getID() . '/' . $this->getID() . '.' . $format; + public function getPath($format = 'mp3') + { + return Config::$music_central_db_path.'/records/'.$this->getAlbum()->getID().'/'.$this->getID().'.'.$format; } /** - * Returns whether this track's physical file exists - * @param String $format Optional file extension - at time of writing this could me "mp3", "ogg" or "mp3.orig" + * Returns whether this track's physical file exists. + * + * @param string $format Optional file extension - at time of writing this could me "mp3", "ogg" or "mp3.orig" + * * @return bool If the file exists */ - public function checkForAudioFile($format = 'mp3') { + public function checkForAudioFile($format = 'mp3') + { return file_exists($this->getPath($format)); } /** - * Queries the last.fm API to find information about a track with the given title/artist combination - * @param String $title track title - * @param String $artist track artist + * Queries the last.fm API to find information about a track with the given title/artist combination. + * + * @param string $title track title + * @param string $artist track artist + * * @return array album: MyRadio_Album object matching the input * position: The track number on the album * duration: The length of the track, in seconds */ - public static function getAlbumDurationAndPositionFromLastfm($title, $artist) { - $details = json_decode(file_get_contents( - 'https://ws.audioscrobbler.com/2.0/?method=track.getInfo&api_key=' - . Config::$lastfm_api_key - . '&artist=' . urlencode($artist) - . '&track=' . urlencode(str_replace(' (Radio Edit)', '', $title)) - . '&format=json'), true); + public static function getAlbumDurationAndPositionFromLastfm($title, $artist) + { + $details = json_decode( + file_get_contents( + 'https://ws.audioscrobbler.com/2.0/?method=track.getInfo&api_key=' + .Config::$lastfm_api_key + .'&artist='.urlencode($artist) + .'&track='.urlencode(str_replace(' (Radio Edit)', '', $title)) + .'&format=json' + ), + true + ); if (!isset($details['track']['album'])) { -//Send some defaults for album info - return array( - 'album' => MyRadio_Album::findOrCreate(Config::$short_name . ' Downloads ' . date('Y'), Config::$short_name), + //Send some defaults for album info + return [ + 'album' => MyRadio_Album::findOrCreate( + Config::$short_name . ' Downloads ' . date('Y'), + Config::$short_name + ), 'position' => 0, - 'duration' => intval($details['track']['duration'] / 1000) - ); + 'duration' => intval($details['track']['duration'] / 1000), + ]; } - return array( - 'album' => MyRadio_Album::findOrCreate($details['track']['album']['title'], $details['track']['album']['artist']), + return [ + 'album' => MyRadio_Album::findOrCreate( + $details['track']['album']['title'], + $details['track']['album']['artist'] + ), 'position' => (int) $details['track']['album']['@attr']['position'], - 'duration' => intval($details['track']['duration'] / 1000) - ); + 'duration' => intval($details['track']['duration'] / 1000), + ]; } - public function setLastfmVerified() { - self::$db->query('UPDATE rec_track SET lastfm_verified=\'t\' WHERE trackid=$1', array($this->getID())); + public function setLastfmVerified() + { + self::$db->query('UPDATE rec_track SET lastfm_verified=\'t\' WHERE trackid=$1', [$this->getID()]); } /** * Get similar Tracks from last.fm. Caches on first call. - * + * * The number of results will vary - the Last.fm API is asked for 50 matches, * of which only ones with a score of 0.25 or higher will be checked, * and then only tracks that are in URY's music library returned. - * - * @todo Last.fm API Rate limit checks + * + * @todo Last.fm API Rate limit checks + * * @return MyRadio_Track[] */ - public function getSimilar() { + public function getSimilar() + { if (empty($this->lastfm_similar)) { - $data = json_decode(file_get_contents( - 'https://ws.audioscrobbler.com/2.0/?method=track.getSimilar&api_key=' - . Config::$lastfm_api_key - . '&track=' . urlencode($this->getTitle()) - . '&artist=' . urlencode($this->getArtist()) - . '&limit=50&format=json'), true); + $data = json_decode( + file_get_contents( + 'https://ws.audioscrobbler.com/2.0/?method=track.getSimilar&api_key=' + .Config::$lastfm_api_key + .'&track='.urlencode($this->getTitle()) + .'&artist='.urlencode($this->getArtist()) + .'&limit=50&format=json' + ), + true + ); if (!is_array($data['similartracks']['track'])) { - trigger_error($this . ' had an empty Similar Tracks result.'); + trigger_error($this.' had an empty Similar Tracks result.'); + return []; } foreach ($data['similartracks']['track'] as $r) { if ($r['match'] >= 0.25) { - $c = self::findByOptions(['title' => $r['name'], + //Try to find an exact match + $c = self::findByOptions( + [ + 'title' => $r['name'], + 'artist' => $r['artist']['name'], + 'limit' => 1, + 'digitised' => true, + 'precise' => true, + ] + ); + //Try to find a not-so-exact match + if (empty($c)) { + $c = self::findByOptions( + [ + 'title' => $r['name'], 'artist' => $r['artist']['name'], 'limit' => 1, - 'digitised' => true]); + 'digitised' => true, + 'precise' => false, + ] + ); + } + //If match found, add track to Similar list if (!empty($c)) { $this->lastfm_similar[] = $c[0]->getID(); } } } - $this->updateCachedObject(); + $this->updateCacheObject(); } return self::resultSetToObjArray($this->lastfm_similar); } /** - * Returns whether the Track is iTones Blacklisted + * Returns whether the Track is iTones Blacklisted. + * * @return bool */ - public function isBlacklisted() { + public function isBlacklisted() + { if ($this->itones_blacklist === null) { - $this->itones_blacklist = (bool) self::$db->num_rows( - self::$db->query('SELECT * FROM jukebox.track_blacklist ' - . 'WHERE trackid=$1', [$this->getID()])); - $this->updateCachedObject(); + $this->itones_blacklist = (bool) self::$db->numRows( + self::$db->query( + 'SELECT * FROM jukebox.track_blacklist + WHERE trackid=$1', + [$this->getID()] + ) + ); + $this->updateCacheObject(); } + return $this->itones_blacklist; } + public function setBlacklisted($blacklist) + { + if ($blacklist === true) { + $this->itones_blacklist = true; + self::$db->query( + 'INSERT INTO jukebox.track_blacklist + (trackid) VALUES ($1)', + [ + $this->getID() + ] + ); + $this->updateCacheObject(); + } elseif ($blacklist === false && $this->isBlacklisted()) { + $this->itones_blacklist = false; + self::$db->query( + 'DELETE FROM jukebox.track_blacklist + WHERE trackid = $1', + [ + $this->getID() + ] + ); + $this->updateCacheObject(); + } + } + /** * Returns various numbers that look pretty on a graph, which concern the Central Music Library. - * + * * The format is compatible with Google Charts. - * - * @return Array + * + * @return array */ - public static function getLibraryStats() { - $num_digitised = (int) self::$db->fetch_column('SELECT COUNT(*) FROM public.rec_track WHERE digitised=\'t\'')[0]; - $num_undigitised = (int) self::$db->fetch_column('SELECT COUNT(*) FROM public.rec_track WHERE digitised=\'f\'')[0]; - $num_clean = (int) self::$db->fetch_column('SELECT COUNT(*) FROM public.rec_track WHERE clean=\'y\'')[0]; - $num_unclean = (int) self::$db->fetch_column('SELECT COUNT(*) FROM public.rec_track WHERE clean=\'n\'')[0]; - $num_cleanunknown = (int) self::$db->fetch_column('SELECT COUNT(*) FROM public.rec_track WHERE clean=\'u\'')[0]; - $num_verified = (int) self::$db->fetch_column('SELECT COUNT(*) FROM public.rec_track WHERE digitised=\'t\' AND lastfm_verified=\'t\'')[0]; - $num_unverified = (int) self::$db->fetch_column('SELECT COUNT(*) FROM public.rec_track WHERE digitised=\'t\' AND lastfm_verified=\'f\'')[0]; - - $num_singles = (int) self::$db->fetch_column('SELECT COUNT(*) FROM public.rec_record WHERE format=\'s\'')[0]; - $num_albums = (int) self::$db->fetch_column('SELECT COUNT(*) FROM public.rec_record WHERE format=\'a\'')[0]; + public static function getLibraryStats() + { + $num_digitised = (int) self::$db->fetchColumn( + 'SELECT COUNT(*) FROM public.rec_track WHERE digitised=\'t\'' + )[0]; + $num_undigitised = (int) self::$db->fetchColumn( + 'SELECT COUNT(*) FROM public.rec_track WHERE digitised=\'f\'' + )[0]; + + $num_clean = (int) self::$db->fetchColumn('SELECT COUNT(*) FROM public.rec_track WHERE clean=\'y\'')[0]; + $num_unclean = (int) self::$db->fetchColumn('SELECT COUNT(*) FROM public.rec_track WHERE clean=\'n\'')[0]; + $num_cleanunknown = (int) self::$db->fetchColumn('SELECT COUNT(*) FROM public.rec_track WHERE clean=\'u\'')[0]; + + $num_verified = (int) self::$db->fetchColumn( + 'SELECT COUNT(*) FROM public.rec_track WHERE digitised=\'t\' AND lastfm_verified=\'t\'' + )[0]; + $num_unverified = (int) self::$db->fetchColumn( + 'SELECT COUNT(*) FROM public.rec_track WHERE digitised=\'t\' AND lastfm_verified=\'f\'' + )[0]; + + $num_singles = (int) self::$db->fetchColumn('SELECT COUNT(*) FROM public.rec_record WHERE format=\'s\'')[0]; + $num_albums = (int) self::$db->fetchColumn('SELECT COUNT(*) FROM public.rec_record WHERE format=\'a\'')[0]; return [ ['Key', 'Value'], @@ -829,8 +1618,160 @@ public static function getLibraryStats() { ['Singles', $num_singles], ['Albums', $num_albums], ['Verified Metadata', $num_verified], - ['Unverified Metadata', $num_unverified] + ['Unverified Metadata', $num_unverified], ]; } + /** + * Gets the track that's on air *right now*. + * + * @param string[] $sources which sources to accept tracklist data from (tracklist.source in db) + * @param bool $allowOffAir Should whatever Jukebox is playing be included even when it's not on air. + * Silly unless 'j' is passed in $sources + * @return null|array + */ + public static function getNowPlaying( + $sources = ['b', 'm', 'o', 'w', 'a', 's', 'j', '1', '2', '4'], + $allowOffAir = false + ) { + // Deal with the boolean coming through the API as a string, + // and therefore is always true + if ($allowOffAir == "false") { + $allowOffAir = false; + } + + // Start a transaction. We're gonna have some fun. + self::$db->query('BEGIN'); + + // Use repeatable read - to ensure that all queries in this TX read at the same "point in time" + self::$db->query('SET TRANSACTION ISOLATION LEVEL REPEATABLE READ'); + + // Fetch permissible source letters and filter sources to prevent nasties + $allowedSources = self::$cache->get('MyRadio_Track:getNowPlaying:allowedSources'); + if (empty($allowedSources)) { + $allowedSources = self::$db->fetchColumn('SELECT sourceid FROM tracklist.source', []); + self::$cache->set('MyRadio_Track:getNowPlaying:allowedSources', $allowedSources, 86400); + } + $sources = array_intersect($sources, $allowedSources); + // Turn into SQL-friendly string + // Like implode, but it doesn't mess with precious numerical char types. + $sourceStr = ""; + for ($i = 0; $i < count($sources); $i++) { + $sourceStr = $sourceStr . "','" . $sources[$i]; + } + + // Get the last thing that was tracklisted - this is either jukebox or WebStudio + // The 30 minutes check is to avoid having something linger for too long if WS forgets to end the tracklist + $lastTracklisted = self::$db->fetchOne( + 'SELECT audiologid, timestart AT TIME ZONE \'Europe/London\' as timestart, trackid, track, artist, album + FROM tracklist.tracklist + LEFT OUTER JOIN tracklist.track_rec USING (audiologid) + LEFT OUTER JOIN tracklist.track_notrec USING (audiologid) + WHERE timestart <= NOW() AND timestart > (NOW() - interval \'30 minutes\') AND timestop IS NULL + AND (state IS NULL OR state = \'c\'' .($allowOffAir ? ' OR state = \'o\'' : '') . ') + AND source IN (\'' . $sourceStr . '\') + ORDER BY timestart DESC + LIMIT 1', + [] + ); + + // Check what's currently on air - if it's a physical studio or OB we'll need to check BAPS + // We do this in SQL, rather than via MyRadio_Selector, to maintain transaction consistency + if (in_array('b', $sources)) { + $result = self::$db->fetchColumn( + 'SELECT action FROM public.selector WHERE time <= NOW() + AND action >= 4 AND action <= 11 + ORDER BY time DESC + LIMIT 1', + [] + ); + $selAction = isset($result[0]) ? intval($result[0]) : 0; + if ($selAction === 4 /* Studio 1 */ || $selAction === 5 /* Studio 2 */ || $selAction == 7 /* OB */) { + // Ditto on the 30 minutes + // The 30 *seconds* is to (hopefully) catch PFLs + $lastBapsLogged = self::$db->fetchOne( + 'SELECT audiologid, timeplayed AT TIME ZONE \'Europe/London\' AS timestart, trackid + FROM public.baps_audiolog + INNER JOIN public.baps_audio USING (audioid) + INNER JOIN tracklist.selbaps ON baps_audiolog.serverid = selbaps.bapsloc + WHERE selaction = $1 + AND timestopped IS NULL + AND trackid IS NOT NULL + AND timeplayed <= (NOW() AT TIME ZONE \'Europe/London\' - interval \'30 seconds\') + AND timeplayed > (NOW() AT TIME ZONE \'Europe/London\' - interval \'30 minutes\') + ORDER BY timeplayed DESC + LIMIT 1 + ', + [ $selAction ] + ); + if (!empty($lastBapsLogged)) { + if (empty($lastTracklisted) + || strtotime($lastBapsLogged['timestart']) > strtotime($lastTracklisted['timestart']) + ) { + // Last BAPS entry is newer than last tracklist entry (if there is one). + $lastTracklisted = $lastBapsLogged; + } + } + } + } + // We're done querying + self::$db->query('COMMIT'); + + if (empty($lastTracklisted)) { + // Nothing playing right now + return null; + } elseif (!empty($lastTracklisted['trackid'])) { + // track_rec + return [ + 'track' => self::getInstance($lastTracklisted['trackid']), + 'start_time' => $lastTracklisted['timestart'] + ]; + } else { + // track_notrec (manual tracklisting) + // Double-check it was in the last five minutes + if (strtotime($lastTracklisted['timestart']) > (time() - 300)) { + return [ + 'track' => [ + 'title' => $lastTracklisted['track'], + 'artist' => $lastTracklisted['artist'], + 'album' => $lastTracklisted['album'] + ], + 'start_time' => $lastTracklisted['timestart'] + ]; + } else { + return null; + } + } + } + + /** + * Returns a list of potential clean statuses, organised so + * they can be used as a SELECT MyRadioFormField data source. + */ + public static function getCleanOptions() + { + self::wakeup(); + + return self::$db->fetchAll( + 'SELECT clean_code AS value, clean_descr AS text FROM public.rec_cleanlookup ORDER BY clean_descr ASC' + ); + } + + /** + * Returns a list of potential genres, organised so + * they can be used as a SELECT MyRadioFormField data source. + */ + public static function getGenres() + { + self::wakeup(); + + return self::$db->fetchAll( + 'SELECT genre_code AS value, genre_descr AS text FROM public.rec_genrelookup ORDER BY genre_descr ASC' + ); + } + + public static function getGraphQLTypeName() + { + return 'Track'; + } } diff --git a/src/Classes/ServiceAPI/MyRadio_TrackCorrection.php b/src/Classes/ServiceAPI/MyRadio_TrackCorrection.php index de9e4650a..dc51fa380 100644 --- a/src/Classes/ServiceAPI/MyRadio_TrackCorrection.php +++ b/src/Classes/ServiceAPI/MyRadio_TrackCorrection.php @@ -1,219 +1,281 @@ - * @package MyRadio_Core - * @uses \Database - * @todo Cache this + * + * @uses \Database + * + * @todo Cache this */ -class MyRadio_TrackCorrection extends MyRadio_Track { - /** - * A "recommended" correction proposal is one that is almost certainly correct, e.g. typo - */ - const LEVEL_RECOMMEND = 1; - /** - * A "suggested" correction proposal is one that is possibly correct, e.g. incorrect information - */ - const LEVEL_SUGGEST = 0; - - /** - * The proposed title for the track - * @var String - */ - private $proposed_title; - /** - * The proposed artist for the track - * @var String - */ - private $proposed_artist; - /** - * The proposed album name for the track. This is *now* a MyRadio_Album - The album may not exist yet. - * @var String - */ - private $proposed_album_name; - - /** - * The ID of the Track Correction Proposal. - * @var int - */ - private $correctionid; - - /** - * The User that has reviewed this Correction, if any. - * @var null:User - */ - private $reviewedby; - - /** - * The recommendation level - one of the LEVEL_ constants - * @var int - */ - private $level; - - /** - * The state of the correction (p)ending, (a)pproved or (r)ejected - * @var String - */ - private $state; - - /** - * Initiates the Track variables - * @param int $correctionid The ID of the track correction proposal to initialise - * @todo Genre class - * @todo Artist normalisation - */ - protected function __construct($correctionid) { - $this->correctionid = (int) $correctionid; - $result = self::$db->fetch_one('SELECT * FROM public.rec_trackcorrection WHERE correctionid=$1 LIMIT 1', - array($this->correctionid)); - if (empty($result)) { - throw new MyRadioException('The specified TrackCorrection does not seem to exist'); - return; +class MyRadio_TrackCorrection extends MyRadio_Track +{ + /** + * A "recommended" correction proposal is one that is almost certainly correct, e.g. typo. + */ + const LEVEL_RECOMMEND = 1; + /** + * A "suggested" correction proposal is one that is possibly correct, e.g. incorrect information. + */ + const LEVEL_SUGGEST = 0; + + /** + * The proposed title for the track. + * + * @var string + */ + private $proposed_title; + /** + * The proposed artist for the track. + * + * @var string + */ + private $proposed_artist; + /** + * The proposed album name for the track. This is *now* a MyRadio_Album - The album may not exist yet. + * + * @var string + */ + private $proposed_album_name; + + /** + * The ID of the Track Correction Proposal. + * + * @var int + */ + private $correctionid; + + /** + * The User that has reviewed this Correction, if any. + * + * @var null:User + */ + private $reviewedby; + + /** + * The recommendation level - one of the LEVEL_ constants. + * + * @var int + */ + private $level; + + /** + * The state of the correction (p)ending, (a)pproved or (r)ejected. + * + * @var string + */ + private $state; + + /** + * Initiates the Track variables. + * + * @param int $correctionid The ID of the track correction proposal to initialise + * + * @todo Genre class + * @todo Artist normalisation + */ + protected function __construct($correctionid) + { + $this->correctionid = (int) $correctionid; + $result = self::$db->fetchOne( + 'SELECT * FROM public.rec_trackcorrection WHERE correctionid=$1 LIMIT 1', + [$this->correctionid] + ); + if (empty($result)) { + throw new MyRadioException('The specified TrackCorrection does not seem to exist', 404); + + return; + } + + $track_res = self::$db->fetchOne('SELECT * FROM public.rec_track WHERE trackid=$1 LIMIT 1', [$trackid]); + if (empty($track_res)) { + throw new MyRadioException('The specified Track does not seem to exist', 404); + } + + parent::__construct($track_res); + + $this->proposed_title = $result['proposed_title']; + $this->proposed_artist = $result['proposed_artist']; + $this->proposed_album_name = $result['proposed_album_name']; + $this->reviewedby = empty($result['reviewedby']) ? null : MyRadio_User::getInstance($result['reviewedby']); + $this->level = (int) $result['level']; + $this->state = $result['state']; + } + + /** + * Creates a new MyRadio_TrackCorrection Proposal. + * + * @param MyRadio_Track $track The Track to correct + * @param string $title The proposed Title + * @param string $artist The proposed Artist + * @param string $album_name The proposed Album + * + * @return MyRadio_TrackCorrection The New Correction object + */ + public static function create( + $track, + $title = 'No Suggestion.', + $artist = 'No Suggestion.', + $album_name = 'No Suggestion.', + $level = self::LEVEL_SUGGEST + ) { + $r = self::$db->fetchColumn( + 'INSERT INTO public.rec_trackcorrection + (trackid, proposed_title, proposed_artist, proposed_album_name, level) + VALUES ($1, $2, $3, $4, $5) RETURNING correctionid', + [$track->getID(), $title, $artist, $album_name, $level] + ); + + if (empty($r)) { + return false; + } + + return self::getInstance((int) $r[0]); + } + + /** + * Get a random "Pending" track correction proposal, or null if there are no proposals. + * + * @return MyRadio_TrackCorrection|null + */ + public static function getRandom() + { + $result = self::$db->fetchColumn( + 'SELECT correctionid FROM public.rec_trackcorrection WHERE state=\'p\' + ORDER BY RANDOM() LIMIT 1' + ); + + if (empty($result)) { + return; + } + + return self::getInstance($result[0]); + } + + public function getProposedTitle() + { + return $this->proposed_title; + } + + public function getProposedArtist() + { + return $this->proposed_artist; + } + + public function getProposedAlbumTitle() + { + return $this->proposed_album_name; + } + + public function getLevel() + { + return $this->level; } - parent::__construct($result['trackid']); - - $this->proposed_title = $result['proposed_title']; - $this->proposed_artist = $result['proposed_artist']; - $this->proposed_album_name = $result['proposed_album_name']; - $this->reviewedby = empty($result['reviewedby']) ? null : MyRadio_User::getInstance($result['reviewedby']); - $this->level = (int)$result['level']; - $this->state = $result['state']; - } - - /** - * Creates a new MyRadio_TrackCorrection Proposal - * @param MyRadio_Track $track The Track to correct - * @param String $title The proposed Title - * @param String $artist The proposed Artist - * @param String $album_name The proposed Album - * @return MyRadio_TrackCorrection The New Correction object - */ - public static function create($track, $title = 'No Suggestion.', - $artist = 'No Suggestion.', $album_name = 'No Suggestion.', $level = self::LEVEL_SUGGEST) { - $r = self::$db->fetch_column('INSERT INTO public.rec_trackcorrection - (trackid, proposed_title, proposed_artist, proposed_album_name, level) - VALUES ($1, $2, $3, $4, $5) RETURNING correctionid', - array($track->getID(), $title, $artist, $album_name, $level)); - - if (empty($r)) return false; - - return self::getInstance((int)$r[0]); - } - - /** - * Get a random "Pending" track correction proposal, or null if there are no proposals - * @return MyRadio_TrackCorrection|null - */ - public static function getRandom() { - $result = self::$db->fetch_column('SELECT correctionid FROM public.rec_trackcorrection WHERE state=\'p\' - ORDER BY RANDOM() LIMIT 1'); - - if (empty($result)) return null; - - return self::getInstance($result[0]); - } - - public function getProposedTitle() { - return $this->proposed_title; - } - - public function getProposedArtist() { - return $this->proposed_artist; - } - - public function getProposedAlbumTitle() { - return $this->proposed_album_name; - } - - public function getLevel() { - return $this->level; - } - - public function getCorrectionID() { - return $this->correctionid; - } - - public function getState() { - return $this->state; - } - - /** - * Apply the proposed correction to the original rec_track entry. - * @param bool $ignore_album If true, the album will not be changed. - * @return boolean - * @todo Does the Cache need updating anywhere? - */ - public function apply($ignore_album = false) { - //Don't apply a "URY Downloads" album - that's worse than whatever is already there. - if (!$ignore_album && strstr($this->getProposedAlbumTitle(), Config::$short_name.' Downloads') === false) { - $this->setAlbum(MyRadio_Album::findOrCreate($this->getProposedAlbumTitle(), $this->getProposedArtist())); + public function getCorrectionID() + { + return $this->correctionid; } - $this->setArtist($this->getProposedArtist()); - $this->setTitle($this->getProposedTitle()); - - self::$db->query('UPDATE public.rec_trackcorrection SET state=\'a\', reviewedby=$2 WHERE correctionid=$1', - array($this->getCorrectionID(), MyRadio_User::getInstance()->getID())); - $this->state = 'a'; - $this->setLastfmVerified(); - return true; - } - - public function reject($permanent = false) { - self::$db->query('UPDATE public.rec_trackcorrection SET state=\'r\', reviewedby=$2 WHERE correctionid=$1', - array($this->getCorrectionID(), MyRadio_User::getInstance()->getID())); - - if ($permanent) { - $this->setLastfmVerified(); + + public function getState() + { + return $this->state; + } + + /** + * Apply the proposed correction to the original rec_track entry. + * + * @param bool $ignore_album If true, the album will not be changed. + * + * @return bool + * + * @todo Does the Cache need updating anywhere? + */ + public function apply($ignore_album = false) + { + //Don't apply a "URY Downloads" album - that's worse than whatever is already there. + if (!$ignore_album && strstr($this->getProposedAlbumTitle(), Config::$short_name.' Downloads') === false) { + $this->setAlbum(MyRadio_Album::findOrCreate($this->getProposedAlbumTitle(), $this->getProposedArtist())); + } + $this->setArtist($this->getProposedArtist()); + $this->setTitle($this->getProposedTitle()); + + self::$db->query( + 'UPDATE public.rec_trackcorrection SET state=\'a\', reviewedby=$2 WHERE correctionid=$1', + [$this->getCorrectionID(), MyRadio_User::getInstance()->getID()] + ); + $this->state = 'a'; + $this->setLastfmVerified(); + + return true; } - } - - /** - * Returns an array of key information, useful for Twig rendering and JSON requests - * @todo Expand the information this returns - * @return Array - */ - public function toDataSource() { - return array( - 'title' => $this->getTitle(), - 'artist' => $this->getArtist(), - 'album' => $this->getAlbum()->toDataSource(), - 'trackid' => $this->getID(), - 'proposed_title' => $this->getProposedTitle(), - 'proposed_artist' => $this->getProposedArtist(), - 'proposed_album' => $this->getProposedAlbumTitle(), - 'level' => $this->getLevel(), - 'correctionid' => $this->getCorrectionID(), - 'state' => $this->getState(), - 'editlink' => array( - 'display' => 'icon', - 'value' => 'script', - 'title' => 'Edit Track Manually', - 'url' => CoreUtils::makeURL('Library', 'editTrack', array('trackid' => $this->getID())) - ), - 'confirmlink' => array( - 'display' => 'icon', - 'value' => 'circle-check', - 'title' => 'Approve Track Correction', - 'url' => CoreUtils::makeURL('Library', 'acceptTrackCorrection', array('correctionid' => $this->getCorrectionID())) - ) - , - 'rejectlink' => array( - 'display' => 'icon', - 'value' => 'trash', - 'title' => 'Reject Track Correction', - 'url' => CoreUtils::makeURL('Library', 'rejectTrackCorrection', array('correctionid' => $this->getCorrectionID())) - ) - ); - } + public function reject($permanent = false) + { + self::$db->query( + 'UPDATE public.rec_trackcorrection SET state=\'r\', reviewedby=$2 WHERE correctionid=$1', + [$this->getCorrectionID(), MyRadio_User::getInstance()->getID()] + ); + + if ($permanent) { + $this->setLastfmVerified(); + } + } + + /** + * Returns an array of key information, useful for Twig rendering and JSON requests. + * @param array $mixins Mixins. Currently unused + * @return array + * @todo Expand the information this returns + */ + public function toDataSource($mixins = []) + { + return [ + 'title' => $this->getTitle(), + 'artist' => $this->getArtist(), + 'album' => $this->getAlbum()->toDataSource(), + 'trackid' => $this->getID(), + 'proposed_title' => $this->getProposedTitle(), + 'proposed_artist' => $this->getProposedArtist(), + 'proposed_album' => $this->getProposedAlbumTitle(), + 'level' => $this->getLevel(), + 'correctionid' => $this->getCorrectionID(), + 'state' => $this->getState(), + 'editlink' => [ + 'display' => 'icon', + 'value' => 'pencil', + 'title' => 'Edit Track Manually', + 'url' => URLUtils::makeURL('Library', 'editTrack', ['trackid' => $this->getID()]), + ], + 'confirmlink' => [ + 'display' => 'icon', + 'value' => 'ok', + 'title' => 'Approve Track Correction', + 'url' => URLUtils::makeURL( + 'Library', + 'acceptTrackCorrection', + ['correctionid' => $this->getCorrectionID()] + ), + ], + 'rejectlink' => [ + 'display' => 'icon', + 'value' => 'trash', + 'title' => 'Reject Track Correction', + 'url' => URLUtils::makeURL( + 'Library', + 'rejectTrackCorrection', + ['correctionid' => $this->getCorrectionID()] + ), + ], + ]; + } } diff --git a/src/Classes/ServiceAPI/MyRadio_TracklistItem.php b/src/Classes/ServiceAPI/MyRadio_TracklistItem.php index f6a514615..d05b24507 100644 --- a/src/Classes/ServiceAPI/MyRadio_TracklistItem.php +++ b/src/Classes/ServiceAPI/MyRadio_TracklistItem.php @@ -1,346 +1,634 @@ - * @package MyRadio_Tracklist - * @uses \Database - * + * + * @uses \Database */ -class MyRadio_TracklistItem extends ServiceAPI { - - private $audiologid; - private $source; - private $starttime; - private $endtime; - private $state; - private $timeslot; - private $bapsaudioid; - - /** - * MyRadio_Track that was played, or an array of artist, album, track, label, length data. - */ - private $track; - - protected function __construct($id) { - $this->audiologid = (int) $id; - - $result = self::$db->fetch_one('SELECT * FROM tracklist.tracklist - LEFT JOIN tracklist.track_rec ON tracklist.audiologid = track_rec.audiologid - LEFT JOIN tracklist.track_notrec ON tracklist.audiologid = track_notrec.audiologid - WHERE tracklist.audiologid=$1 LIMIT 1', array($id)); - if (empty($result)) { - throw new MyRadioException('The requested TracklistItem does not appear to exist!', 400); +class MyRadio_TracklistItem extends ServiceAPI +{ + const BASE_TRACKLISTITEM_SQL = + 'SELECT * FROM tracklist.tracklist + LEFT JOIN tracklist.track_rec USING (audiologid) + LEFT JOIN tracklist.track_notrec USING (audiologid)'; + private $audiologid; + private $source; + private $starttime; + private $endtime; + private $state; + private $timeslot; + private $bapsaudioid; + + /** + * MyRadio_Track that was played, or an array of artist, album, track, label, length data. + */ + private $track; + + protected function __construct($result) + { + $this->audiologid = (int) $result['audiologid']; + + $this->source = $result['source']; + $this->starttime = strtotime($result['timestart']); + $this->endtime = strtotime($result['timestop']); + $this->state = $result['state']; + $this->timeslot = is_numeric($result['timeslotid']) ? + MyRadio_Timeslot::getInstance($result['timeslotid']) : null; + $this->bapsaudioid = is_numeric($result['bapsaudioid']) ? (int) $result['bapsaudioid'] : null; + + $this->track = is_numeric($result['trackid']) ? $result['trackid'] : + [ + 'title' => $result['track'], + 'artist' => $result['artist'], + 'album' => $result['album'], + 'trackid' => null, + 'trackno' => (int) $result['trackno'], + 'length' => $result['length'], + 'record_label' => $result['label'], + ]; + } + + protected static function factory($id) + { + $result = self::$db->fetchOne(self::BASE_TRACKLISTITEM_SQL.' WHERE tracklist.audiologid=$1 LIMIT 1', [$id]); + if (empty($result)) { + throw new MyRadioException('The requested TracklistItem does not appear to exist.', 404); + } + + return new self($result); + } + + /** + * Create a new TracklistItem, returning the new item. + * + * @param int $trackid The ID of the track to tracklist. + * @param int $timeslotid The ID of the timeslot to tracklist to. Optional, defaults to current show. + * @param int $starttime Epoch time of the start of the tracklist. Optional, defaults to current time. + * @param char $sourceid The id of the tracklist source (baps, webstudio, etc), see tracklist.source. + * Defaults to 'api' + * @param char $state The state of the tracklist, see tracklist.state. Defaults to 'confirmed' + * + * + * @return MyRadio_TracklistItem + * + * @throws MyRadioException + */ + public static function create($trackid, $timeslotid = null, $starttime = null, $sourceid = 'a', $state = null) + { + + if (AuthUtils::hasPermission(AUTH_TRACKLIST_ALL)) { + $tracklist_all = true; + } elseif (AuthUtils::hasPermission(AUTH_TRACKLIST_OWN)) { + $tracklist_all = false; + } else { + throw new MyRadioException( + "The current user does not have permission to create a tracklistitem.", + 403 + ); + } + + if ($timeslotid != null && $tracklist_all == false) { + throw new MyRadioException( + "The current user doesn't have permission to set a tracklist on a show other than their own.", + 403 + ); + } + + $timeslot_was_null = false; + if ($timeslotid == null) { + $timeslot_was_null = true; + $timeslot = MyRadio_Timeslot::getCurrentTimeslot(); + $timeslotid = $timeslot != null ? $timeslot->getID() : null; // will be null if jukebox etc. + } else { + $timeslot = MyRadio_Timeslot::getInstance($timeslotid); + } + + if ($starttime == null) { + $starttime = time(); + } + + if ($timeslot == null) { + // we're on jukebox + if ($tracklist_all == false) { + throw new MyRadioException( + "The current user doesn't have permission to set a tracklist on a show other than their own.", + 403 + ); + } + } else { + if ($tracklist_all == false && !$timeslot->isCurrentUserAnOwner()) { + throw new MyRadioException( + "Current user doesn't have permission to tracklist to a show they aren't credited on.", + 403 + ); + } + if ($timeslot->getStartTime() > $starttime || $timeslot->getEndTime() < $starttime) { + throw new MyRadioException( + "The starttime provided was outside the window of the requested timeslot.", + 400 + ); + } + } + + // Table is timestamp with no timezone, so we need to account for BST + $dst_offset = timezone_offset_get(timezone_open(Config::$timezone), date_create('@'.$starttime)); + if ($dst_offset !== false) { + $starttime = $starttime + $dst_offset; + } + + + $track = MyRadio_Track::getInstance($trackid); + + # If we've been left to work out which state we're in (confirmed or off air), let's look this up. + if ($state == null) { + $state = in_array($sourceid, self::getTracklistSourcesOnAirAtTime($starttime)) ? 'c': 'o'; + + // If we didn't originally supply a timeslotid, and we're tracklisting off air + // Don't attach to the current timeslot. + // This is useful for BAPS, where it doesn't know if it's tracklisting to a show, + // or if it's the on air studio. + // We don't want to report a track as played off air to a timeslot if it's in a different room etc. + if ($state == 'o' && $timeslot_was_null == true) { + $timeslotid = null; + } + } + + self::$db->query('BEGIN'); + + $audiologid = self::$db->fetchOne( + 'INSERT INTO tracklist.tracklist (source, timeslotid, timestart, state) + VALUES ($1, $2, $3, $4) RETURNING audiologid', + [$sourceid, $timeslotid, CoreUtils::getTimestamp($starttime), $state] + ); + + if ($audiologid['audiologid'] == null) { + self::$db->query('ABORT'); + throw new MyRadioException( + "Was not able to register tracklist entry. Source is likely invalid.", + 400 + ); + } + + self::$db->query( + 'INSERT INTO tracklist.track_rec (audiologid, recordid, trackid) + VALUES ($1, $2, $3)', + [$audiologid['audiologid'], $track->getAlbum()->getID(), $track->getID()] + ); + + self::$db->query('COMMIT'); + + return self::getInstance($audiologid['audiologid']); + } + + public function getEndTime() + { + return $this->endtime; + } + + public function setEndTime() + { + if ($this->starttime) { + // Table is timestamp with no timezone, so we need to account for BST + $endtime = time(); + $dst_offset = timezone_offset_get(timezone_open(Config::$timezone), date_create('@'.$endtime)); + if ($dst_offset !== false) { + $endtime = $endtime + $dst_offset; + } + $time = CoreUtils::getTimestamp($endtime); + if (AuthUtils::hasPermission(AUTH_TRACKLIST_ALL) + || (AuthUtils::hasPermission(AUTH_TRACKLIST_OWN) + && $this->timeslot->getSeason()->getShow()->isCurrentUserAnOwner()) + ) { + self::$db->query( + 'UPDATE tracklist.tracklist SET timestop=$1 WHERE audiologid=$2', + [$time, $this->getID()] + ); + $this->endtime = strtotime($time); + } else { + throw new MyRadioException( + "Current user doesn't have permission to set the endtime of a tracklistitem not from their show.", + 403 + ); + } + } else { + throw new MyRadioException( + "This timeslotitem does not have a start time. An end time therefore cannot be set.", + 400 + ); + } + return $this; + } + + public function getID() + { + return $this->audiologid; } - $this->source = $result['source']; - $this->starttime = strtotime($result['timestart']); - $this->endtime = strtotime($result['timestop']); - $this->state = $result['state']; - $this->timeslot = is_numeric($result['timeslotid']) ? MyRadio_Timeslot::getInstance($result['timeslotid']) : null; - $this->bapsaudioid = is_numeric($result['bapsaudioid']) ? (int) $result['bapsaudioid'] : null; - - $this->track = is_numeric($result['trackid']) ? $result['trackid'] : - array( - 'title' => $result['track'], - 'artist' => $result['artist'], - 'album' => $result['album'], - 'trackid' => null, - 'trackno' => (int) $result['trackno'], - 'length' => $result['length'], - 'record_label' => $result['label'] - ); - } - - public function getID() { - return $this->audiologid; - } - - public function getTrack() { - return is_array($this->track) ? $this->track : + public function getTrack() + { + return is_array($this->track) ? $this->track : MyRadio_Track::getInstance($this->track); - } - - public function getStartTime() { - return $this->starttime; - } - - /** - * Returns an array of all TracklistItems played during the given Timeslot - * @param int $timeslotid - * @return Array - */ - public static function getTracklistForTimeslot($timeslotid, $offset = 0) { - $result = self::$db->fetch_column('SELECT audiologid FROM tracklist.tracklist - WHERE timeslotid=$1 - AND (state ISNULL OR state != \'d\') - AND audiologid > $2 - ORDER BY timestart ASC', - array($timeslotid, $offset)); - - $items = array(); - foreach ($result as $item) { - $items[] = self::getInstance($item); } - return $items; - } + public function getStartTime() + { + return $this->starttime; + } + + /** + * Get which tracklist sources (tracklist.source) are on air based on the selector status at a given time. + * + * @param int $time Epoch time. Optional, defaults to current time. + * + * @return char[] Tracklist sources + */ + public static function getTracklistSourcesOnAirAtTime($time = null) + { + $sel_action = MyRadio_Selector::getSelActionAtTime($time); + + $sources = self::$db->fetchColumn( + 'SELECT sourceid FROM tracklist.selsources WHERE selaction=$1', + [$sel_action] + ); - /** - * Find all tracks played by Jukebox - * @param int $start Period to start log from. Default 0. - * @param int $end Period to end log from. Default time(). - * @param bool $include_playout Optional. Default true. If true, include statistics from when jukebox was not on air, - * i.e. when it was only feeding campus bars. - */ - public static function getTracklistForJukebox($start = null, $end = null, $include_playout = true) { - self::wakeup(); + return $sources; + } + + /** + * Returns an array of all TracklistItems played during the given Timeslot. + * + * @param int|MyRadio_Timeslot $timeslotid The ID of the Timeslot + * @param int $offset Skip items with an audiologid <= this + * + * @return array + */ + public static function getTracklistForTimeslot($timeslotid, $offset = 0) + { + if ($timeslotid instanceof MyRadio_Timeslot) { + $timeslotid = $timeslotid->getID(); + } + $result = self::$db->fetchAll( + self::BASE_TRACKLISTITEM_SQL + .' WHERE timeslotid=$1' + .' AND (state ISNULL OR state != \'d\')' + .' AND tracklist.audiologid > $2' + .' ORDER BY timestart ASC', + [$timeslotid, $offset] + ); + + $items = []; + foreach ($result as $item) { + $items[] = new self($item); + } - $start = $start === null ? '1970-01-01 00:00:00' : CoreUtils::getTimestamp($start); - $end = $end === null ? CoreUtils::getTimestamp() : CoreUtils::getTimestamp($end); + return $items; + } - $result = self::$db->fetch_column('SELECT audiologid FROM tracklist.tracklist WHERE source=\'j\' - AND timestart >= $1 AND timestart <= $2' . ($include_playout ? '' : ' AND state!=\'u\' AND state!=\'d\''), array($start, $end)); + /** + * Find all tracks played by Jukebox. + * + * @param int $start Period to start log from. Default 0. + * @param int $end Period to end log from. Default time(). + * @param bool $include_playout Optional. If true, include statistics from when jukebox was not on air, + * i.e. when it was only feeding campus bars. Default true. + */ + public static function getTracklistForJukebox($start = null, $end = null, $include_playout = true) + { + self::wakeup(); + + $start = $start === null ? '1970-01-01 00:00:00' : CoreUtils::getTimestamp($start); + $end = $end === null ? CoreUtils::getTimestamp() : CoreUtils::getTimestamp($end); + + $result = self::$db->fetchAll( + self::BASE_TRACKLISTITEM_SQL + .' WHERE source=\'j\'' + .' AND timestart >= $1 AND timestart <= $2' + .($include_playout ? '' : ' AND state!=\'u\' AND state!=\'d\''), + [$start, $end] + ); + + $items = []; + foreach ($result as $item) { + $items[] = new self($item); + } - $items = array(); - foreach ($result as $item) { - $items[] = self::getInstance((int) $item); + return $items; } - return $items; - } - - /** - * Find all tracks played in the given timeframe, as datasources. - * Not datasource runs out of RAM pretty quick. - * - * @param int $start Period to start log from. Required. - * @param int $end Period to end log from. Default time(). - * @param bool $include_playout If true, includes tracks played on /jukebox or /campus_playout while a show was on. - */ - public static function getTracklistForTime($start, $end = null, $include_playout = false) { - self::wakeup(); - - $start = CoreUtils::getTimestamp($start); - $end = $end === null ? CoreUtils::getTimestamp() : CoreUtils::getTimestamp($end); - - $result = self::$db->fetch_column('SELECT audiologid FROM tracklist.tracklist - WHERE timestart >= $1 AND timestart <= $2 AND (state IS NULL OR state=\'c\'' - . ($include_playout ? 'OR state = \'o\')' : ')') - . ' ORDER BY timestart ASC', array($start, $end)); - - $return = []; - foreach ($result as $id) { - if (sizeof($return) == 100000) { + /** + * Find all tracks played in the given timeframe, as datasources. + * Not datasource runs out of RAM pretty quick. + * + * @todo Datasources are a lot nicer than they used to be - revisit this + * + * @param int $start Period to start log from. Required. + * @param int $end Period to end log from. Default time(). + * @param bool $include_playout If true, includes tracks played on /jukebox or /campus_playout while a show was on. + */ + public static function getTracklistForTime($start, $end = null, $include_playout = false) + { + self::wakeup(); + + $start = CoreUtils::getTimestamp($start); + $end = $end === null ? CoreUtils::getTimestamp() : CoreUtils::getTimestamp($end); + + $result = self::$db->fetchAll( + self::BASE_TRACKLISTITEM_SQL + .' WHERE timestart >= $1 AND timestart <= $2 AND (state IS NULL OR state=\'c\'' + .($include_playout ? 'OR state = \'o\')' : ')') + .' ORDER BY timestart ASC', + [$start, $end] + ); + + $return = []; + foreach ($result as $item) { + if (sizeof($return) == 100000) { + return $return; + } + + $obj = new self($item); + $data = $obj->toDataSource(); + + unset($data['audiologid']); + unset($data['editlink']); + unset($data['state']); + unset($data['type']); + unset($data['length']); + unset($data['clean']); + unset($data['digitised']); + unset($data['deletelink']); + unset($data['trackno']); + unset($data['intro']); + unset($data['outro']); + + + //for manual SIS entries + if (!isset($data['trackid'])) { + $data['trackid'] = "SIS Manual"; + } + + if (is_array($data['album'])) { + $data['label'] = $data['album']['label']; + $data['album'] = $data['album']['title']; + } else { + $data['label'] = $data['record_label']; + unset($data['record_label']); + } + + $return[] = $data; + if (is_object($obj->getTrack())) { + $obj->getTrack()->removeInstance(); + } + $obj->removeInstance(); + unset($obj); + } + return $return; - } - - $obj = self::getInstance($id); - $data = $obj->toDataSource(); - - unset($data['audiologid']); - unset($data['editlink']); - unset($data['state']); - unset($data['type']); - unset($data['length']); - unset($data['clean']); - unset($data['digitised']); - unset($data['deletelink']); - unset($data['trackno']); - - if (is_array($data['album'])) { - $data['label'] = $data['album']['label']; - $data['album'] = $data['album']['title']; - } else { - $data['label'] = $data['record_label']; - unset($data['record_label']); - } - - $return[] = $data; - if (is_object($obj->getTrack())) { - $obj->getTrack()->removeInstance(); - } - $obj->removeInstance(); - unset($obj); } - return $return; - } - - /** - * Takes as input a result set of num_plays and trackid, and generates the extended Datasource output used by - * getTracklistStats(.*)() - * @return Array, 2D, with the inner dimension being a MyRadio_Track Datasource output, with the addition of: - * num_plays: The number of times the track was played - * total_playtime: The total number of seconds the track has been on air - * in_playlists: A CSV of playlists the Track is in - */ - private static function trackAmalgamator($result) { - $data = array(); - foreach ($result as $row) { - /** - * @todo Temporary hack due to lack of fkey on tracklist.track_rec - */ - try { - $trackobj = MyRadio_Track::getInstance($row['trackid']); - } catch (MyRadioException $e) { - continue; - } - $track = $trackobj->toDataSource(); - $track['num_plays'] = $row['num_plays']; - $track['total_playtime'] = $row['num_plays'] * $trackobj->getDuration(); - - $playlistobjs = iTones_Playlist::getPlaylistsWithTrack($trackobj); - $track['in_playlists'] = ''; - foreach ($playlistobjs as $playlist) { - $track['in_playlists'] .= $playlist->getTitle() . ', '; - } - - $data[] = $track; + /** + * Takes as input a result set of num_plays and trackid, and generates the extended Datasource output used by + * getTracklistStats(.*)(). + * + * @return Array, 2D, with the inner dimension being a MyRadio_Track Datasource output, with the addition of: + * num_plays: The number of times the track was played + * total_playtime: The total number of seconds the track has been on air + * in_playlists: A CSV of playlists the Track is in + */ + private static function trackAmalgamator($result, $playlists = true) + { + $data = []; + foreach ($result as $row) { + /* + * @todo Temporary hack due to lack of fkey on tracklist.track_rec + */ + try { + $trackobj = MyRadio_Track::getInstance($row['trackid']); + } catch (MyRadioException $e) { + continue; + } + $track = $trackobj->toDataSource(); + $track['num_plays'] = $row['num_plays']; + $track['total_playtime'] = $row['num_plays'] * $trackobj->getDuration(); + + $track['in_playlists'] = ''; + + if ($playlists) { + $playlistobjs = iTones_Playlist::getPlaylistsWithTrack($trackobj); + $track['in_playlists'] = implode(', ', array_map(function ($i) { + return $i->getTitle(); + }, $playlistobjs)); + } + + $data[] = $track; + } + + return $data; + } + + /** + * Get an amalgamation of all tracks played by Jukebox. This looks at all played tracks within the proposed + * timeframe, and outputs the play count of each Track, including the total time played. + * + * @param int $start Period to start log from. Default 0. + * @param int $end Period to end log from. Default time(). + * @param bool $include_playout Optional. If true, include statistics from when jukebox was not on air, + * i.e. when it was only feeding campus bars. Default true. + * @param bool $playlists Whether to get playlist membership metadata for tracks. + * + * @return Array, 2D, with the inner dimension being a MyRadio_Track Datasource output, with the addition of: + * num_plays: The number of times the track was played + * total_playtime: The total number of seconds the track has been on air + * in_playlists: A CSV of playlists the Track is in + */ + public static function getTracklistStatsForJukebox( + $start = null, + $end = null, + $include_playout = true, + $playlists = false + ) { + self::wakeup(); + + $start = $start === null ? '1970-01-01 00:00:00' : CoreUtils::getTimestamp($start); + $end = $end === null ? CoreUtils::getTimestamp() : CoreUtils::getTimestamp($end); + + $result = self::$db->fetchAll( + 'SELECT COUNT(trackid) AS num_plays, trackid FROM tracklist.tracklist + LEFT JOIN tracklist.track_rec ON tracklist.audiologid = track_rec.audiologid + WHERE source=\'j\' AND timestart >= $1 AND timestart <= $2 AND trackid IS NOT NULL' + .($include_playout ? '' : 'AND state != \'o\'') + .' GROUP BY trackid ORDER BY num_plays DESC', + [$start, $end] + ); + + return self::trackAmalgamator($result, $playlists); } - return $data; - } - - /** - * Get an amalgamation of all tracks played by Jukebox. This looks at all played tracks within the proposed timeframe, - * and outputs the play count of each Track, including the total time played. - * @param int $start Period to start log from. Default 0. - * @param int $end Period to end log from. Default time(). - * @param bool $include_playout Optional. Default true. If true, include statistics from when jukebox was not on air, - * i.e. when it was only feeding campus bars. - * @return Array, 2D, with the inner dimension being a MyRadio_Track Datasource output, with the addition of: - * num_plays: The number of times the track was played - * total_playtime: The total number of seconds the track has been on air - * in_playlists: A CSV of playlists the Track is in - */ - public static function getTracklistStatsForJukebox($start = null, $end = null, $include_playout = true) { - self::wakeup(); - - $start = $start === null ? '1970-01-01 00:00:00' : CoreUtils::getTimestamp($start); - $end = $end === null ? CoreUtils::getTimestamp() : CoreUtils::getTimestamp($end); - - $result = self::$db->fetch_all('SELECT COUNT(trackid) AS num_plays, trackid FROM tracklist.tracklist - LEFT JOIN tracklist.track_rec ON tracklist.audiologid = track_rec.audiologid - WHERE source=\'j\' AND timestart >= $1 AND timestart <= $2 AND trackid IS NOT NULL - ' . ($include_playout ? '' : 'AND state != \'o\'') . ' - GROUP BY trackid ORDER BY num_plays DESC', array($start, $end)); - - return self::trackAmalgamator($result); - } - - /** - * Get an amalgamation of all tracks played by BAPS. This looks at all played tracks within the proposed timeframe, - * and outputs the play count of each Track, including the total time played. - * @param int $start Period to start log from. Default 0. - * @param int $end Period to end log from. Default time(). - * @return Array, 2D, with the inner dimension being a MyRadio_Track Datasource output, with the addition of: - * num_plays: The number of times the track was played - * total_playtime: The total number of seconds the track has been on air - * in_playlists: A CSV of playlists the Track is in - */ - public static function getTracklistStatsForBAPS($start = null, $end = null) { - self::wakeup(); - - $start = $start === null ? '1970-01-01 00:00:00' : CoreUtils::getTimestamp($start); - $end = $end === null ? CoreUtils::getTimestamp() : CoreUtils::getTimestamp($end); - - $result = self::$db->fetch_all('SELECT COUNT(trackid) AS num_plays, trackid FROM tracklist.tracklist - LEFT JOIN tracklist.track_rec ON tracklist.audiologid = track_rec.audiologid - WHERE source=\'b\' AND timestart >= $1 AND timestart <= $2 AND trackid IS NOT NULL - GROUP BY trackid ORDER BY num_plays DESC', array($start, $end)); - return self::trackAmalgamator($result); - } - - /** - * Returns if the given track has been played in the last $time seconds - * - * @param MyRadio_Track $track - * @param int $time Optional. Default 21600 (6 hours) - */ - public static function getIfPlayedRecently(MyRadio_Track $track, $time = 21600) { - $result = self::$db->fetch_column('SELECT timestart FROM tracklist.tracklist - LEFT JOIN tracklist.track_rec ON tracklist.audiologid = track_rec.audiologid - WHERE timestart >= $1 AND trackid = $2', array(CoreUtils::getTimestamp(time() - $time), $track->getID())); - - return sizeof($result) !== 0; - } - - /** - * Check whether queuing the given Track for playout right now would be a - * breach of our PPL Licence. - * - * The PPL Licence states that a maximum of two songs from an artist or album - * in a two hour period may be broadcast. Any more is a breach of this licence - * so we should really stop doing it. - * - * @param MyRadio_Track $track - * @param bool $include_queue If true, will include the tracks in the iTones - * queue. - * @param int $time. If set, will check if playing it at $time would be a/was - * a breach. No, this isn't magic and know the future accurately. - * @return bool - */ - public static function getIfAlbumArtistCompliant(MyRadio_Track $track, $include_queue = true, $time = null) { - if ($time == null) { - $time = time(); + + /** + * Get an amalgamation of all tracks played by various sources (just not jukebox). This looks at all played tracks within the proposed timeframe, + * and outputs the play count of each Track, including the total time played. + * + * @param int $start Period to start log from. Default 0. + * @param int $end Period to end log from. Default time(). + * @param bool $playlists Whether to get playlist membership metadata for the tracks. + * + * @return Array, 2D, with the inner dimension being a MyRadio_Track Datasource output, with the addition of: + * num_plays: The number of times the track was played + * total_playtime: The total number of seconds the track has been on air + * in_playlists: A CSV of playlists the Track is in + */ + public static function getTracklistStatsForBAPS($start = null, $end = null, $playlists = false) + { + self::wakeup(); + + $start = $start === null ? '1970-01-01 00:00:00' : CoreUtils::getTimestamp($start); + $end = $end === null ? CoreUtils::getTimestamp() : CoreUtils::getTimestamp($end); + + $result = self::$db->fetchAll( + 'SELECT COUNT(trackid) AS num_plays, trackid FROM tracklist.tracklist + LEFT JOIN tracklist.track_rec ON tracklist.audiologid = track_rec.audiologid + WHERE source != \'j\' AND timestart >= $1 AND timestart <= $2 AND trackid IS NOT NULL + GROUP BY trackid ORDER BY num_plays DESC', + [$start, $end] + ); + + return self::trackAmalgamator($result, $playlists); } - $timeout = CoreUtils::getTimestamp($time - (3600 * 2)); //Two hours ago /** - * The title check is a hack to work around our default album - * being URY Downloads + * Returns if the given track has been played in the last $time seconds. + * + * @param MyRadio_Track $track + * @param int $time Optional. Default 21600 (6 hours) */ - $result = self::$db->fetch_column('SELECT COUNT(*) FROM tracklist.tracklist - LEFT JOIN tracklist.track_rec USING (audiologid) - LEFT JOIN (SELECT recordid, title AS album FROM public.rec_record) AS t1 - USING (recordid) - LEFT JOIN public.rec_track USING (trackid) - WHERE (rec_track.recordid=$1 OR rec_track.artist=$2) - AND timestart >= $3 - AND timestart < $4 - AND album NOT ILIKE \''.Config::$short_name.' Downloads%\'', array($track->getAlbum()->getID(), - $track->getArtist(), $timeout, CoreUtils::getTimestamp($time))); - - if ($include_queue) { - foreach (iTones_Utils::getTracksInAllQueues() as $req) { - if (empty($req['trackid'])) { - continue; + public static function getIfPlayedRecently(MyRadio_Track $track, $time = 21600) + { + $result = self::$db->fetchColumn( + 'SELECT timestart FROM tracklist.tracklist + LEFT JOIN tracklist.track_rec ON tracklist.audiologid = track_rec.audiologid + WHERE timestart >= $1 AND trackid = $2', + [CoreUtils::getTimestamp(time() - $time), $track->getID()] + ); + + return sizeof($result) !== 0; + } + + /** + * Check whether queuing the given Track for playout right now would be a + * breach of our PPL Licence. + * + * The PPL Licence states that a maximum of three songs from an album (and no + * more than two consecutively) AND a maximum of four songs by an artist (and + * no more than three consecutively) may be broadcast in any two hour period. + * Any more is a breach of this licence, so we should really stop doing it. + * + * @param MyRadio_Track $track + * @param bool $include_queue If true, will include the tracks in the iTones queue. + * @param int $time If set, will check if playing it at $time would be a/was a breach. + * No, this isn't magic and know the future accurately. + * + * @return bool + */ + public static function getIfAlbumArtistCompliant(MyRadio_Track $track, $include_queue = true, $time = null) + { + if ($time == null) { + $time = time(); } - $t = MyRadio_Track::getInstance($req['trackid']); + $timeout = CoreUtils::getTimestamp($time - 3600); //One hour ago - /** + /* * The title check is a hack to work around our default album * being URY Downloads */ - if (($t->getAlbum()->getID() == $track->getAlbum()->getID() && stristr($t->getAlbum()->getTitle(), Config::$short_name.' Downloads') === false) or $t->getArtist() === $track->getArtist()) { - $result[0] ++; + $result = self::$db->fetchColumn( + 'SELECT COUNT(*) FROM tracklist.tracklist + LEFT JOIN tracklist.track_rec USING (audiologid) + LEFT JOIN (SELECT recordid, title AS album FROM public.rec_record) AS t1 + USING (recordid) + LEFT JOIN public.rec_track USING (trackid) + WHERE (rec_track.recordid=$1 OR rec_track.artist=$2) + AND timestart >= $3 + AND timestart < $4 + AND album NOT ILIKE \''.Config::$short_name.' Downloads%\'', + [ + $track->getAlbum()->getID(), + $track->getArtist(), + $timeout, + CoreUtils::getTimestamp($time), + ] + ); + + if ($include_queue) { + foreach (iTones_Utils::getTracksInAllQueues() as $req) { + if (empty($req['trackid'])) { + continue; + } + $t = MyRadio_Track::getInstance($req['trackid']); + + /* + * The title check is a hack to work around our default album + * being URY Downloads + */ + if (($t->getAlbum()->getID() === $track->getAlbum()->getID() + && stristr($t->getAlbum()->getTitle(), Config::$short_name.' Downloads') === false) + || $t->getArtist() === $track->getArtist() + ) { + ++$result[0]; + } + } } - } - } - return ($result[0] < 2); - } - - public function toDataSource($full = false) { - if (is_array($this->track)) { - $return = $this->track; - } else { - $return = $this->getTrack()->toDataSource($full); + return $result[0] == 0; } - $return['starttime'] = date('d/m/Y H:i:s', $this->getStartTime()); - //$return['endtime'] = $this->getEndTime(); - $return['state'] = $this->state; - $return['audiologid'] = $this->audiologid; - return $return; - } + public function toDataSource($mixins = []) + { + if (is_array($this->track)) { + // If manually tracklisted, track_norec table is just a plain text album. + // Make it an array like regular tracks. + if (!is_array($this->track["album"])) { + $album = [ + "title" => $this->track["album"], + "recordid" => null, + "artist" => $this->track["artist"], + "cdid" => null, + "date_added" => date('d/m/Y H:i', $this->getStartTime()), + "date_released" => null, + "format" => "Album", + "last_modified" => null, + "location" => null, + "media" => "Manual Tracklist", + "member_add" => null, + "member_edit" => null, + "record_label" => "", + "status" => "digital only", + "label" => "Manual Tracklist" + ]; + $this->track["album"] = $album; + } + $return = $this->track; + } else { + $return = $this->getTrack()->toDataSource($mixins); + } + $return['time'] = $this->getStartTime(); + $return['starttime'] = date('d/m/Y H:i:s', $this->getStartTime()); + $return['endtime'] = $this->getEndTime() == null ? null : date('d/m/Y H:i:s', $this->getEndTime()); + $return['state'] = $this->state; + $return['audiologid'] = $this->audiologid; + return $return; + } } diff --git a/src/Classes/ServiceAPI/MyRadio_TrainingStatus.php b/src/Classes/ServiceAPI/MyRadio_TrainingStatus.php index a01fd33da..db045ef97 100644 --- a/src/Classes/ServiceAPI/MyRadio_TrainingStatus.php +++ b/src/Classes/ServiceAPI/MyRadio_TrainingStatus.php @@ -1,277 +1,396 @@ - * @package MyRadio_Core */ +class MyRadio_TrainingStatus extends ServiceAPI +{ + /** + * The ID of the Training Status. + * + * @var int + */ + private $presenterstatusid; + + /** + * The Title of the Training Status. + * + * @var string + */ + private $descr; + + /** + * The numerical weight of the Training Status. + * + * The bigger the number, the further down the list it appears, + * and the more senior the status. + * + * @var int + */ + private $ordering; + + /** + * The Training Status a member must have before achieving this one. + * + * @var MyRadio_TrainingStatus + */ + private $depends; + + /** + * The Training Status a member must have in order to award this one. + * + * @var MyRadio_TrainingStatus + */ + private $can_award; + + /** + * A long description of the capabilities of a member with this Training Status. + * + * @var string + */ + private $detail; + + /** + * An archived training status will still be visible for people who already have it, but cannot be newly awarded. + * + * @var bool + */ + private bool $archived; + + /** + * Users who have achieved this Training Status. + * + * This array is initialised the first time it is requested. + * + * @var int[] + */ + private $awarded_to = null; + + /** + * Permissions granted to Users with this Training Status. + * + * @var int[] + */ + private $permissions; + + /** + * Create a new TrainingStatus object. Generally, you should use getInstance. + * + * @param int $statusid The ID of the TrainingStatus. + * + * @throws MyRadioException + */ + protected function __construct($statusid) + { + $this->presenterstatusid = (int) $statusid; + + $result = self::$db->fetchOne('SELECT * FROM public.l_presenterstatus WHERE presenterstatusid=$1', [$statusid]); + + if (empty($result)) { + throw new MyRadioException('The specified Training Status ('.$statusid.') does not seem to exist', 404); + + return; + } -class MyRadio_TrainingStatus extends ServiceAPI { - /** - * The ID of the Training Status - * @var int - */ - private $presenterstatusid; - - /** - * The Title of the Training Status - * @var String - */ - private $descr; - - /** - * The numerical weight of the Training Status. - * - * The bigger the number, the further down the list it appears, - * and the more senior the status. - * - * @var int - */ - private $ordering; - - /** - * The Training Status a member must have before achieving this one. - * - * @var MyRadio_TrainingStatus - */ - private $depends; - - /** - * The Training Status a member must have in order to award this one. - * - * @var MyRadio_TrainingStatus - */ - private $can_award; - - /** - * A long description of the capabilities of a member with this Training Status. - * - * @var String - */ - private $detail; - - /** - * Users who have achieved this Training Status. - * - * This array is initialised the first time it is requested. - * - * @var int[] - */ - private $awarded_to = null; - - /** - * Permissions granted to Users with this Training Status. - * - * @var int[] - */ - private $permissions; - - /** - * Create a new TrainingStatus object. Generally, you should use getInstance. - * - * @param int $statusid The ID of the TrainingStatus. - * @throws MyRadioException - */ - protected function __construct($statusid) { - $this->presenterstatusid = (int)$statusid; - - $result = self::$db->fetch_one('SELECT * FROM public.l_presenterstatus WHERE presenterstatusid=$1', array($statusid)); - - if (empty($result)) { - throw new MyRadioException('The specified Training Status ('.$statusid.') does not seem to exist'); - return; + $this->descr = $result['descr']; + $this->ordering = (int) $result['ordering']; + $this->detail = $result['detail']; + $this->archived = isset($result['archived']) && $result['archived'] === 't'; + + $this->depends = empty($result['depends']) ? null : $result['depends']; + $this->can_award = empty($result['can_award']) ? null : $result['can_award']; + } + + /** + * Get the presenterstatusid. + * + * @return int + */ + public function getID() + { + return $this->presenterstatusid; } - - $this->descr = $result['descr']; - $this->ordering = (int)$result['ordering']; - $this->detail = $result['detail']; - - $this->depends = empty($result['depends']) ? null : $result['depends']; - $this->can_award = empty($result['can_award']) ? null : $result['can_award']; - - $this->permissions = self::$db->fetch_column('SELECT typeid FROM public.auth_trainingstatus WHERE presenterstatusid=$1', [$statusid]); - } - - /** - * Get the presenterstatusid - * @return int - */ - public function getID() { - return $this->presenterstatusid; - } - - /** - * Get the Title - * - * Internally, this is the `descr` field for compatibility. - * - * @return String - */ - public function getTitle() { - return $this->descr; - } - - /** - * Get details about the TrainingStatus' purpose - * - * @return String - */ - public function getDetail() { - return $this->detail; - } - - /** - * Get the permissions this Training Status grants - * - * @return int[] - */ - public function getPermissions() { - return $this->permissions; - } - - /** - * Get the TrainingStatus a member must have before being awarded this one, if any. - * - * Returns null if there is no dependency. - * - * @return MyRadio_TrainingStatus - */ - public function getDepends() { - return empty($this->depends) ? null : self::getInstance($this->depends); - } - - /** - * Checks if the user has the Training Status the one depends on. - * - * @param MyRadio_User $user Default current User. - * @return bool True if no dependency or dependency gained, false otherwise. - */ - public function hasDependency(MyRadio_User $user = null) { - if ($user === null) { - $user = MyRadio_User::getInstance(); + + /** + * Get the Title. + * + * Internally, this is the `descr` field for compatibility. + * + * @return string + */ + public function getTitle() + { + return $this->descr; } - return ($this->getDepends() == null or $this->getDepends()->isAwardedTo($user)); - } - - /** - * Gets the TrainingStatus a member must have before awarding this one. - * - * @return MyRadio_TrainingStatus - */ - public function getAwarder() { - return self::getInstance($this->can_award); - } - - /** - * Returns if the User can Award this Training Status - * @param MyRadio_User $user - * @return bool - */ - public function canAward(MyRadio_User $user = null) { - if ($user === null) { - $user = MyRadio_User::getInstance(); + + /** + * Get details about the TrainingStatus' purpose. + * + * @return string + */ + public function getDetail() + { + return $this->detail; } - - return $this->getAwarder()->isAwardedTo($user); - } - - /** - * Get an array of all UserTrainingStatuses this TrainingStatus has been - * awarded to, and hasn't been revoked from. - * - * @param int $ids If true, just returns User Training Status IDs instead of - * UserTrainingStatuses. - * @return MyRadio_User[]|int - */ - public function getAwardedTo($ids = false) { - if ($this->awarded_to === null) { - $this->awarded_to = self::$db->fetch_column( - 'SELECT memberpresenterstatusid FROM member_presenterstatus - WHERE presenterstatusid=$1 AND revokedtime IS NULL', [$this->getID()]); + + /** + * Is this training status archived? + * @return bool + */ + public function isArchived(): bool + { + return $this->archived; } - - return $ids ? $this->awarded_to : - MyRadio_UserTrainingStatus::resultSetToObjArray($this->awarded_to); - } - - /** - * Checks if the User has this Training Status - * - * @param MyRadio_User $user - * @return bool - */ - public function isAwardedTo(MyRadio_User $user = null) { - if ($user === null) { - $user = MyRadio_User::getInstance(); + + /** + * Get the permissions this Training Status grants. + * + * @return int[] + */ + public function getPermissions() + { + if (!isset($this->permissions)) { + $this->permissions = array_map( + 'intval', + self::$db->fetchColumn( + 'SELECT typeid FROM public.auth_trainingstatus WHERE presenterstatusid=$1', + [$this->presenterstatusid] + ) + ); + } + return $this->permissions; } - - return in_array($user->getID(), array_map(function($x){ - return $x->getAwardedTo()->getID();}, - $this->getAwardedTo())); - } - - /** - * Get an array of properties for this TrainingStatus. - * - * @return Array - */ - public function toDataSource() { - return array( - 'status_id' => $this->getID(), - 'title' => $this->getTitle(), - 'detail' => $this->getDetail(), - 'depends' => $this->getDepends(), - 'awarded_by' => $this->getAwarder() - ); - } - - /** - * Get all Training Statuses. - * @return MyRadio_TrainingStatus[] - */ - public static function getAll() { - return self::resultSetToObjArray(self::$db->fetch_column( - 'SELECT presenterstatusid FROM public.l_presenterstatus ' - . 'ORDER BY presenterstatusid')); - } - - /** - * The all the Training Statuses the User can currently be awarded. - * - * A User cannot award themselves statuses. - * - * @param MyRadio_User $to The User getting the Training Status. - * @param MyRadio_User $by The User awarding the Training Status. - * @return MyRadio_TrainingStatus[] - */ - public static function getAllAwardableTo(MyRadio_User $to, MyRadio_User $by = null) { - if ($by === null) { - $by = MyRadio_User::getInstance(); + + /** + * Get the TrainingStatus a member must have before being awarded this one, if any. + * + * Returns null if there is no dependency. + * + * @return MyRadio_TrainingStatus + */ + public function getDepends() + { + return empty($this->depends) ? null : MyRadio_TrainingStatus::getInstance($this->depends); } - if ($to === $by) { - return []; + + /** + * Checks if the user has the Training Status the one depends on. + * + * @param MyRadio_User $user Default current User. + * + * @return bool True if no dependency or dependency gained, false otherwise. + */ + public function hasDependency(MyRadio_User $user = null) + { + if ($user === null) { + $user = MyRadio_User::getInstance(); + } + + return $this->getDepends() == null or $this->getDepends()->isAwardedTo($user); } - - $statuses = []; - foreach (self::getAll() as $status) { - if ((!$status->isAwardedTo($to)) && $status->hasDependency($to) - && $status->canAward($by)) { - $statuses[] = $status; - } + + /** + * Gets the TrainingStatus a member must have before awarding this one. + * + * @return MyRadio_TrainingStatus + */ + public function getAwarder() + { + return MyRadio_TrainingStatus::getInstance($this->can_award); } - return $statuses; - } -} \ No newline at end of file + /** + * Returns if the User can Award this Training Status. + * + * @param MyRadio_User $user + * + * @return bool + */ + public function canAward(MyRadio_User $user = null) + { + if ($this->isArchived()) { + return false; + } + + if ($user === null) { + $user = MyRadio_User::getInstance(); + } + + if ($user->hasAuth(AUTH_AWARDANYTRAINING)) { + // I am become trainer, doer of trainings + return true; + } + + return $this->getAwarder()->isAwardedTo($user); + } + + /** + * Get an array of all UserTrainingStatuses this TrainingStatus has been + * awarded to, and hasn't been revoked from. + * + * @param int $ids If true, just returns User Training Status IDs instead of + * UserTrainingStatuses. + * + * @return MyRadio_User[]|int + */ + public function getAwardedTo($ids = false) + { + if ($this->awarded_to === null) { + $this->awarded_to = self::$db->fetchColumn( + 'SELECT memberpresenterstatusid FROM member_presenterstatus + WHERE presenterstatusid=$1 AND revokedtime IS NULL', + [$this->getID()] + ); + } + + return $ids ? $this->awarded_to : MyRadio_UserTrainingStatus::resultSetToObjArray($this->awarded_to); + } + + /** + * Checks if the User has this Training Status. + * + * @param MyRadio_User $user + * + * @return bool + */ + public function isAwardedTo(MyRadio_User $user = null) + { + if ($user === null) { + $user = MyRadio_User::getInstance(); + } + + return in_array( + $user->getID(), + array_map( + function ($x) { + return $x->getAwardedTo()->getID(); + }, + $this->getAwardedTo() + ) + ); + } + + /** + * Get an array of properties for this TrainingStatus. + * @param array $mixins Mixins. Unused. + * @return array + */ + public function toDataSource($mixins = []) + { + return [ + 'status_id' => $this->getID(), + 'title' => $this->getTitle(), + 'detail' => $this->getDetail(), + 'depends' => $this->getDepends(), + 'awarded_by' => $this->getAwarder(), + ]; + } + + /** + * Get all Training Statuses. + * + * @return MyRadio_TrainingStatus[] + */ + public static function getAll() + { + return self::resultSetToObjArray( + self::$db->fetchColumn( + 'SELECT presenterstatusid FROM public.l_presenterstatus + ORDER BY presenterstatusid' + ) + ); + } + + /** Get all Training Statuses the user can train as options for MyRadioFormField + * + * @param MyRadio_User $user The user trying to award status + * + * @return array + */ + + public static function getOptionsToTrain($user) + { + $options = []; + foreach (self::getAll() as $status) { + if ($status->isArchived()) { + continue; + } + if ($status->canAward($user)) { + $options[] = ["value" => $status->getID(), "text" => $status->getTitle()]; + } + } + return $options; + } + + /** + * The all the Training Statuses the User can currently be awarded. + * + * A User cannot award themselves statuses. + * + * @param MyRadio_User $to The User getting the Training Status. + * @param MyRadio_User $by The User awarding the Training Status. + * + * @return MyRadio_TrainingStatus[] + */ + public static function getAllAwardableTo(MyRadio_User $to, MyRadio_User $by = null) + { + if ($by === null) { + $by = MyRadio_User::getInstance(); + } + if ($to === $by) { + return []; + } + + $statuses = []; + foreach (self::getAll() as $status) { + if ($status->isArchived()) { + continue; + } + if ((!$status->isAwardedTo($to)) + && $status->hasDependency($to) + && $status->canAward($by) + ) { + $statuses[] = $status; + } + } + + return $statuses; + } + + /** + * All the Training Status a User can be awarded, regardless of who's awarding + * + * @param MyRadio_User $to The User being awarded the training + * + * @return MyRadio_TrainingStatus[] + */ + + public static function getAllToBeEarned(MyRadio_User $to) + { + $statuses = []; + foreach (self::getAll() as $status) { + if ($status->isArchived()) { + continue; + } + if ((!$status->isAwardedTo($to)) + && $status->hasDependency($to) + ) { + $statuses[] = $status; + } + } + + return $statuses; + } +} diff --git a/src/Classes/ServiceAPI/MyRadio_Type.php b/src/Classes/ServiceAPI/MyRadio_Type.php index 8a2304ac8..7fa3e8902 100644 --- a/src/Classes/ServiceAPI/MyRadio_Type.php +++ b/src/Classes/ServiceAPI/MyRadio_Type.php @@ -1,59 +1,62 @@ - * @package MyRadio_Core - * @uses \Database + * + * @uses \Database */ -abstract class MyRadio_Type extends ServiceAPI { - /** - * The machine-readable name of the type. - * @var String - */ - private $name; +abstract class MyRadio_Type extends ServiceAPI +{ + /** + * The machine-readable name of the type. + * + * @var string + */ + private $name; - /** - * The human-readable description/descriptive name of the type. - * @var String - */ - private $description; + /** + * The human-readable description/descriptive name of the type. + * + * @var string + */ + private $description; - /** - * This should be included in the implementor's __construct function. - * - * @param $name The machine-readable name of the type. - * @param $description The human-readable description/descriptive name of - * the type. - */ - protected function construct_type($name, $description) { - $this->name = $name; - $this->description = $description; - } + /** + * This should be included in the implementor's __construct function. + * + * @param $name The machine-readable name of the type. + * @param $description The human-readable description/descriptive name of + * the type. + */ + protected function constructType($name, $description) + { + $this->name = $name; + $this->description = $description; + } - /** - * Retrieves this type's machine readable name. - * - * @return The name. - */ - public function getName() { - return $this->name; - } + /** + * Retrieves this type's machine readable name. + * + * @return The name. + */ + public function getName() + { + return $this->name; + } - /** - * Retrieves this type's human readable description. - * - * @return The description. - */ - public function getDescription() { - return $this->description; - } + /** + * Retrieves this type's human readable description. + * + * @return The description. + */ + public function getDescription() + { + return $this->description; + } } diff --git a/src/Classes/ServiceAPI/MyRadio_User.php b/src/Classes/ServiceAPI/MyRadio_User.php old mode 100644 new mode 100755 index 8720c90b4..025461b84 --- a/src/Classes/ServiceAPI/MyRadio_User.php +++ b/src/Classes/ServiceAPI/MyRadio_User.php @@ -1,187 +1,253 @@ - * @package MyRadio_Core - * @uses \Database - * @uses \CacheProvider + * It is not a singleton for Impersonate purposes. + * + * @uses \Database + * @uses \CacheProvider */ -class MyRadio_User extends ServiceAPI { +class MyRadio_User extends ServiceAPI implements APICaller +{ + use MyRadio_APICaller_Common; /** - * Stores the user's memberid - * @var int + * Stores the currently logged in User's object after first use. + * @var MyRadio_User|boolean */ - private $memberid; - + private static $current_user; /** - * Stores the User's permissions - * @var Array + * Stores the user's memberid. + * + * @var int */ - private $permissions; + private $memberid; /** - * Stores the User's first name - * @var String + * Stores the User's first name. + * + * @var string */ private $fname; /** - * Stores the User's last name - * @var String + * Stores the User's last name. + * + * @var string */ private $sname; /** - * Stores the User's gender (either 'm' or 'f') - * @var String - */ - private $sex; - - /** - * Stores the User's preferred contact address - * @var String + * Stores the User's preferred contact address. + * + * @var string */ private $email; /** - * Stores the ID of the User's college + * Stores the ID of the User's college. + * * @var int */ private $collegeid; /** - * Stores the String name of the User's college - * @var String + * Stores the String name of the User's college. + * + * @var string */ private $college; /** - * Stores the User's phone number - * @var String + * Stores the User's phone number. + * + * @var string */ private $phone; /** - * Stores whether the User wants to receive email + * Stores whether the User wants to receive email. + * * @var bool */ private $receive_email; /** - * Stores the User's username on internal servers, if they have one - * @var String + * Stores the User's username on internal servers, if they have one. + * + * @var string */ private $local_name; /** * Stores the User's internal email alias, if they have one. * The mail server actually uses this to calculate the values. - * @var String + * + * @var string */ private $local_alias; /** - * Stores the User's eduroam ID - * @var String + * Stores the User's eduroam ID. + * + * @var string */ private $eduroam; /** - * Stores whether or not the account is locked out and cannot be used + * Stores whether or not the account is locked out and cannot be used. + * * @var bool */ private $account_locked; /** - * Stores payment information about the User + * Stores payment information about the User. + * * @var array */ private $payment; /** - * Stores all the User's officerships + * Stores all the User's officerships. + * * @var array */ private $officerships; /** - * Stores all the training data / status of the user + * Stores all the training data / status of the user. + * * @var array */ private $training; /** - * Stores the time the User joined URY + * Stores the time the User joined URY. + * * @var int */ private $joined; /** * Stores the datetime the User last logged in on. - * @var String timestamp with timezone + * + * @var string timestamp with timezone */ private $last_login; /** - * Photoid of the User's profile photo + * Photoid of the User's profile photo. + * * @var int */ private $profile_photo; /** * Stores a User's biography. HTML, so ensure you | raw if outputting! - * @var String + * + * @var string */ private $bio = ''; /** * Initialised on first request, stores a list of Show IDs the User has. - * + * * @var int[] */ private $shows; + /** + * Users radio time, calculated from signed in shows + * + * @var mixed + */ + private $radioTime; + /** * The Authentication Provider that should be used when logging this user in - * default Null (any) - * - * @var String|null + * default Null (any). + * + * @var string|null */ private $auth_provider; /** * If true, this user needs to change their password at next logon. - * - * @var boolean + * + * @var bool */ private $require_password_change; /** - * Initiates the User variables + * True if user has signed Presenter Contract. + * + * @var bool + */ + private $contract_signed; + + /** + * True if user has agreed to our privacy statement. + * + * @var bool + */ + private $gdpr_accepted; + + /** + * default if user can be deleted + * informed if user has been warned about deletion + * optout if user has opted out of deletion + * deleted if user has been deleted + * + * @var enum + */ + private $data_removal; + + /** + * If true, hides user data from API. + * + * @var bool + */ + private $hide_profile; + + /** + * Initiates the User variables. + * * @param int $memberid The ID of the member to initialise */ - protected function __construct($memberid) { - $this->memberid = (int)$memberid; + protected function __construct($memberid) + { + $this->memberid = (int) $memberid; //Get the base data - $data = self::$db->fetch_one( - 'SELECT fname, sname, sex, college AS collegeid, l_college.descr AS college, - phone, email, receive_email, local_name, local_alias, eduroam, - account_locked, last_login, joined, profile_photo, bio, - auth_provider, require_password_change - FROM member, l_college - WHERE memberid=$1 - AND member.college = l_college.collegeid - LIMIT 1', array($memberid)); + $data = self::$db->fetchOne( + 'SELECT fname, sname, college AS collegeid, l_college.descr AS college, + phone, email, receive_email::boolean::text, local_name, local_alias, eduroam, + account_locked::boolean::text, last_login, joined, profile_photo, bio, + auth_provider, require_password_change::boolean::text, contract_signed::boolean::text, gdpr_accepted::boolean::text, + data_removal, hide_profile::boolean::text + FROM member, l_college + WHERE memberid=$1 + AND member.college = l_college.collegeid + LIMIT 1', + [$this->memberid] + ); if (empty($data)) { //This user doesn't exist throw new MyRadioException('The specified User does not appear to exist.', 404); @@ -192,72 +258,57 @@ protected function __construct($memberid) { $this->$key = (int) strtotime($value); } elseif (filter_var($value, FILTER_VALIDATE_INT)) { $this->$key = (int) $value; - } elseif ($value === 't') { + } elseif ($value === 'true') { $this->$key = true; - } elseif ($value === 'f') { + } elseif ($value === 'false') { $this->$key = false; } else { $this->$key = $value; } } + } - //Get the user's permissions - $this->permissions = self::$db->fetch_column('SELECT lookupid FROM auth_officer - WHERE officerid IN (SELECT officerid FROM member_officer - WHERE memberid=$1 AND from_date <= now() AND - (till_date IS NULL OR till_date > now()- interval \'1 month\')) - UNION SELECT lookupid FROM auth WHERE memberid=$1 AND - starttime < now() AND (endtime IS NULL OR endtime >= now())', - array($memberid)); - - $this->payment = self::$db->fetch_all('SELECT year, paid - FROM member_year - WHERE memberid = $1 - ORDER BY year ASC;', array($memberid)); - - // Get the User's officerships - $this->officerships = self::$db->fetch_all('SELECT officerid,officer_name,teamid,from_date,till_date - FROM member_officer - INNER JOIN officer - USING (officerid) - WHERE memberid = $1 - AND type!=\'m\' - ORDER BY from_date,till_date;', array($memberid)); - - // Get Training info all into array - $this->training = self::$db->fetch_column('SELECT memberpresenterstatusid - FROM public.member_presenterstatus LEFT JOIN public.l_presenterstatus USING (presenterstatusid) - WHERE memberid=$1 ORDER BY ordering, completeddate ASC', array($this->memberid)); - - if ($this->isCurrentlyPaid()) { - //Add training permissions, but only if currently paid - foreach ($this->getAllTraining() as $training) { - $this->permissions = array_merge($this->permissions, $training->getPermissions()); - } - } + public function clearPermissionCache() + { + $this->permissions = null; + return $this; + } + public function clearOfficershipCache() + { + $this->officerships = null; + return $this; + } + public function clearTrainingCache() + { + $this->training = null; + return $this; } /** - * Returns if the User is currently an Officer - * + * Returns if the User is currently an Officer. + * * @return bool */ - public function isOfficer() { + public function isOfficer() + { foreach ($this->getOfficerships() as $officership) { - if (empty($officership['till_date']) or $officership['till_date'] >= time()) { + if (empty($officership->getTillDate()) or $officership->getTillDate() >= time()) { return true; } } + return false; } /** - * Returns if the user is Studio Trained - * @return boolean + * Returns if the user is Studio Trained. + * + * @return bool */ - public function isStudioTrained() { + public function isStudioTrained() + { foreach ($this->getAllTraining(true) as $train) { - if ($train->getID() == 1) { + if ($train->getTitle() == "Studio Trained") { return true; } } @@ -266,12 +317,14 @@ public function isStudioTrained() { } /** - * Returns if the user is Studio Demoed - * @return boolean + * Returns if the user is Studio Demoed. + * + * @return bool */ - public function isStudioDemoed() { + public function isStudioDemoed() + { foreach ($this->getAllTraining(true) as $train) { - if ($train->getID() == 2) { + if ($train->getTitle() === "Studio Trained" || $train->getTitle() === "WebStudio Trained") { return true; } } @@ -280,12 +333,14 @@ public function isStudioDemoed() { } /** - * Returns if the user is a Trainer - * @return boolean + * Returns if the user is a Trainer. + * + * @return bool */ - public function isTrainer() { + public function isTrainer() + { foreach ($this->getAllTraining(true) as $train) { - if ($train->getID() == 3) { + if ($train->getTitle() === "Trainer") { return true; } } @@ -295,11 +350,29 @@ public function isTrainer() { /** * Get all types of training the User has. - * + * * @param bool $ignore_revoked If true, Revoked statuses will not be included. + * * @return Array[MyRadio_UserTrainingStatus] */ - public function getAllTraining($ignore_revoked = false) { + public function getAllTraining($ignore_revoked = false) + { + + if($this->isProfileHidden()){ + return []; + } + + if (!$this->training) { + // Get Training info all into array + $this->training = self::$db->fetchColumn( + 'SELECT memberpresenterstatusid + FROM public.member_presenterstatus LEFT JOIN public.l_presenterstatus USING (presenterstatusid) + WHERE memberid=$1 ORDER BY ordering, completeddate ASC', + [$this->getID()] + ); + $this->updateCacheObject(); + } + if ($ignore_revoked) { $data = []; foreach (MyRadio_UserTrainingStatus::resultSetToObjArray($this->training) as $train) { @@ -307,6 +380,7 @@ public function getAllTraining($ignore_revoked = false) { $data[] = $train; } } + return $data; } else { return MyRadio_UserTrainingStatus::resultSetToObjArray($this->training); @@ -316,118 +390,186 @@ public function getAllTraining($ignore_revoked = false) { /** * Returns whether the User has paid the correct amount to be * a full member in the current year. + * * @return bool */ - public function isCurrentlyPaid() { + public function isCurrentlyPaid() + { foreach ($this->getAllPayments() as $payment) { if ($payment['year'] == CoreUtils::getAcademicYear()) { return $payment['paid'] >= Config::$membership_fee; } } + return false; } /** * Return whether the User is currently has any shows. - * @return boolean + * + * @return bool */ - public function hasShow() { + public function hasShow() + { return sizeof($this->getShows()) !== 0; } /** - * Returns the User's memberid + * Returns the User's memberid. + * * @return int The User's memberid */ - public function getID() { + public function getID() + { return $this->memberid; } /** - * Returns the User's first name - * @return string The User's first name + * Returns the User's first name. + * + * @return string The User's first name */ - public function getFName() { + public function getFName() + { return $this->fname; } /** - * Returns the User's surname - * @return string The User's surname + * Returns the User's surname. + * + * @return string The User's surname */ - public function getSName() { + public function getSName() + { return $this->sname; } /** - * Returns the User's full name as one string - * @return string The User's name - */ - public function getName() { - return $this->fname . ' ' . $this->sname; - } - - /** - * Returns the User's sex - * @return string The User's sex + * Returns the User's full name as one string. + * + * @return string The User's name */ - public function getSex() { - return $this->sex; + public function getName() + { + return $this->fname.' '.$this->sname; } - public function getLastLogin() { + public function getLastLogin() + { return $this->last_login; } /** - * Returns the User's profile Photo (or null if there is not one) + * Returns the User's profile Photo (or null if there is not one). + * * @return MyRadio_Photo */ - public function getProfilePhoto() { + public function getProfilePhoto() + { if (!empty($this->profile_photo)) { return MyRadio_Photo::getInstance($this->profile_photo); } else { - return null; + return; } } /** - * Returns all the user's active permission flags - * @return Array - */ - public function getPermissions() { + * Returns all the user's active permission flags. + * @todo this doesn't include permissions assigned by IP (cf. login.php#L86-L99) + * It seems like those are only used by scripts though + * + * @return int[] + */ + public function getPermissions() + { + if (!$this->permissions) { + //Get the user's permissions + $permissions = array_map( + 'intval', + self::$db->fetchColumn( + 'SELECT lookupid FROM auth_officer + WHERE officerid IN (SELECT officerid FROM member_officer + WHERE memberid=$1 + AND from_date <= now() + AND (till_date IS NULL OR till_date > now()- interval \'1 month\')) + UNION SELECT lookupid FROM auth + WHERE memberid=$1 + AND starttime < now() + AND (endtime IS NULL OR endtime >= now())', + [$this->getID()] + ) + ); + + if ($this->isCurrentlyPaid()) { + //Add training permissions, but only if currently paid + foreach ($this->getAllTraining() as $training) { + $permissions = array_merge($permissions, $training->getPermissions()); + } + } + + $this->permissions = array_values(array_unique($permissions)); + + $this->updateCacheObject(); + } + return $this->permissions; } + /** + * Returns information about this user's permission flags. + * + * The result has the format: + * - typeid: permission ID + * - descr: the permission's description + * - phpconstant: the PHP constant for that permission + * + * @return array + */ + public function getPermissionsInfo() + { + $perms = $this->getPermissions(); + $params = '{' . implode(',', $perms) . '}'; + return self::$db->fetchAll( + "SELECT typeid, descr, phpconstant + FROM l_action + WHERE typeid = ANY('$params'::int[])" // I hate this - but it's safe + ); + } + /** * Returns the User's email address. If the email address is null, it is * assumed their eduroam address is the preferred contact method. - * + * * This is the address that emails to the User actually *go to*. It is not * the address that should be shown to your standard member. * See getPublicEmail(). - * - * @todo hardcoded domains here. - * @return string The User's email + * + * @todo hardcoded domains here. + * + * @return string The User's email */ - public function getEmail() { - if (strstr($this->email, '@ury.org.uk') !== false or strstr($this->email, '@ury.york.ac.uk') !== false) { + public function getEmail() + { + if (empty($this->email)) { + return $this->getEduroam().'@'.Config::$eduroam_domain; + } + + $domain = $domain = substr(strrchr($this->email, '@'), 1); + if (in_array($domain, Config::$local_email_domains)) { //The user has set an alias or their local mailbox here. //Return the local mailbox, or, failing that, eduroam $local = $this->getLocalName(); if (!empty($local)) { - return $local; + return $local.'@'.Config::$email_domain; } else { //ffs, some people don't have an eduroam either. $eduroam = $this->getEduroam(); if (empty($eduroam)) { - return null; + return; } else { - return $eduroam . '@'.Config::$eduroam_domain; + return $eduroam.'@'.Config::$eduroam_domain; } } - } elseif (empty($this->email)) { - return $this->getEduroam() . '@'.Config::$eduroam_domain; } else { return $this->email; } @@ -439,187 +581,347 @@ public function getEmail() { * official @ury.org.uk, but wants it fowarded, then you set the local_alias * to the @ury.org.uk prefix, and email to their personal address. */ - public function getPublicEmail() { - /** - * This works around a PHP bug: - * Fatal error: Can't use method return value in write context is thrown if the getter is used directly in empty() - */ + public function getPublicEmail() + { + /* This works around a PHP bug: + * Fatal error: Can't use method return value in write context + * is thrown if the getter is used directly in empty() */ $alias = $this->getLocalAlias(); - return empty($alias) ? $this->getEmail() : $alias . '@ury.org.uk'; + + return empty($alias) ? $this->getEmail() : $alias.'@'.Config::$email_domain; } /** * Returns the User's eduroam ID, i.e. their @york.ac.uk email address. - * @return String + * + * @return string */ - public function getEduroam() { - return str_replace('@' . Config::$eduroam_domain, '', $this->eduroam); + public function getEduroam() + { + if ($this->eduroam == null) { + return ""; + } + return str_replace('@'.Config::$eduroam_domain, '', $this->eduroam); } /** - * Returns the User's college id + * Returns the User's college id. + * * @return int The User's college id */ - public function getCollegeID() { + public function getCollegeID() + { return $this->collegeid; } /** - * Returns the User's college name + * Returns the User's college name. + * * @return string The User's college */ - public function getCollege() { + public function getCollege() + { return $this->college; } /** - * Returns the User's phone number + * Returns the User's phone number. + * * @return int The User's phone */ - public function getPhone() { + public function getPhone() + { return $this->phone; } /** - * Gets every year the member has paid + * Gets every year the member has paid. */ - public function getAllPayments() { + public function getAllPayments() + { + if (!$this->payment) { + $this->payment = self::$db->fetchAll( + 'SELECT year, paid + FROM member_year + WHERE memberid = $1 + ORDER BY year ASC;', + [$this->getID()] + ); + $this->updateCacheObject(); + } + return $this->payment; } /** - * Returns if the User is set to recive email - * @return bool if receive_email is set + * Returns if the User is set to recive email. + * + * @return bool if receive_email is set */ - public function getReceiveEmail() { + public function getReceiveEmail() + { return $this->receive_email; } /** - * Returns the User's local server account + * Returns the User's local server account. + * * @return string The User's local_name */ - public function getLocalName() { + public function getLocalName() + { return $this->local_name; } /** - * Returns the User's email alias + * Returns the User's email alias. + * * @return string The User's local_alias */ - public function getLocalAlias() { + public function getLocalAlias() + { return $this->local_alias; } /** - * Returns the User's uni account + * Returns the User's uni account. + * * @return string The User's uni email + * * @todo This is a duplication of getEduroam. */ - public function getUniAccount() { + public function getUniAccount() + { return $this->eduroam; } /** - * Returns if the User's account is locked + * Returns if the User's account is locked. + * * @return bool if the account is locked */ - public function getAccountLocked() { + public function getAccountLocked() + { return $this->account_locked; } /** - * Get all the User's past, present and future officerships + * Get all the User's past, present and future officerships. + * @param bool $includeMemberships if true, non-officer team memberships will be included + * @return MyRadio_UserOfficership[] */ - public function getOfficerships() { - return $this->officerships; + public function getOfficerships($includeMemberships = false) + { + if ($this->isProfileHidden()){ + return []; + } + + if (!empty($this->officerships) && !$includeMemberships) { + return $this->officerships; + } + // We don't want it to be cached with includeMemberships + $ids = self::$db->fetchColumn( + 'SELECT member_officerid + FROM member_officer + INNER JOIN officer + USING (officerid) + WHERE memberid = $1' + . (!$includeMemberships ? ' AND type!=\'m\'' : '') + .' ORDER BY from_date,till_date;', + [$this->getID()] + ); + $result = MyRadio_UserOfficership::resultSetToObjArray($ids); + + if (!$includeMemberships) { + $this->officerships = $result; + } + + return $result; + } + + /** + * Get the User's radio time, calculated from timeslot signins + */ + public function getRadioTime() + { + if (!$this->radioTime) { + try { + $this->radioTime = self::$db->fetchColumn( + 'SELECT sum(duration) + FROM schedule.show_season_timeslot + INNER JOIN schedule.show_season USING (show_season_id) + INNER JOIN schedule.show_credit USING (show_id) + INNER JOIN sis2.member_signin USING(show_season_timeslot_id) + WHERE show_credit.creditid = $1 + AND member_signin.memberid = $1 + AND show_credit.effective_from <= show_season_timeslot.start_time + AND (show_credit.effective_to > show_season_timeslot.start_time + OR show_credit.effective_to IS NULL) + AND show_credit.approvedid IS NOT NULL;', + [$this->getID()] + )[0]; + + $this->updateCacheObject(); + } catch (MyRadioException $e) { + $this->radioTime = null; + } + } + + return $this->radioTime; } /** - * Gets the User's MyRadio Profile page URL - * @return String + * Gets the User's MyRadio Profile page URL. + * + * @return string */ - public function getURL() { - return CoreUtils::makeURL('Profile', 'view', array('memberid' => $this->getID())); + public function getURL() + { + return URLUtils::makeURL('Profile', 'view', ['memberid' => $this->getID()]); } /** - * Gets the User's bio - * @return String + * Gets the User's bio. + * + * @return string */ - public function getBio() { + public function getBio() + { return $this->bio; } /** - * Get the User's auth provider - * - * @return String + * Get the User's auth provider. + * + * @return string */ - public function getAuthProvider() { + public function getAuthProvider() + { return $this->auth_provider; } /** - * Get whether the user needs to change their password - * - * @return boolean + * Get whether the user has signed the Presenter Contract. + * + * @return bool if the presenter has signed the contract + */ + public function hasSignedContract() + { + if (empty(Config::$contract_uri)) { + return true; + } else { + return $this->contract_signed; + } + } + + public function hasOptedOutDeletion() + { + return $this->data_removal == 'optout'; + } + + + /** + * Get whether the user needs to change their password. + * + * @return bool */ - public function getRequirePasswordChange() { + public function getRequirePasswordChange() + { return $this->require_password_change; } + public function isGDPRSigned() + { + return $this->gdpr_accepted; + } + + public function isProfileHidden() + { + return $this->hide_profile; + } + /** * Returns an array of Shows which the User owns or is an active * credit in. Guaranteed order by first broadcast date of the show. - * + * * @param int $show_type_id - * @return Array an array of Show objects attached to the given user - */ - public function getShows($show_type_id = 1) { - $this->shows = self::$db->fetch_column('SELECT show_id FROM schedule.show - WHERE memberid=$1 OR show_id IN - (SELECT show_id FROM schedule.show_credit - WHERE creditid=$1 AND effective_from <= NOW() AND - (effective_to >= NOW() OR effective_to IS NULL)) - ORDER BY (SELECT start_time FROM schedule.show_season_timeslot - WHERE show_season_id IN - (SELECT show_season_id FROM schedule.show_season WHERE show_id=schedule.show.show_id) - ORDER BY start_time LIMIT 1) - ASC', array($this->getID())); //Wasn't that ORDER BY fun. - - $return = array(); - foreach ($this->shows as $show_id) { + * @param bool $current_term_only if true, will only include shows with seasons this term + * + * @return array an array of Show objects attached to the given user + */ + public function getShows($show_type_id = 1, $current_term_only = false) + { + + if ($this->isProfileHidden()){ + return []; + } + + $sql = 'SELECT show_id FROM schedule.show + WHERE memberid=$1 OR show_id IN + (SELECT show_id FROM schedule.show_credit + WHERE creditid=$1 AND + (effective_to >= NOW() OR effective_to IS NULL)) + '; + $params = [$this->getID()]; + + if ($current_term_only) { + $sql .= ' AND EXISTS ( + SELECT * FROM schedule.show_season + WHERE schedule.show_season.show_id=schedule.show.show_id + AND schedule.show_season.termid=$2 + )'; + $params[] = MyRadio_Term::getActiveApplicationTerm()->getID(); + } + + $sql .= ' ORDER BY (SELECT start_time FROM schedule.show_season_timeslot + WHERE show_season_id IN + (SELECT show_season_id FROM schedule.show_season WHERE show_id=schedule.show.show_id) + ORDER BY start_time LIMIT 1) + ASC';//Wasn't that ORDER BY fun. + + $result = self::$db->fetchColumn($sql, $params); + + // Don't screw up the show cache with term-limited shows + if (!$current_term_only) { + $this->shows = $result; + } + + $return = []; + foreach ($result as $show_id) { $show = MyRadio_Show::getInstance($show_id); if ($show->getShowType() == $show_type_id) { $return[] = $show; } } + return $return; } /** - * Returns if the user has the given permission. - * - * Always use CoreUtils::hasAuth when working with the current user. - * - * @param int $authid The permission to test for - * @return boolean Whether this user has the requested permission + * Finds all the email addresses and lists that go to this user. + * + * @return MyRadio_EmailDestination[] */ - public function hasAuth($authid) { - return in_array($authid, $this->permissions); + public function getAllEmails() + { + return MyRadio_EmailDestination::getAllSourcesForUser(self::$db, $this->getID()); } /** - * Searches for Users with a name starting with $name - * @param String $name The name to search for. If there is a space, it is assumed the second word is the surname - * @param int $limit The maximum number of Users to return. -1 uses the ajax_limit_default setting. - * @return Array A 2D Array where every value of the first dimension is an Array as follows:
    - * memberid: The unique id of the User
    - * fname: The actual first name of the User
    - * sname: The actual last name of the User - */ - public static function findByName($name, $limit = -1) { + * Searches for Users with a name starting with $name. + * + * @param string $name The name to search for. If there is a space, it is assumed the second word is the surname + * @param int $limit The maximum number of Users to return. -1 uses the ajax_limit_default setting. + * + * @return array A 2D Array where every value of the first dimension is an Array as follows:
    + * memberid: The unique id of the User
    + * fname: The actual first name of the User
    + * sname: The actual last name of the User
    + * eduroam: The actual eduroam account of the User
    + * local_alias: The actual local alias (THISPART@emailaddr) for the user + */ + public static function findByName($name, $limit = -1) + { if ($limit == -1) { $limit = Config::$ajax_limit_default; } @@ -627,17 +929,24 @@ public static function findByName($name, $limit = -1) { $name = trim($name); $names = explode(' ', $name); if (isset($names[1])) { - return self::$db->fetch_all('SELECT memberid, fname, sname FROM member - WHERE fname ILIKE $1 || \'%\' AND sname ILIKE $2 || \'%\' - ORDER BY sname, fname LIMIT $3', array($names[0], $names[1], $limit)); + return self::$db->fetchAll( + 'SELECT memberid, fname, sname, eduroam, local_alias FROM member + WHERE fname ILIKE $1 || \'%\' AND sname ILIKE $2 || \'%\' + ORDER BY sname, fname LIMIT $3', + [$names[0], $names[1], $limit] + ); } else { - return self::$db->fetch_all('SELECT memberid, fname, sname FROM member - WHERE fname ILIKE $1 || \'%\' OR sname ILIKE $1 || \'%\' - ORDER BY sname, fname LIMIT $2', array($name, $limit)); + return self::$db->fetchAll( + 'SELECT memberid, fname, sname, eduroam, local_alias FROM member + WHERE fname ILIKE $1 || \'%\' OR sname ILIKE $1 || \'%\' + ORDER BY sname, fname LIMIT $2', + [$name, $limit] + ); } } - public static function getInstance($itemid = -1) { + public static function getInstance($itemid = -1) + { if ($itemid === -1) { if (isset($_SESSION['memberid'])) { $itemid = $_SESSION['memberid']; @@ -645,15 +954,38 @@ public static function getInstance($itemid = -1) { throw new MyRadioException('Trying to get current user info with no current user'); } } - return parent::getInstance($itemid); + + if (isset($_SESSION['memberid']) && $itemid == $_SESSION['memberid']) { + if (!self::$current_user) { + self::$current_user = parent::getInstance($itemid); + } + + return self::$current_user; + } else { + return parent::getInstance($itemid); + } } - + + /** + * Returns the current logged in user, or null if there is none. + * @return MyRadio_User|null + */ + public static function getCurrentUser() + { + if (isset($_SESSION['memberid'])) { + return self::getInstance(); + } else { + return null; + } + } + /** * Returns the current logged in user, or failing that, the System User. - * + * * @return MyRadio_User */ - public static function getCurrentOrSystemUser() { + public static function getCurrentOrSystemUser() + { if (isset($_SESSION['memberid'])) { return self::getInstance(); } else { @@ -662,27 +994,31 @@ public static function getCurrentOrSystemUser() { } /** - * Runs a super-long pSQL query that returns the information used to generate the Profile Timeline - * @return Array A 2D Array where every value of the first dimension is an Array as follows:
    - * timestamp: When the event occurred, formatted as d/m/Y
    - * message: A text description of the event
    - * photo: The photoid of a thumbnail to render with the event + * Runs a super-long pSQL query that returns the information used to generate the Profile Timeline. + * + * @return array A 2D Array where every value of the first dimension is an Array as follows:
    + * timestamp: When the event occurred, as a Unix timestamp
    + * message: A text description of the event
    + * photo: The relative web path of a thumbnail to render with the event, or null */ - public function getTimeline() { - $events = array(); + public function getTimeline() + { + $events = []; //Get Officership history foreach ($this->getOfficerships() as $officer) { $events[] = [ - 'message' => 'became ' . $officer['officer_name'], - 'timestamp' => strtotime($officer['from_date']), - 'photo' => Config::$photo_officership_get + 'message' => 'became '.$officer->getOfficer()->getName(), + 'timestamp' => strtotime($officer->getFromDate()), + //'photo' => MyRadio_Photo::getInstance(Config::$photo_officership_get)->getRelativeWebPath(), + 'photo' => null ]; - if ($officer['till_date'] != null) { + if ($officer->getTillDate() != null) { $events[] = [ - 'message' => 'stepped down as ' . $officer['officer_name'], - 'timestamp' => strtotime($officer['till_date']), - 'photo' => Config::$photo_officership_down + 'message' => 'stepped down as '.$officer->getOfficer()->getName(), + 'timestamp' => strtotime($officer->getTillDate()), + //'photo' => MyRadio_Photo::getInstance(Config::$photo_officership_down)->getRelativeWebPath(), + 'photo' => null ]; } } @@ -696,26 +1032,29 @@ public function getTimeline() { } } foreach ($show->getAllSeasons() as $season) { - if (sizeof($season->getAllTimeslots()) === 0) + if (sizeof($season->getAllTimeslots()) === 0) { continue; + } if ($season->getSeasonNumber() == 1) { $events[] = [ - 'message' => 'started a new Show as ' . $credit . ' of ' . $season->getMeta('title'), - 'timestamp' => strtotime($season->getAllTimeslots()[0]->getStartTime()), - 'photo' => $show->getShowPhoto() + 'message' => 'started a new Show as '.$credit.' of '.$season->getMeta('title'), + 'timestamp' => $season->getAllTimeslots()[0]->getStartTime(), + 'photo' => $show->getShowPhoto(), ]; } else { $events[] = [ - 'message' => 'was ' . $credit . ' on Season ' . $season->getSeasonNumber() . ' of ' . $season->getMeta('title'), - 'timestamp' => strtotime($season->getAllTimeslots()[0]->getStartTime()), - 'photo' => $show->getShowPhoto() + 'message' => 'was ' . $credit + . ' on Season ' . $season->getSeasonNumber() + . ' of '.$season->getMeta('title'), + 'timestamp' => $season->getAllTimeslots()[0]->getStartTime(), + 'photo' => $show->getShowPhoto(), ]; } } } //Get their officership history, show history and awards - /* $result = self::$db->fetch_all( + /* $result = self::$db->fetchAll( SELECT \'won an award: \' || name AS message, awarded AS timestamp, \'photo_award_get\' AS photo FROM myury.award_categories, myury.award_member @@ -725,26 +1064,29 @@ public function getTimeline() { } */ //Get when they joined URY - $events[] = array( - 'timestamp' => strtotime($this->joined), - 'message' => 'joined ' . Config::$short_name, - 'photo' => Config::$photo_joined - ); + $events[] = [ + 'timestamp' => $this->joined, + 'message' => 'joined '.Config::$short_name, + //'photo' => MyRadio_Photo::getInstance(Config::$photo_joined)->getRelativeWebPath(), + 'photo' => null + ]; return $events; } /** - * - * @param String $paramName The key to update, e.g. account_locked. - * Don't be silly and try to set memberid. Bad things will happen. - * @param mixed $value The value to set the param to. Type depends on $paramName. + * @param string $paramName The key to update, e.g. account_locked. + * Don't be silly and try to set memberid. Bad things will happen. + * @param mixed $value The value to set the param to. Type depends on $paramName. */ - private function setCommonParam($paramName, $value) { + private function setCommonParam($paramName, $value) + { /** * You won't believe how annoying psql can be about '' already being used on a unique key. + * You also won't believe that in php, '' == false evaluates to true, so we need ===, + * otherwise a query to change $value to false will not work as desired. */ - if ($value == '') { + if ($value === '') { $value = null; } //Maps Class variable names to their database values, if they mismatch. @@ -764,7 +1106,7 @@ private function setCommonParam($paramName, $value) { $paramName = $param_maps[$paramName]; } - self::$db->query('UPDATE member SET ' . $paramName . '=$1 WHERE memberid=$2', array($value, $this->getID())); + self::$db->query('UPDATE member SET '.$paramName.'=$1 WHERE memberid=$2', [$value, $this->getID()]); $this->updateCacheObject(); return true; @@ -772,78 +1114,144 @@ private function setCommonParam($paramName, $value) { /** * Sets the User's account locked status. - * + * * If a User's account is locked, access to all URY services is blocked by * MyRadio and IMAP. - * + * * @param bool $bool True for Locked, False for Unlocked. Default True. + * * @return MyRadio_User */ - public function setAccountLocked($bool = true) { + public function setAccountLocked($bool = true) + { $this->setCommonParam('account_locked', $bool); + return $this; } - + + /** + * Sets the User's account hidden status. + * + * If a User's account is hidden it will reject myradio api calls towards that user. + * + * @param bool $bool True for hidden. False for unhidden. + * + * @return MyRadio_User + */ + public function setHideProfile($bool = false) + { + $this->setCommonParam('hide_profile', $bool); + + return $this; + } + /** * Sets the user's require password change status. * If a user has requested a new password, this should be set to true. * Should be set to false when the user actually changes their password. * * @param bool $bool True for change required, False otherwise. Default T. + * * @return MyRadio_User */ - public function setRequirePasswordChange($bool = true) { + public function setRequirePasswordChange($bool = true) + { $this->setCommonParam('require_password_change', $bool); + + return $this; + } + + /** + * sets if the user has signed the privacy statement. + * + * @param bool $bool True for GDPR signed + * + * @return MyRadio_User + */ + public function setSignedGDPR($bool = true) + { + $this->setCommonParam('gdpr_accepted', $bool); + + return $this; + } + + /** + * sets if the user has signed the privacy statement. + * + * @param enum + * + * @return MyRadio_User + */ + public function setDataRemoval($removal) + { + $this->setCommonParam('data_removal', $removal); + return $this; } /** * Set's a User's college ID. - * + * * College IDs can be acquired using User::getColleges(). - * + * * @param int $college_id The ID of the college. + * * @return MyRadio_User */ - public function setCollegeID($college_id) { + public function setCollegeID($college_id) + { $this->setCommonParam('collegeid', $college_id); + return $this; } /** - * Set the user's eduroam address - * + * Set the user's eduroam address. + * * @param type $eduroam The User's UoY address, i.e. abc123@york.ac.uk (@york.ac.uk optional) + * * @return MyRadio_User */ - public function setEduroam($eduroam) { + public function setEduroam($eduroam) + { //Require the user to be part of this eduroam domain - if (strstr($eduroam, '@') !== false && - strstr($eduroam, '@'.Config::$eduroam_domain) === false) { - throw new MyRadioException('Eduroam account should be @'.Config::$eduroam_domain.'! Use of other eduroam accounts is blocked. - This is a basic validation filter, so if there is a valid reason for another account to be here, this check - can be removed.', 400); + if (strstr($eduroam, '@') !== false + && strstr($eduroam, '@'.Config::$eduroam_domain) === false + ) { + throw new MyRadioException( + 'Eduroam account should be @' + .Config::$eduroam_domain + .'! Use of other eduroam accounts is blocked. ' + .'This is a basic validation filter, so if there is a valid reason for another account to be here, ' + .'this check can be removed.', + 400 + ); } - + //Remove the domain if it is set $eduroam = str_replace('@'.Config::$eduroam_domain, '', $eduroam); if (empty($eduroam) && empty($this->email)) { throw new MyRadioException('Can\'t set both Email and Eduroam to null.', 400); - } elseif ($this->getEduroam() !== $eduroam && MyRadio_User::findByEmail($eduroam) !== null) { - throw new MyRadioException('The eduroam account ' . $eduroam . ' is already allocated to another User.', 500); + } elseif ($this->getEduroam() !== $eduroam && self::findByEmail($eduroam) !== null) { + throw new MyRadioException('The eduroam account '.$eduroam.' is already allocated to another User.', 400); } $this->setCommonParam('eduroam', $eduroam); + return $this; } /** * Sets the User's primary contact Email. If null, eduroam is used. - * @param String $email + * + * @param string $email + * * @return MyRadio_User + * * @throws MyRadioException */ - public function setEmail($email) { + public function setEmail($email) + { if ($email === '') { $email = null; } @@ -853,198 +1261,265 @@ public function setEmail($email) { if (empty($email) && empty($this->eduroam)) { throw new MyRadioException('Can\'t set both Email and Eduroam to null.', 400); - } elseif ($email !== $this->email && MyRadio_User::findByEmail($email) !== null) { - throw new MyRadioException('The email account ' . $email . ' is already allocated to another User.', 500); + } elseif ($email !== $this->email && self::findByEmail($email) !== null && self::findByEmail($email) != $this) { + throw new MyRadioException('The email account '.$email.' is already allocated to another User.', 400); } $this->setCommonParam('email', $email); + return $this; } /** - * Sets the User's first name - * @param String $fname + * Sets the User's first name. + * + * @param string $fname + * * @return MyRadio_User + * * @throws MyRadioException */ - public function setFName($fname) { + public function setFName($fname) + { if (empty($fname)) { throw new MyRadioException('Oh come on, everybody has a name.', 400); } $this->setCommonParam('fname', $fname); + return $this; } /** - * Set the User's official @ury.org.uk prefix. Usually fname.sname - * @param String $alias + * Set the User's official @ury.org.uk prefix. Usually fname.sname. + * + * @param string $alias + * * @return MyRadio_User + * * @throws MyRadioException */ - public function setLocalAlias($alias) { + public function setLocalAlias($alias) + { if ($alias !== $this->local_alias && self::findByEmail($alias) !== null) { - throw new MyRadioException('That Mailbox Name is already in use. Please choose another.', 500); + throw new MyRadioException('That Mailbox Name is already in use. Please choose another.', 400); } $this->setCommonParam('local_alias', $alias); + return $this; } /** - * Set the User's server account name - * @param String $name + * Set the User's server account name. + * + * @param string $name + * * @return MyRadio_User + * * @throws MyRadioException */ - public function setLocalName($name) { - if ($name !== $this->local_name && self::findByEmail($name) !== null) { - throw new MyRadioException('That Mailbox Alias is already in use. Please choose another.', 500); + public function setLocalName($name) + { + if (strstr($name, '@') !== false) { + throw new MyRadioException('Mailbox alias may not contain an @ symbol'); + } + if ($name !== $this->local_name && self::findByEmail($name) !== null && self::findByEmail($name) != $this) { + throw new MyRadioException('That Mailbox Alias is already in use. Please choose another.', 400); } $this->setCommonParam('local_name', $name); + return $this; } /** - * Set the User's phone number - * @param String $phone A string of numbers (because leading 0) + * Set the User's phone number. + * + * @param string $phone A string of numbers (because leading 0) + * * @return MyRadio_User + * * @throws MyRadioException */ - public function setPhone($phone) { + public function setPhone($phone) + { //Clear whitespace $phone = preg_replace('/\s/', '', $phone); if (!empty($phone) && strlen($phone) !== 11) { throw new MyRadioException('A phone number should have 11 digits.', 400); } $this->setCommonParam('phone', $phone); + return $this; } /** - * Set the User's profile photo + * Set the User's profile photo. + * * @param MyRadio_Photo $photo + * * @return MyRadio_User */ - public function setProfilePhoto(MyRadio_Photo $photo) { + public function setProfilePhoto(MyRadio_Photo $photo) + { $this->setCommonParam('profile_photo', $photo->getID()); + return $this; } /** - * Set whether the User should receive Emails - * @param boolean $bool + * Set whether the User should receive Emails. + * + * @param bool $bool + * * @return MyRadio_User */ - public function setReceiveEmail($bool = true) { + public function setReceiveEmail($bool = true) + { $this->setCommonParam('receive_email', $bool); + return $this; } /** - * Set the User's preferred Auth provider - * @param String $provider + * Set the User's preferred Auth provider. + * + * @param string $provider + * * @return MyRadio_User */ - public function setAuthProvider($provider = null) { + public function setAuthProvider($provider = null) + { $this->setCommonParam('auth_provider', $provider); + return $this; } /** * Set the User's last name. - * @param String $sname + * + * @param string $sname + * * @return MyRadio_User + * * @throws MyRadioException */ - public function setSName($sname) { + public function setSName($sname) + { if (empty($sname)) { throw new MyRadioException('Yes, your last name is a thing.', 400); } $this->setCommonParam('sname', $sname); - return $this; - } - - /** - * Set the User's Gender - * @param char $initial (m)ale, (f)emale or (o)ther - * @return MyRadio_User - * @throws MyRadioException - */ - public function setSex($initial = 'o') { - $initial = strtolower($initial); - if (!in_array($initial, array('m', 'f', 'o'))) { - throw new MyRadioException('You can be either "(M)ale", "(F)emale", or "(O)ther". You can\'t be none of these,' - . ' or more than one of these. Sorry.'); - } - $this->setCommonParam('sex', $initial); return $this; } /** * Set the User's HTML biography. - * @param String $bio + * + * @param string $bio + * * @return MyRadio_User */ - public function setBio($bio) { + public function setBio($bio) + { $this->setCommonParam('bio', $bio); + return $this; } - public function setPayment($amount, $year = null) { + public function setPayment($amount, $year = null) + { if ($year === null) { $year = CoreUtils::getAcademicYear(); } $amount = number_format($amount, 2); - foreach ($this->payment as $k => $v) { + foreach ($this->getAllPayments() as $k => $v) { if ($v['year'] == $year && $v['paid'] == $amount) { return; } elseif ($v['year'] == $year) { //Change payment. - self::$db->query('UPDATE member_year SET paid=$1' - . ' WHERE year=$2 AND memberid=$3', [(float) $amount, $year, $this->getID()]); + self::$db->query( + 'UPDATE member_year SET paid=$1 + WHERE year=$2 AND memberid=$3', + [(float) $amount, $year, $this->getID()] + ); $this->payment[$k]['paid'] = $amount; + $this->clearPermissionCache()->updateCacheObject(); $this->updateCacheObject(); return; } } //Not a member this year - self::$db->query('INSERT INTO member_year (paid, year, memberid)' - . ' VALUES ($1, $2, $3)', [(float) $amount, $year, $this->getID()]); - $this->payment[] = ['year' => $year, 'amount' => (float) $amount]; + self::$db->query( + 'INSERT INTO member_year (paid, year, memberid) + VALUES ($1, $2, $3)', + [(float) $amount, $year, $this->getID()] + ); + $this->payment[] = ['year' => $year, 'paid' => $amount]; + $this->clearPermissionCache()->updateCacheObject(); $this->updateCacheObject(); return; } + /** + * Sets the user's contract signed-ness status. + * + * @param bool $bool True for contract being signed, False otherwise. Default F. + * + * @return MyRadio_User + */ + public function setContractSigned($bool = false) + { + if (empty(Config::$contract_uri) === false) { + $this->setCommonParam('contract_signed', $bool); + } + + return $this; + } + /** * Sets the User's last login time to right now. - * Use this when they're being logged in (weird that) + * Use this when they're being logged in (weird that). */ - public function updateLastLogin() { + public function updateLastLogin() + { $this->last_login = CoreUtils::getTimestamp(); - self::$db->query('UPDATE public.member SET last_login=$1' - . ' WHERE memberid=$2', [$this->last_login, $this->getID()]); + self::$db->query( + 'UPDATE public.member SET last_login=$1 + WHERE memberid=$2', + [$this->last_login, $this->getID()] + ); $this->updateCacheObject(); } /** * Searched for the user with the given email address, returning the User if they exist, or null if it fails. - * @param String $email + * + * @param string $email + * * @return null|MyRadio_User */ - public static function findByEmail($email) { + public static function findByEmail($email) + { if (empty($email)) { - return null; + return; } //Doing this instead of ILIKE halves the query time $email = strtolower($email); self::wakeup(); - $result = self::$db->fetch_column('SELECT memberid FROM public.member WHERE email LIKE $1 OR eduroam LIKE $1 - OR local_name LIKE $2 OR local_alias LIKE $2 OR eduroam LIKE $2', array($email, explode('@', $email)[0])); + $result = self::$db->fetchColumn( + 'SELECT memberid FROM public.member WHERE email LIKE $1 OR eduroam LIKE $1 + OR local_name LIKE $3 OR local_alias LIKE $3 OR eduroam LIKE $2', + [ + $email, + str_replace('@'.Config::$eduroam_domain, '', $email), + str_replace('@'.Config::$email_domain, '', $email), + ] + ); if (empty($result)) { - return null; + return; } else { return self::getInstance($result[0]); } @@ -1052,18 +1527,22 @@ public static function findByEmail($email) { /** * Please use MyRadio_TrainingStatus. - * + * * @deprecated + * * @return MyRadio_User[] */ - public static function findAllTrained() { + public static function findAllTrained() + { self::wakeup(); trigger_error('Use of deprecated method User::findAllTrained.', E_USER_WARNING); - $trained = self::$db->fetch_column('SELECT memberid FROM public.member_presenterstatus WHERE presenterstatusid=1'); - $members = array(); + $trained = self::$db->fetchColumn( + 'SELECT memberid FROM public.member_presenterstatus WHERE presenterstatusid=1' + ); + $members = []; foreach ($trained as $mid) { - $member = MyRadio_User::getInstance($mid); + $member = self::getInstance($mid); if ($member->isStudioTrained()) { $members[] = $member; } @@ -1074,20 +1553,25 @@ public static function findAllTrained() { /** * Please use MyRadio_TrainingStatus. - * + * * @deprecated + * * @return MyRadio_User[] */ - public static function findAllDemoed() { + public static function findAllDemoed() + { self::wakeup(); trigger_error('Use of deprecated method User::findAllDemoed.', E_USER_WARNING); - $trained = self::$db->fetch_column('SELECT memberid FROM public.member_presenterstatus WHERE presenterstatusid=2'); - $members = array(); + $trained = self::$db->fetchColumn( + 'SELECT memberid FROM public.member_presenterstatus WHERE presenterstatusid=2' + ); + $members = []; foreach ($trained as $mid) { - $member = MyRadio_User::getInstance($mid); - if ($member->isStudioDemoed()) + $member = self::getInstance($mid); + if ($member->isStudioDemoed()) { $members[] = $member; + } } return $members; @@ -1095,18 +1579,22 @@ public static function findAllDemoed() { /** * Please use MyRadio_TrainingStatus. - * + * * @deprecated + * * @return MyRadio_User[] */ - public static function findAllTrainers() { + public static function findAllTrainers() + { self::wakeup(); trigger_error('Use of deprecated method User::findAllTrainers.', E_USER_WARNING); - $trained = self::$db->fetch_column('SELECT memberid FROM public.member_presenterstatus WHERE presenterstatusid=3'); - $members = array(); + $trained = self::$db->fetchColumn( + 'SELECT memberid FROM public.member_presenterstatus WHERE presenterstatusid=3' + ); + $members = []; foreach ($trained as $mid) { - $member = MyRadio_User::getInstance($mid); + $member = self::getInstance($mid); if ($member->isTrainer()) { $members[] = $member; } @@ -1117,11 +1605,16 @@ public static function findAllTrainers() { /** * Returns an Array of all mappings for official aliases to emails go to. - * @return Array[] [[from, to]] + * + * @return array[] [[from, to]] */ - public static function getAllAliases() { - $users = self::resultSetToObjArray(self::$db->fetch_column( - 'SELECT memberid FROM public.member WHERE local_alias IS NOT NULL')); + public static function getAllAliases() + { + $users = self::resultSetToObjArray( + self::$db->fetchColumn( + 'SELECT memberid FROM public.member WHERE local_alias IS NOT NULL' + ) + ); $data = []; foreach ($users as $user) { @@ -1129,7 +1622,12 @@ public static function getAllAliases() { if (empty($email)) { continue; } else { - $data[] = [$user->getLocalAlias(), $email]; + $local = $user->getLocalAlias(); + $data[] = [$local, $email]; + $eduroam = $user->getEduroam(); + if (!empty($eduroam) && ($eduroam !== $email)) { + $data[] = [$eduroam, $email]; + } } } @@ -1137,377 +1635,801 @@ public static function getAllAliases() { } /** - * Gets the edit form for this User, with the permissions available for the current User + * Gets the edit form for this User, with the permissions available for the current User. */ - public function getEditForm() { - if ($this->getID() !== MyRadio_User::getInstance()->getID() && !MyRadio_User::getInstance()->hasAuth(AUTH_EDITANYPROFILE)) { - throw new MyRadioException(MyRadio_User::getInstance() . ' tried to edit ' . $this . '!'); + public function getEditForm() + { + if ($this->getID() !== self::getInstance()->getID() && !self::getInstance()->hasAuth(AUTH_EDITANYPROFILE)) { + throw new MyRadioException(self::getInstance().' tried to edit '.$this.'!'); } - $form = new MyRadioForm('profileedit', 'Profile', 'doEdit', array('title' => 'Edit Profile')); + $form = new MyRadioForm('profileedit', 'Profile', 'edit', ['title' => 'Edit Profile']); //Personal details $form->addField(new MyRadioFormField('memberid', MyRadioFormField::TYPE_HIDDEN, ['value' => $this->getID()])) - ->addField(new MyRadioFormField('sec_personal', MyRadioFormField::TYPE_SECTION, array( - 'label' => 'Personal Details' - ))) - ->addField(new MyRadioFormField('fname', MyRadioFormField::TYPE_TEXT, array( + ->addField( + new MyRadioFormField( + 'sec_personal', + MyRadioFormField::TYPE_SECTION, + [ + 'label' => 'Personal Details', + ] + ) + ) + ->addField( + new MyRadioFormField( + 'fname', + MyRadioFormField::TYPE_TEXT, + [ 'required' => true, 'label' => 'First Name', - 'value' => $this->getFName() - ))) - ->addField(new MyRadioFormField('sname', MyRadioFormField::TYPE_TEXT, array( + 'value' => $this->getFName(), + ] + ) + ) + ->addField( + new MyRadioFormField( + 'sname', + MyRadioFormField::TYPE_TEXT, + [ 'required' => true, 'label' => 'Last Name', - 'value' => $this->getSName() - ))) - ->addField(new MyRadioFormField('sex', MyRadioFormField::TYPE_SELECT, array( - 'required' => true, - 'label' => 'Gender', - 'value' => $this->getSex(), - 'options' => array( - array('value' => 'm', 'text' => 'Male'), - array('value' => 'f', 'text' => 'Female'), - array('value' => 'o', 'text' => 'Other') - ) - ))) - ->addField(new MyRadioFormField('sec_personal_close', MyRadioFormField::TYPE_SECTION_CLOSE) - ); + 'value' => $this->getSName(), + ] + ) + ); + if (empty(Config::$contract_uri) === false) { + $form->addField( + new MyRadioFormField( + 'contract', + MyRadioFormField::TYPE_CHECK, + [ + 'required' => false, + 'label' => 'I, '.$this->getName().', agree to abide by ' + .Config::$short_name.'\'s station rules and regulations as ' + .'set out in the Presenter\'s Contract, ' + .'and the Ofcom Programming Code. ' + .'I have fully read and understood these rules and regulations, ' + .'and I understand that if I break any of the rules or ' + .'regulations stated by Ofcom or its successor, I will be ' + .'solely liable for any resulting fines or actions that may ' + .'be levied against '.Config::$long_name.'.', + 'options' => ['checked' => $this->hasSignedContract()], + ] + ) + ); + } + $form->addField( + new MyRadioFormField( + 'data_removal', + MyRadioFormField::TYPE_CHECK , + [ + 'required' => false, + 'label' => 'I Do not wish for my data to be deleted at any point. (this can be changed later)', + 'options' => ['checked' => $this->hasOptedOutDeletion()], + ] + ) + ); + $form->addField( + new MyRadioFormField( + 'hide', + MyRadioFormField::TYPE_CHECK , + [ + 'required' => false, + 'label' => 'Hide profile on public website (Strongly discouraged for current members)', + 'options' => ['checked' => $this->isProfileHidden()], + ] + ) + ); + $form->addField(new MyRadioFormField('sec_personal_close', MyRadioFormField::TYPE_SECTION_CLOSE)); //Contact details - $form->addField(new MyRadioFormField('sec_contact', MyRadioFormField::TYPE_SECTION, array( - 'label' => 'Contact Details' - ))) - ->addField(new MyRadioFormField('collegeid', MyRadioFormField::TYPE_SELECT, array( + $form->addField( + new MyRadioFormField( + 'sec_contact', + MyRadioFormField::TYPE_SECTION, + [ + 'label' => 'Contact Details', + ] + ) + ) + ->addField( + new MyRadioFormField( + 'collegeid', + MyRadioFormField::TYPE_SELECT, + [ 'required' => true, 'label' => 'College', 'options' => self::getColleges(), - 'value' => $this->getCollegeID() - ))) - ->addField(new MyRadioFormField('phone', MyRadioFormField::TYPE_TEXT, array( + 'value' => $this->getCollegeID(), + ] + ) + ) + ->addField( + new MyRadioFormField( + 'phone', + MyRadioFormField::TYPE_TEXT, + [ 'required' => false, 'label' => 'Phone Number', - 'value' => $this->getPhone() - ))) - ->addField(new MyRadioFormField('email', MyRadioFormField::TYPE_EMAIL, array( + 'value' => $this->getPhone(), + ] + ) + ) + ->addField( + new MyRadioFormField( + 'email', + MyRadioFormField::TYPE_EMAIL, + [ 'required' => false, 'label' => 'Email', - 'value' => $this->email - ))) - ->addField(new MyRadioFormField('receive_email', MyRadioFormField::TYPE_CHECK, array( + 'value' => $this->email, + ] + ) + ) + ->addField( + new MyRadioFormField( + 'receive_email', + MyRadioFormField::TYPE_CHECK, + [ 'required' => false, 'label' => 'Receive Email?', - 'options' => array('checked' => $this->getReceiveEmail()), - 'explanation' => 'If unchecked, you will receive no emails, even if you are subscribed to mailing lists.' - ))) - ->addField(new MyRadioFormField('eduroam', MyRadioFormField::TYPE_TEXT, array( + 'options' => ['checked' => $this->getReceiveEmail()], + 'explanation' => 'If unchecked, you will receive no emails, ' + .'even if you are subscribed to mailing lists.', + ] + ) + ); + $uni_email = NULL; + if ($this->getUniAccount() !== NULL) { + $uni_email = str_replace('@'.Config::$eduroam_domain, '', $this->getUniAccount()); + } + $form->addField( + new MyRadioFormField( + 'eduroam', + MyRadioFormField::TYPE_TEXT, + [ 'required' => false, 'label' => 'University Email', - 'value' => str_replace('@york.ac.uk', '', $this->getUniAccount()), - 'explanation' => '@york.ac.uk' - ))) - ->addField(new MyRadioFormField('sec_contact_close', MyRadioFormField::TYPE_SECTION_CLOSE) - ); + 'value' => $uni_email, + 'explanation' => '@'.Config::$eduroam_domain, + ] + ) + ) + ->addField(new MyRadioFormField('sec_contact_close', MyRadioFormField::TYPE_SECTION_CLOSE)); //About Me - $form->addField(new MyRadioFormField('sec_about', MyRadioFormField::TYPE_SECTION, array( - 'label' => 'About Me', - 'explanation' => 'If you\'d like to share a little more about yourself, then I\'m happy to listen!' - )))->addField( - new MyRadioFormField('photo', MyRadioFormField::TYPE_FILE, array( - 'required' => false, - 'label' => 'Profile Photo', - 'explanation' => 'Share your Radio Face with all our members. If we ever launch presenter pages on the website, we\'ll use this there too.' - )) + $form->addField( + new MyRadioFormField( + 'sec_about', + MyRadioFormField::TYPE_SECTION, + [ + 'label' => 'About Me', + 'explanation' => 'If you\'d like to share a little more about yourself, then I\'m happy to listen!', + ] + ) + )->addField( + new MyRadioFormField( + 'photo', + MyRadioFormField::TYPE_FILE, + [ + 'required' => false, + 'label' => 'Profile Photo', + 'explanation' => 'Share your Radio Face with all our members. ' + .'Also displayed on the presenter pages on the website', + ] + ) )->addField( - new MyRadioFormField('bio', MyRadioFormField::TYPE_BLOCKTEXT, array( - 'required' => false, - 'label' => 'Bio', - 'explanation' => 'Tell use about yourself - if you\'re a committee member please introduce yourself!', - 'value' => $this->getBio() - )) + new MyRadioFormField( + 'bio', + MyRadioFormField::TYPE_BLOCKTEXT, + [ + 'required' => false, + 'label' => 'Bio', + 'explanation' => "Tell us about yourself - if you're a committee member please introduce yourself!", + 'value' => $this->getBio(), + ] + ) )->addField(new MyRadioFormField('sec_about_close', MyRadioFormField::TYPE_SECTION_CLOSE)); //Mailbox - if (MyRadio_User::getInstance()->hasAuth(AUTH_CHANGESERVERACCOUNT)) { - $form->addField(new MyRadioFormField('sec_server', MyRadioFormField::TYPE_SECTION, array( - 'label' => Config::$short_name . ' Mailbox Account', - 'explanation' => 'Before changing these settings, please ensure you understand the guidelines and' - . ' documentation on ' . Config::$long_name . '\'s Internal Email Service' - ))) - ->addField(new MyRadioFormField('local_name', MyRadioFormField::TYPE_TEXT, array( + if (self::getInstance()->hasAuth(AUTH_CHANGESERVERACCOUNT)) { + $form->addField( + new MyRadioFormField( + 'sec_server', + MyRadioFormField::TYPE_SECTION, + [ + 'label' => Config::$short_name.' Mailbox Account', + 'explanation' => 'Before changing these settings, please ensure you understand the guidelines ' + . 'and documentation on '.Config::$long_name.'\'s Internal Email Service', + ] + ) + ) + ->addField( + new MyRadioFormField( + 'local_name', + MyRadioFormField::TYPE_TEXT, + [ 'required' => false, 'label' => 'Server Account (Mailbox)', 'value' => $this->getLocalName(), - 'explanation' => 'Best practice is their ITS Username' - ))) - ->addField(new MyRadioFormField('local_alias', MyRadioFormField::TYPE_TEXT, array( + 'explanation' => 'Best practice is their ITS Username', + ] + ) + ) + ->addField( + new MyRadioFormField( + 'local_alias', + MyRadioFormField::TYPE_TEXT, + [ 'required' => false, - 'label' => '@ury.org.uk Alias', + 'label' => '@'.Config::$email_domain.' Alias', 'value' => $this->getLocalAlias(), - 'explanation' => 'Usually, this is firstname.lastname (i.e. ' . - strtolower($this->getFName() . '.' . $this->getSName()) . ')' - ))) - ->addField(new MyRadioFormField('sec_server_close', MyRadioFormField::TYPE_SECTION_CLOSE)); + 'explanation' => 'Usually, this is firstname.lastname (i.e. '. + strtolower($this->getFName().'.'.$this->getSName()).')', + ] + ) + ) + ->addField(new MyRadioFormField('sec_server_close', MyRadioFormField::TYPE_SECTION_CLOSE)); } - return $form; } - public static function getColleges() { - return self::$db->fetch_all('SELECT collegeid AS value, descr AS text FROM public.l_college'); + public static function getColleges() + { + return self::$db->fetchAll('SELECT collegeid AS value, descr AS text FROM public.l_college'); } /** * Create a new User, returning the user. At least one of Email OR eduroam * must be filled in. Password will be generated automatically and emailed to - * the user. - * - * @param string $fname The User's first name. - * @param string $sname The User's last name. - * @param string $eduroam The User's @york.ac.uk address. - * @param char $sex The User's gender. - * @param int $collegeid The User's college. - * @param string $email The User's non @york.ac.uk address. - * @param string $phone The User's phone number. - * @param bool $receive_email Whether the User should receive emails. - * @param float $paid How much the User has paid this Membership Year + * the user. See schema/api.json. + * + * @param array $params An assoc array (possibly decoded from JSON), taking a format generally based on what + * toDataSource produces fname and sname are required. + * * @return MyRadio_User + * * @throws MyRadioException */ - public static function create($fname, $sname, $eduroam = null, $sex = 'o', $collegeid = null, $email = null, $phone = null, $receive_email = true, $paid = 0.00) { - /** + public static function create($params) + { + $defaults = [ + 'eduroam' => null, + 'collegeid' => null, + 'email' => null, + 'phone' => null, + 'receive_email' => true, + 'paid' => 0.00, + ]; + + $params = array_merge($defaults, $params); + + /* * Deal with the UNIQUE constraint on the DB table. + * Some bad clients will pass this empty string (including SwaggerUI, as it doesn't support null types) */ - if ($phone === '') { - $phone = null; + if ($params['phone'] === '') { + $params['phone'] = null; } + if ($params['eduroam'] === '') { + $params['eduroam'] = null; + } + if ($params['email'] === '') { + $params['email'] = null; + } + //Validate input - if (empty($collegeid)) { - $collegeid = Config::$default_college; - } elseif (!is_numeric($collegeid)) { + if (empty($params['fname']) || empty($params['sname'])) { + throw new MyRadioException('fname and sname are required.', 400); + } + + if (empty($params['collegeid'])) { + $params['collegeid'] = Config::$default_college; + } elseif (!is_numeric($params['collegeid'])) { throw new MyRadioException('Invalid College ID!', 400); } - if (empty($eduroam) && empty($email)) { + if (empty($params['eduroam']) && empty($params['email'])) { throw new MyRadioException('At least one of eduroam or email must be provided.', 400); } - + //Require the user to be part of this eduroam domain - if (strstr($eduroam, '@') !== false && - strstr($eduroam, '@'.Config::$eduroam_domain) === false) { - throw new MyRadioException('Eduroam account should be @'.Config::$eduroam_domain.'! Use of other eduroam accounts is blocked. - This is a basic validation filter, so if there is a valid reason for another account to be here, this check - can be removed.', 400); + if (strstr($params['eduroam'], '@') !== false + && strstr($params['eduroam'], '@'.Config::$eduroam_domain) === false + ) { + throw new MyRadioException( + 'Eduroam account should be @'.Config::$eduroam_domain.'! Use of other eduroam accounts is blocked. + This is a basic validation filter, so if there is a valid reason for another account to be here, + this check can be removed.', + 400 + ); } - - //Remove the domain if it is set - $eduroam = str_replace('@'.Config::$eduroam_domain, '', $eduroam); - if (empty($eduroam) && empty($this->email)) { - throw new MyRadioException('Can\'t set both Email and Eduroam to null.', 400); + //Remove the domain if it is set and lowercase it + if (!empty($params['eduroam'])) { + $params['eduroam'] = str_replace('@'.Config::$eduroam_domain, '', $params['eduroam']); + $params['eduroam'] = strtolower($params['eduroam']); } - if ($sex !== 'm' && $sex !== 'f' && $sex !== 'o') { - throw new MyRadioException('User gender must be m, f or o!', 400); + if (empty($params['eduroam']) && empty($params['email'])) { + throw new MyRadioException('Can\'t set both Email and Eduroam to null.', 400); } - if (!is_numeric($paid)) { + if (!is_numeric($params['paid'])) { throw new MyRadioException('Invalid payment amount!', 400); } //Check if it looks like the user might already exist - if (MyRadio_User::findByEmail($eduroam) !== null or - MyRadio_User::findByEmail($email) !== null) { - throw new MyRadioException('This User already appears to exist. ' - . 'Their eduroam or email is already used.'); + if (self::findByEmail($params['eduroam']) !== null + or self::findByEmail($params['email']) !== null + ) { + throw new MyRadioException( + 'This User already appears to exist. ' + .'Their eduroam or email is already used.', + 400 + ); } //Looks good. Generate a password for them. - $plain_pass = CoreUtils::newPassword(); + $plain_pass = empty($params['provided_password']) ? CoreUtils::newPassword() : $params['provided_password']; //Actually create the member! - $r = self::$db->fetch_column('INSERT INTO public.member (fname, sname, sex, - college, phone, email, receive_email, eduroam, require_password_change) - VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9) RETURNING memberid', array( - $fname, - $sname, - $sex, - $collegeid, - $email, - $phone, - $receive_email, - $eduroam, - true - )); + $r = self::$db->fetchColumn( + 'INSERT INTO public.member (fname, sname, college, phone, + email, receive_email, eduroam, require_password_change) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING memberid', + [ + $params['fname'], + $params['sname'], + $params['collegeid'], + $params['phone'], + $params['email'], + $params['receive_email'], + $params['eduroam'], + true + ] + ); if (empty($r)) { throw new MyRadioException('Failed to create User!', 500); } $memberid = $r[0]; - $user = MyRadio_User::getInstance($memberid); + $user = self::getInstance($memberid); //Activate the member's account for the current academic year - $user->activateMemberThisYear($paid); + $user->activateMemberThisYear($params['paid']); //Set the user's password - Shibbobleh_Utils::setPassword($memberid, $plain_pass); + (new MyRadioDefaultAuthenticator())->setPassword($user, $plain_pass); //Send a welcome email (this will not send if receive_email is not enabled!) - /** + /* * @todo Make this easier to change * @todo Link to Facebook events */ - $uname = empty($eduroam) ? $email : str_replace('@york.ac.uk', '', $eduroam); - $welcome_email = str_replace(['#NAME', '#USER', '#PASS'], [$fname, $uname, $plain_pass], Config::$welcome_email); + $uname = empty($params['eduroam']) ? + $params['email'] : str_replace(Config::$eduroam_domain, '', $params['eduroam']); + if (!empty($params['provided_password'])) { + $plain_pass = '(The password you entered when registering)'; + } - //Send the email - MyRadioEmail::create(array('members' => array(MyRadio_User::getInstance($memberid))), 'Welcome to ' . Config::$short_name . ' - Getting Involved and Your Account', $welcome_email, 'getinvolved@' . Config::$email_domain); + $welcome_email = str_replace( + ['#NAME'], + [$params['fname']], + Config::$welcome_email + ); + $account_details_email = str_replace( + ['#NAME', '#USER', '#PASS'], + [$params['fname'], $uname, $plain_pass], + Config::$account_details_email + ); + + if (Config::$welcome_email_sender_memberid != null) { + $welcome_from = self::getInstance(Config::$welcome_email_sender_memberid); + } else { + $welcome_from = null; + } + + //Send the emails + MyRadioEmail::sendEmailToUser( + self::getInstance($memberid), + 'Welcome to '.Config::$short_name.' - Getting Involved', + $welcome_email, + $from = $welcome_from + ); + MyRadioEmail::sendEmailToUser( + self::getInstance($memberid), + 'Welcome to '.Config::$short_name.' - Your Account', + $account_details_email + );// comes from no-reply - return MyRadio_User::getInstance($memberid); + return $user; } /** * Update a User's account so that they are active for the current academic year. - * + * * Activating a membership re-activates basic access to web services, and * renews their mailing list subscriptions. - * - * @param int $paid - * @return boolean + * + * @param float $paid + * + * @return bool */ - public function activateMemberThisYear($paid = 0) { - self::$db->query('INSERT INTO public.member_year (memberid, year, paid) VALUES ($1, $2, $3)', array($this->getID(), CoreUtils::getAcademicYear(), $paid)); + public function activateMemberThisYear($paid = 0) + { + if (!$this->isActiveMemberForYear()) { + $this->setPayment($paid); + } return true; } /** - * Generates the form needed to quick-add URY members + * Creates a new User, or activates a user, if it already exists. + * + * @param string $fname The User's first name. + * @param string $sname The User's last name. + * @param string $eduroam The User's @york.ac.uk address. + * @param int $collegeid The User's college. + * @param string $email The User's non @york.ac.uk address. + * @param string $phone The User's phone number. + * @param bool $receive_email Whether the User should receive emails. + * @param float $paid How much the User has paid this Membership Year + * + * @api POST + * + * @return MyRadio_User|null + */ + public static function createOrActivate( + $fname, + $sname, + $eduroam = null, + $collegeid = null, + $email = null, + $phone = null, + $receive_email = true, + $paid = 0.00 + ) { + $user = self::findByEmail($eduroam); + // Fine, we'll try with the email then. + if ($user === null) { + $user = self::findByEmail($email); + } + + if ($user !== null && $user->activateMemberThisYear($paid)) { + return null; + } else { + $data = [ + 'fname' => $fname, + 'sname' => $sname, + 'eduroam' => $eduroam, + 'collegeid' => $collegeid, + 'email' => $email, + 'phone' => $phone, + 'receive_email' => $receive_email, + 'paid' => $paid, + ]; + return self::create($data); + } + } + + /** + * createOrActivate a user with protection behind recaptcha. + * + * @param string $fname The User's first name. + * @param string $sname The User's last name. + * @param string $eduroam The User's @york.ac.uk address. + * @param int $collegeid The User's college. + * @param string $email The User's non @york.ac.uk address. + * @param string $phone The User's phone number. + * + * @return mixed MyRadio_User if successful, Array of errors if not + */ + public static function createActivateAPI( + $fname, + $sname, + $captcha, + $eduroam = null, + $collegeid = null, + $email = null, + $phone = null + ) { + $captchaResponse = AuthUtils::verifyRecaptcha($captcha, $_SERVER['REMOTE_ADDR']); + + if ($captchaResponse === true) { + return self::createOrActivate($fname, $sname, $eduroam, $collegeid, $email, $phone); + } else { + return $captchaResponse; + } + } + + /** + * Checks whether the user is an active member (has a record in member_year) for the current year. + * + * @return bool + */ + public function isActiveMemberForYear($year = null) + { + // Use the current academic year as default if one isn't specified + if ($year === null) { + $year = CoreUtils::getAcademicYear(); + } + // If the current year exists in payments (even with a value of £0, the member is active) + foreach ($this->getAllPayments() as $payment) { + if ($payment['year'] == $year) { + return true; + } + } + + return false; + } + + public function grantPermission($authid, $from = null, $to = null) + { + if ($to !== null) { + $tostamp = CoreUtils::getTimestamp($to); + } else { + $tostamp = null; + } + self::$db->query( + 'INSERT INTO public.auth + (memberid, lookupid, starttime, endtime) VALUES ($1, $2, $3, $4)', + [$this->getID(), $authid, CoreUtils::getTimestamp($from), $to] + ); + + if (($from === null || $from < time()) && ($to === null || $to > time())) { + $this->permissions[] = (int) $authid; + } + } + + /** + * Generates the form needed to quick-add URY members. + * * @throws MyRadioException + * * @return MyRadioForm */ - public static function getQuickAddForm() { - if (!MyRadio_User::getInstance()->hasAuth(AUTH_ADDMEMBER)) { - throw new MyRadioException(MyRadio_User::getInstance() . ' tried to add members!'); + public static function getQuickAddForm() + { + if (!self::getInstance()->hasAuth(AUTH_ADDMEMBER)) { + throw new MyRadioException(self::getInstance().' tried to add members!'); } - $form = new MyRadioForm('profilequickadd', 'Profile', 'doQuickAdd', array('title' => 'Add Member (Quick)')); + $form = new MyRadioForm('profilequickadd', 'Profile', 'quickAdd', ['title' => 'Add Member (Quick)']); //Personal details - $form->addField(new MyRadioFormField('sec_personal', MyRadioFormField::TYPE_SECTION, array( - 'label' => 'Personal Details' - ))) - ->addField(new MyRadioFormField('fname', MyRadioFormField::TYPE_TEXT, array( - 'required' => true, - 'label' => 'First Name' - ))) - ->addField(new MyRadioFormField('sname', MyRadioFormField::TYPE_TEXT, array( + $form->addField( + new MyRadioFormField( + 'sec_personal', + MyRadioFormField::TYPE_SECTION, + [ + 'label' => 'Personal Details', + ] + ) + ) + ->addField( + new MyRadioFormField( + 'fname', + MyRadioFormField::TYPE_TEXT, + [ 'required' => true, - 'label' => 'Last Name' - ))) - ->addField(new MyRadioFormField('sex', MyRadioFormField::TYPE_SELECT, array( + 'label' => 'First Name', + ] + ) + ) + ->addField( + new MyRadioFormField( + 'sname', + MyRadioFormField::TYPE_TEXT, + [ 'required' => true, - 'label' => 'Gender', - 'options' => array( - array('value' => 'm', 'text' => 'Male'), - array('value' => 'f', 'text' => 'Female'), - array('value' => 'o', 'text' => 'Other') - ) - ))); + 'label' => 'Last Name', + ] + ) + ); //Contact details - $form->addField(new MyRadioFormField('sec_contact', MyRadioFormField::TYPE_SECTION, array( - 'label' => 'Contact Details' - ))) - ->addField(new MyRadioFormField('collegeid', MyRadioFormField::TYPE_SELECT, array( + $form->addField( + new MyRadioFormField( + 'sec_contact', + MyRadioFormField::TYPE_SECTION, + [ + 'label' => 'Contact Details', + ] + ) + ) + ->addField( + new MyRadioFormField( + 'collegeid', + MyRadioFormField::TYPE_SELECT, + [ 'required' => true, 'label' => 'College', - 'options' => self::getColleges() - ))) - ->addField(new MyRadioFormField('eduroam', MyRadioFormField::TYPE_TEXT, array( + 'options' => self::getColleges(), + ] + ) + ) + ->addField( + new MyRadioFormField( + 'eduroam', + MyRadioFormField::TYPE_TEXT, + [ 'required' => true, 'label' => 'University Email', - 'explanation' => '@york.ac.uk' - ))) - ->addField(new MyRadioFormField('phone', MyRadioFormField::TYPE_TEXT, array( + 'explanation' => '@'.Config::$eduroam_domain, + ] + ) + ) + ->addField( + new MyRadioFormField( + 'phone', + MyRadioFormField::TYPE_TEXT, + [ 'required' => false, - 'label' => 'Phone Number' - ))); + 'label' => 'Phone Number', + ] + ) + ); return $form; } /** - * Generates the form needed to bulk-add URY members + * Generates the form needed to bulk-add URY members. + * * @throws MyRadioException + * * @return MyRadioForm */ - public static function getBulkAddForm() { - if (!MyRadio_User::getInstance()->hasAuth(AUTH_ADDMEMBER)) { - throw new MyRadioException(MyRadio_User::getInstance() . ' tried to add members!'); + public static function getBulkAddForm() + { + if (!self::getInstance()->hasAuth(AUTH_ADDMEMBER)) { + throw new MyRadioException(self::getInstance().' tried to add members!'); } - $form = new MyRadioForm('profilebulkadd', 'Profile', 'doBulkAdd', array('title' => 'Add Member (Bulk)')); + $form = new MyRadioForm('profilebulkadd', 'Profile', 'bulkAdd', ['title' => 'Add Member (Bulk)']); //Personal details - $form->addField(new MyRadioFormField('bulkaddrepeater', MyRadioFormField::TYPE_TABULARSET, array( - 'options' => array( - new MyRadioFormField('fname', MyRadioFormField::TYPE_TEXT, array( - 'required' => true, - 'label' => 'First Name' - )), - new MyRadioFormField('sname', MyRadioFormField::TYPE_TEXT, array( - 'required' => true, - 'label' => 'Last Name' - )), - new MyRadioFormField('sex', MyRadioFormField::TYPE_SELECT, array( - 'required' => true, - 'label' => 'Gender', - 'options' => array( - array('value' => 'm', 'text' => 'Male'), - array('value' => 'f', 'text' => 'Female'), - array('value' => 'o', 'text' => 'Other') - ))), - new MyRadioFormField('collegeid', MyRadioFormField::TYPE_SELECT, array( - 'required' => true, - 'label' => 'College', - 'options' => self::getColleges() - )), - new MyRadioFormField('eduroam', MyRadioFormField::TYPE_TEXT, array( - 'required' => true, - 'label' => 'University Email', - 'explanation' => '@york.ac.uk' - )) + $form->addField( + new MyRadioFormField( + 'bulkaddrepeater', + MyRadioFormField::TYPE_TABULARSET, + [ + 'label' => "Member Details", + 'options' => [ + new MyRadioFormField( + 'fname', + MyRadioFormField::TYPE_TEXT, + [ + 'required' => true, + 'label' => 'First Name', + ] + ), + new MyRadioFormField( + 'sname', + MyRadioFormField::TYPE_TEXT, + [ + 'required' => true, + 'label' => 'Last Name', + ] + ), + new MyRadioFormField( + 'collegeid', + MyRadioFormField::TYPE_SELECT, + [ + 'required' => true, + 'label' => 'College', + 'options' => self::getColleges(), + ] + ), + new MyRadioFormField( + 'eduroam', + MyRadioFormField::TYPE_TEXT, + [ + 'required' => true, + 'label' => 'University Email', + 'explanation' => '@'.Config::$eduroam_domain, + ] + ), + ], + ] ) - ))); + ); return $form; } - public function toDataSource($full = true) { + public function getEmptyData(){ + $data['officerships'] = []; + $data['training'] = []; + $data['shows'] = []; + $data['paid'] = []; + $data['locked'] = false; + $data['college'] = 10; + $data['email'] = NULL; + $data['phone'] = NULL; + $data['eduroam'] = NULL; + $data['local_alias'] = ''; + $data['local_name'] = 'Hidden User'; + $data['last_login'] = NULL; + $data['payment'] = []; + $data['is_currently_paid'] = false; + $data['bio'] = 'This user is hidden'; + $data['memberid'] = $this->getID(); + $data['fname'] = 'Hidden'; + $datap['sname'] = 'User'; + $data['public_email'] = ''; + $data['url'] = $this->getURL(); + $data['receive_email'] = false; + $data['contract_signed'] = false; + $data['photo'] = Config::$default_person_uri; + $data['radioTime'] = $this->getRadioTime(); + return $data; + } + + /** + * @mixin officerships Provides 'officerships' that the user has held. + * @mixin training Provides the 'training' that the user has had. + * @mixin shows Provides the 'shows' that the user is a part of. + * @mixin personal_data Provides 'paid', 'locked', 'college' and other information considered personal. + */ + public function toDataSource($mixins = []) + { + if ($this->isProfileHidden()){ + return $this->getEmptyData(); + } + + $mixin_funcs = [ + 'officerships' => function (&$data) use ($mixins) { + $data['officerships'] = CoreUtils::setToDataSource($this->getOfficerships(), $mixins); + }, + 'all_officerships' => function (&$data) use ($mixins) { + $data['officerships'] = CoreUtils::setToDataSource($this->getOfficerships(true), $mixins); + }, + 'training' => function (&$data) { + $data['training'] = CoreUtils::dataSourceParser($this->getAllTraining()); + }, + 'shows' => function (&$data) { + $data['shows'] = CoreUtils::dataSourceParser($this->getShows()); + }, + 'personal_data' => function (&$data) { + $data['paid'] = $this->getAllPayments(); + $data['locked'] = $this->getAccountLocked(); + $data['college'] = $this->getCollege(); + $data['email'] = $this->getEmail(); + $data['phone'] = $this->getPhone(); + $data['eduroam'] = $this->getEduroam(); + $data['local_alias'] = $this->getLocalAlias(); + $data['local_name'] = $this->getLocalName(); + $data['last_login'] = $this->getLastLogin(); + }, + 'payment' => function (&$data) { + $data['payment'] = $this->getAllPayments(); + $data['is_currently_paid'] = $this->isCurrentlyPaid(); + }, + ]; + $data = [ 'memberid' => $this->getID(), - 'locked' => $this->getAccountLocked(), - 'college' => $this->getCollege(), 'fname' => $this->getFName(), 'sname' => $this->getSName(), - 'sex' => $this->getSex(), - 'receive_email' => $this->getReceiveEmail(), - 'public_email' => $this->getEmail(), + 'public_email' => $this->getPublicEmail(), 'url' => $this->getURL(), - 'local_name' => $this->getLocalName() + 'receive_email' => $this->getReceiveEmail(), + 'contract_signed' => $this->hasSignedContract() ]; - if ($full) { - $data['paid'] = $this->getAllPayments(); - $data['photo'] = $this->getProfilePhoto() === null ? - Config::$default_person_uri : $this->getProfilePhoto()->getURL(); - $data['bio'] = $this->getBio(); - $data['shows'] = CoreUtils::dataSourceParser( - $this->getShows(), false); - $data['officerships'] = $this->getOfficerships(); - $data['training'] = CoreUtils::dataSourceParser($this->getAllTraining(), false); - } + + $data['photo'] = $this->getProfilePhoto() === null ? + Config::$default_person_uri : $this->getProfilePhoto()->getURL(); + $data['bio'] = $this->getBio(); + $data['radioTime'] = $this->getRadioTime(); + + $this->addMixins($data, $mixins, $mixin_funcs); return $data; } + public static function getGraphQLTypeName() + { + return 'User'; + } } diff --git a/src/Classes/ServiceAPI/MyRadio_UserOfficership.php b/src/Classes/ServiceAPI/MyRadio_UserOfficership.php new file mode 100644 index 000000000..8f42934a2 --- /dev/null +++ b/src/Classes/ServiceAPI/MyRadio_UserOfficership.php @@ -0,0 +1,94 @@ +id = $data['member_officerid']; + $this->memberid = $data['memberid']; + $this->officerid = $data['officerid']; + $this->from_date = strtotime($data['from_date']); + $this->till_date = empty($data['till_date']) ? null : strtotime($data['till_date']); + } + + /** + * @return int + */ + public function getID() + { + return $this->id; + } + + /** + * @return MyRadio_User + */ + public function getUser() + { + return MyRadio_User::getInstance($this->memberid); + } + + /** + * @return MyRadio_Officer + */ + public function getOfficer() + { + return MyRadio_Officer::getInstance($this->officerid); + } + + /** + * @return int + */ + public function getFromDate() + { + return $this->from_date; + } + + /** + * @return int|null + */ + public function getTillDate() + { + return $this->till_date; + } + + protected static function factory($itemid) + { + $data = self::$db->fetchOne( + 'SELECT member_officerid, memberid, officerid, from_date, till_date + FROM public.member_officer + WHERE member_officerid = $1', + [$itemid] + ); + + if (empty($data)) { + throw new MyRadioException("Couldn't track down MyRadio_UserOfficership#$itemid", 404); + } + return new self($data); + } + + public function toDataSource($mixins = []) + { + return [ + 'id' => $this->id, + // Ensure we don't get infinite recursion + 'member' => $this->getUser()->toDataSource(), + 'officer' => $this->getOfficer()->toDataSource(), + 'from_date' => date("Y-m-d", $this->from_date), + 'till_date' => $this->till_date === null ? null : date("Y-m-d", $this->till_date), + // Compatibility with old MyRadio_User::getOfficerships + 'officer_name' => $this->getOfficer()->getName(), + ]; + } +} diff --git a/src/Classes/ServiceAPI/MyRadio_UserTrainingStatus.php b/src/Classes/ServiceAPI/MyRadio_UserTrainingStatus.php index 29cad04b5..e872de59a 100644 --- a/src/Classes/ServiceAPI/MyRadio_UserTrainingStatus.php +++ b/src/Classes/ServiceAPI/MyRadio_UserTrainingStatus.php @@ -1,202 +1,231 @@ - * @package MyRadio_Core */ +class MyRadio_UserTrainingStatus extends MyRadio_TrainingStatus +{ + /** + * The ID of the UserPresenterStatus. + * + * @var int + */ + private $memberpresenterstatusid; + + /** + * The User the TrainingStatus was awarded to. + * + * @var int + */ + private $user; + + /** + * The timestamp the UserTrainingStatus was Awarded. + * + * @var int + */ + private $awarded_time; + + /** + * The memberid of the User that granted this UserTrainingStatus. + * + * @var int + */ + private $awarded_by; + + /** + * The timestamp the UserTrainingStatus was Revoked (null if still active). + * + * @var int + */ + private $revoked_time; + + /** + * The memberid of the User that revoked this UserTrainingStatus. + * + * @var int + */ + private $revoked_by; + + /** + * Create a new UserTrainingStatus object. + * + * @param int $statusid The ID of the UserTrainingStatus. + * + * @throws MyRadioException + */ + protected function __construct($statusid) + { + $this->memberpresenterstatusid = (int) $statusid; + + $result = self::$db->fetchOne( + 'SELECT * FROM public.member_presenterstatus + WHERE memberpresenterstatusid=$1', + [$statusid] + ); + + if (empty($result)) { + throw new MyRadioException('The specified UserTrainingStatus ('.$statusid.') does not seem to exist', 404); + } + + $this->user = (int) $result['memberid']; + $this->awarded_time = strtotime($result['completeddate']); + $this->awarded_by = (int) $result['confirmedby']; + $this->revoked_time = $result['revokedtime'] ? strtotime($result['revokedtime']) : null; + $this->revoked_by = (int) $result['revokedby']; + + parent::__construct($result['presenterstatusid']); + } + + /** + * Get the memberpresenterstatusid. + * + * @return int + */ + public function getUserTrainingStatusID() + { + return $this->memberpresenterstatusid; + } -class MyRadio_UserTrainingStatus extends MyRadio_TrainingStatus { - - /** - * The ID of the UserPresenterStatus - * - * @var int - */ - private $memberpresenterstatusid; - - /** - * The User the TrainingStatus was awarded to. - * @var int - */ - private $user; - - /** - * The timestamp the UserTrainingStatus was Awarded - * @var int - */ - private $awarded_time; - - /** - * The memberid of the User that granted this UserTrainingStatus - * @var int - */ - private $awarded_by; - - /** - * The timestamp the UserTrainingStatus was Revoked (null if still active) - * @var int - */ - private $revoked_time; - - /** - * The memberid of the User that revoked this UserTrainingStatus - * @var int - */ - private $revoked_by; - - /** - * Create a new UserTrainingStatus object. - * - * @param int $statusid The ID of the UserTrainingStatus. - * @throws MyRadioException - */ - protected function __construct($statusid) { - $this->memberpresenterstatusid = (int)$statusid; - - $result = self::$db->fetch_one('SELECT * FROM public.member_presenterstatus - WHERE memberpresenterstatusid=$1', array($statusid)); - - if (empty($result)) { - throw new MyRadioException('The specified UserTrainingStatus ('.$statusid.') does not seem to exist'); + /** + * Get the User that Awarded this Training Status. + * + * @return MyRadio_User + */ + public function getAwardedBy() + { + return MyRadio_User::getInstance($this->awarded_by); } - - $this->user = (int)$result['memberid']; - $this->awarded_time = strtotime($result['completeddate']); - $this->awarded_by = (int)$result['confirmedby']; - $this->revoked_time = strtotime($result['revokedtime']); - $this->revoked_by = (int)$result['revokedby']; - - parent::__construct($result['presenterstatusid']); - } - - /** - * Get the memberpresenterstatusid - * @return int - */ - public function getUserTrainingStatusID() { - return $this->memberpresenterstatusid; - } - - /** - * Get the User that Awarded this Training Status - * @return MyRadio_User - */ - public function getAwardedBy() { - return MyRadio_User::getInstance($this->awarded_by); - } - - /** - * Get the User that was Awarded this Training Status - * @return MyRadio_User - */ - public function getAwardedTo($id = false) { - return $id ? $this->user : MyRadio_User::getInstance($this->user); - } - - /** - * Get the time the User was Awarded this Training Status - * @return int - */ - public function getAwardedTime() { - return $this->awarded_time; - } - - /** - * Get the User that Revoked this Training Status - * @return MyRadio_User|null - */ - public function getRevokedBy() { - return empty($this->revoked_by) ? null : MyRadio_User::getInstance($this->revoked_by); - } - - /** - * Get the time the User had this Training Status Revoked - * @return int - */ - public function getRevokedTime() { - return $this->revoked_time; - } - - /** - * Get an array of properties for this UserTrainingStatus. - * - * @return Array - */ - public function toDataSource($full = true) { - $data = parent::toDataSource(); - $data['user_status_id'] = $this->getUserTrainingStatusID(); - $data['awarded_to'] = [ - 'display' => 'text', - 'url' => $this->getAwardedTo()->getURL(), - 'value' => $this->getAwardedTo()->getName() - ]; - $data['awarded_by'] = $this->getAwardedBy()->toDataSource($full); - $data['awarded_time'] = $this->getAwardedTime(); - $data['revoked_by'] = ($this->getRevokedBy() === null ? null : - $this->getRevokedBy()->toDataSource($full)); - $data['revoked_time'] = $this->getRevokedTime(); - return $data; - } - - /** - * Creates a new User - Training Status map, awarding that User the training status. - * - * @param MyRadio_TrainingStatus $status The status to be awarded - * @param MyRadio_User $awarded_to The User to be awarded the training status - * @param MyRadio_User $awarded_by The User that is granting the training status - * @return \self - * @throws MyRadioException - */ - public static function create(MyRadio_TrainingStatus $status, MyRadio_User $awarded_to, - MyRadio_User $awarded_by = null) { - //Does the User already have this? - foreach ($awarded_to->getAllTraining(true) as $training) { - if ($training->getID() === $status->getID()) { - return $training; - } + + /** + * Get the User that was Awarded this Training Status. + * + * @return MyRadio_User + */ + public function getAwardedTo($id = false) + { + return $id ? $this->user : MyRadio_User::getInstance($this->user); } - - if ($awarded_by === null) { - $awarded_by = MyRadio_User::getInstance(); + + /** + * Get the time the User was Awarded this Training Status. + * + * @return int + */ + public function getAwardedTime() + { + return $this->awarded_time; } - - //Check whether this user can do that. - if (in_array(array_map(function($x){return $x->getID();}, $awarded_by->getAllTraining(true)), - $status->getAwarder()->getID()) === false) { - throw new MyRadioException($awarded_by .' does not have permission to award '.$status); + + /** + * Get the User that Revoked this Training Status. + * + * @return MyRadio_User|null + */ + public function getRevokedBy() + { + return empty($this->revoked_by) ? null : MyRadio_User::getInstance($this->revoked_by); } - //Check whether the target user has the prerequisites - if ($status->getDepends() !== null and in_array($status->getDepends()->getID(), - array_map(function($x){return $x->getID();}, $awarded_to->getAllTraining(true))) === false) { - throw new MyRadioException($awarded_to .' does not have the prerequisite training to be awarded '.$status); + + /** + * Get the time the User had this Training Status Revoked. + * + * @return int + */ + public function getRevokedTime() + { + return $this->revoked_time; + } + + /** + * Get an array of properties for this UserTrainingStatus. + * @param array $mixins Mixins + * @return array + */ + public function toDataSource($mixins = []) + { + $data = parent::toDataSource(); + $data['user_status_id'] = $this->getUserTrainingStatusID(); + $data['awarded_to'] = [ + 'display' => 'text', + 'url' => $this->getAwardedTo()->getURL(), + 'value' => $this->getAwardedTo()->getName(), + ]; + $data['awarded_by'] = [ + 'display' => 'text', + 'url' => $this->getAwardedBy()->getURL(), + 'value' => $this->getAwardedBy()->getName(), + ]; + $data['awarded_time'] = $this->getAwardedTime(); + $data['revoked_by'] = ($this->getRevokedBy() === null ? null : + $this->getRevokedBy()->toDataSource($mixins)); + $data['revoked_time'] = $this->getRevokedTime(); + + return $data; } - - $id = self::$db->fetch_column('INSERT INTO public.member_presenterstatus ' - . '(memberid, presenterstatusid, confirmedby) VALUES' - . '($1, $2, $3) RETURNING memberpresenterstatusid', [ + + /** + * Creates a new User - Training Status map, awarding that User the training status. + * + * @param MyRadio_TrainingStatus $status The status to be awarded + * @param MyRadio_User $awarded_to The User to be awarded the training status + * @param MyRadio_User $awarded_by The User that is granting the training status + * + * @return \self + * + * @throws MyRadioException + */ + public static function create( + MyRadio_TrainingStatus $status, + MyRadio_User $awarded_to, + MyRadio_User $awarded_by = null + ) { + //Does the User already have this? + foreach ($awarded_to->getAllTraining(true) as $training) { + if ($training->getID() === $status->getID()) { + return $training; + } + } + + if ($awarded_by === null) { + $awarded_by = MyRadio_User::getInstance(); + } + + if (!$status->canAward($awarded_by)) { + throw new MyRadioException($awarded_by.' does not have permission to award '.$status); + } + + if (!$status->hasDependency($awarded_to)) { + throw new MyRadioException($awarded_to.' does not have the prerequisite training to be awarded '.$status); + } + + $id = self::$db->fetchColumn( + 'INSERT INTO public.member_presenterstatus (memberid, presenterstatusid, confirmedby) + VALUES ($1, $2, $3) RETURNING memberpresenterstatusid', + [ $awarded_to->getID(), $status->getID(), - $awarded_by->getID() - ])[0]; - - //Force the User to be updated on next request. - self::$cache->delete(MyRadio_User::getCacheKey($awarded_to->getID())); - - return new self($id); - } + $awarded_by->getID(), + ] + )[0]; + + //Force the User to be updated on next request. + self::$cache->delete(MyRadio_User::getCacheKey($awarded_to->getID())); + return new self($id); + } } diff --git a/src/Classes/ServiceAPI/MyRadio_Webcam.php b/src/Classes/ServiceAPI/MyRadio_Webcam.php index d256bb22a..67ef2f0e8 100644 --- a/src/Classes/ServiceAPI/MyRadio_Webcam.php +++ b/src/Classes/ServiceAPI/MyRadio_Webcam.php @@ -1,100 +1,158 @@ + * Provides the MyRadio_Webcam class. */ +namespace MyRadio\ServiceAPI; + +use MyRadio\Config; +use MyRadio\ServiceAPI\MyRadio_User; +use MyRadio\MyRadioException; /** - * @todo Document + * Deals with Webcam features within MyRadio. + * + * @uses \Database */ -class MyRadio_Webcam extends ServiceAPI { - - public static function getStreams() { - return self::$db->fetch_all('SELECT * FROM webcam.streams ORDER BY streamid ASC'); - } - - public static function incrementViewCounter(MyRadio_User $user) { - //Get the current view counter. We do this as a separate query in case the row doesn't exist yet - $counter = self::$db->fetch_one('SELECT timer FROM webcam.memberviews WHERE memberid = $1', array($user->getID())); - if (empty($counter)) { - $counter = 0; - $sql = 'INSERT INTO webcam.memberviews (memberid, timer) VALUES ($1, $2)'; - } else { - $counter = $counter['timer']; - $sql = 'UPDATE webcam.memberviews SET timer=$2 WHERE memberid=$1'; +class MyRadio_Webcam extends ServiceAPI +{ + public static function getStreams() + { + return self::$db->fetchAll('SELECT * FROM webcam.streams ORDER BY streamid ASC'); } - $counter += 15; - - self::$db->query($sql, array($user->getID(), $counter)); - return $counter; - } - - /** - * Returns the available range of times for the Webcam Archives - */ - public static function getArchiveTimeRange() { - $files = scandir(Config::$webcam_archive_path); - $earliest = time(); - $latest = time(); - foreach ($files as $file) { - //Files are stored in the format yyyymmddhh-cam.mpg, if it doesn't match that pattern, skip it - if (!preg_match('/^[0-9]{10}\-[a-zA-Z0-9\-]+\.mpg$/', $file)) continue; - //Get a nicer timestamp format PHP can work with - $str = preg_replace('/^([0-9]{4})([0-9]{2})([0-9]{2})([0-9]{2}).*$/', '$1-$2-$3 $4:00:00', $file); - $time = strtotime($str); - if ($time < $earliest) $earliest = $time; - if ($time > $latest) $latest = $time; + + /** + * Increments the logged in user's webcam counter. + */ + public static function incrementViewCounter() + { + $user = MyRadio_User::getCurrentUser(); // This bit will fail if it's not an actual user calling the API. + //Get the current view counter. We do this as a separate query in case the row doesn't exist yet + $counter = self::getViewCounter($user); + + if (isset($_SESSION['webcam_lastcounterincrement']) && $_SESSION['webcam_lastcounterincrement'] > time() -10) { + /* + * Occurs when browser wakes up and tries to spam all the missed updates, + * or if multiple webcam pages are open. In this case, don't actually increment. + */ + throw new MyRadioException('Requested increment too soon after last increment.', 400); + } + + // We haven't tried to increment the webcam recently, allow it and update the time it was last incremented. + $_SESSION['webcam_lastcounterincrement'] = time(); + if (empty($counter)) { + $counter = 0; + $sql = 'INSERT INTO webcam.memberviews (memberid, timer) VALUES ($1, $2)'; + } else { + $counter = $counter['timer']; + $sql = 'UPDATE webcam.memberviews SET timer=$2 WHERE memberid=$1'; + } + + /* + * We must assume this, instead of calculating it. + * This is because the session remains if you close the webcam page, + * so would count the time if you closed and re-opened the webcam page + */ + $counter += 15; + + + self::$db->query($sql, [$user->getID(), $counter]); + + return $counter; + } + + public static function getViewCounter(MyRadio_User $user) + { + $counter = self::$db->fetchOne('SELECT timer FROM webcam.memberviews WHERE memberid = $1', [$user->getID()]); + return $counter; } - echo $earliest.'='.$latest; - } - - /** - * Returns the id and location of the currentl selected webcam - * @return array webcam id and location - */ - public static function getCurrentWebcam() { - $current = file_get_contents(Config::$webcam_current_url); - - switch ($current) { - case '0': $location = 'Jukebox'; - break; - case '2': $location = 'Studio 1'; - break; - case '3': $location = 'Studio 1 Secondary'; - break; - case '4': $location = 'Studio 2'; - break; - case '5': $location = 'Office'; - break; - case '6': $location = 'Hall'; - break; - case '8': $location = 'OB'; - break; - default: $location = $current; - $current = 7; - break; + + /** + * Returns the available range of times for the Webcam Archives. + */ + public static function getArchiveTimeRange() + { + $files = scandir(Config::$webcam_archive_path); + $earliest = time(); + $latest = time(); + foreach ($files as $file) { + //Files are stored in the format yyyymmddhh-cam.mpg, if it doesn't match that pattern, skip it + if (!preg_match('/^[0-9]{10}\-[a-zA-Z0-9\-]+\.mpg$/', $file)) { + continue; + } + //Get a nicer timestamp format PHP can work with + $str = preg_replace('/^([0-9]{4})([0-9]{2})([0-9]{2})([0-9]{2}).*$/', '$1-$2-$3 $4:00:00', $file); + $time = strtotime($str); + if ($time < $earliest) { + $earliest = $time; + } + if ($time > $latest) { + $latest = $time; + } + } + echo $earliest.'='.$latest; } - return [ - 'current' => $current, - 'webcam' => $location - ]; - } + /** + * Returns the id and location of the currentl selected webcam. + * + * @return array webcam id and location + */ + public static function getCurrentWebcam() + { + if (Config::$webcam_current_url) { + $response = file_get_contents(Config::$webcam_current_url); + $response = json_decode($response, true); + $streams = self::getStreams(); + switch ($response['camera']) { + case 'cam1': + $location = 'Jukebox'; + break; + case 'cam2': + $location = 'Outside Broadcast'; + break; + case 'webstudio': + $location = 'WebStudio'; + break; + case 'offair': + $location = 'Off Air'; + break; + default: + $location = "Unknown Source"; + foreach ($streams as $stream) { + if ($stream['camera'] == $response['camera']) { + $location = $stream["streamname"]; + break; + } + } + } - /** - * [setWebcam description] - * @param [type] $id [description] - */ - public static function setWebcam($id) { - if (($id == 0) || - ($id == 2) || - ($id == 3) || - ($id == 4) || - ($id == 8) || - (!strncmp($id, "http://", strlen("http://")))) { - file_get_contents(Config::$webcam_set_url.$id); + return [ + 'camera' => $response['camera'], + 'location' => $location, + ]; + } else { + return [ + 'camera' => -1, + 'location' => null, + ]; + } } - } -} \ No newline at end of file + /** + * Changes the currently public live webcam. + * + * @param string $id A string of the correct camera. + */ + public static function setWebcam($id) + { + $validCams = ['studio1', 'studio2', 'cam1', 'cam2', 'cam5', 'hall', 'office']; + if (in_array($id, $validCams)) { + $ch = \curl_init(Config::$webcam_set_url.$id); + \curl_setopt($ch, CURLOPT_POST, true); + \curl_setopt($ch, CURLOPT_AUTOREFERER, 1); + \curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + + \curl_exec($ch); // ignore response + } + } +} diff --git a/src/Classes/ServiceAPI/Profile.php b/src/Classes/ServiceAPI/Profile.php index b917cf30f..ccd96a4a7 100644 --- a/src/Classes/ServiceAPI/Profile.php +++ b/src/Classes/ServiceAPI/Profile.php @@ -1,154 +1,153 @@ - * @author Lloyd Wallis - * @todo Merge into User - * @package MyRadio_Profile - * @version 20130516 - * @uses \Database - * @uses \CacheProvider + * @uses \Database + * @uses \CacheProvider */ -class Profile extends ServiceAPI { - /** - * Stores an Array representation of all members from the getAllMembers function when it is first called. - * This is also cached using a CacheProvider - * @var Array - */ - private static $allMembers = null; - /** - * Stores an Array representation of this year's members from the getThisYearsMembers function when it is first called - * This is also cached using a CacheProvider - * @var Array - */ - private static $thisYearsMembers = null; - /** - * Stores an Array representation of the current officers from the getCurrentOfficers function when it is first called - * This is also cached using a CacheProvider - * @var Array - */ - private static $currentOfficers = null; - /** - * Stores an Array representation of the current officerships and members holding them from the getOfficers function when it is first called - * This is also cached using a CacheProvider - * @var Array - */ - private static $officers = null; - - /** - * Returns an Array representation of all URY members. On first run, this is cached locally in the class, and - * shared in the CacheProvider until the Cache is cleared - * - * @return Array A two-dimensional Array, each element in the first dimension container the following details about - * a member, sorted by their name: - * - * memberid: The user's unique memberid - * name: The user's last and first names formatted as sname, fname - * college: The name of the member's college (not the ID!) - * paid: How much the member has paid this year - */ - public static function getAllMembers() { - //Return the object if it is cached - self::$allMembers = self::$cache->get('MyRadioProfile_allMembers'); - if (self::$allMembers === false) { - self::wakeup(); - self::$allMembers = - self::$db->fetch_all('SELECT member.memberid, sname || \', \' || fname AS name, l_college.descr AS college, paid - FROM member LEFT JOIN (SELECT * FROM member_year WHERE year = $1) AS member_year - ON ( member.memberid = member_year.memberid ), l_college - WHERE member.college = l_college.collegeid - ORDER BY sname ASC', array(CoreUtils::getAcademicYear())); - self::$cache->set('MyRadioProfile_allMembers', self::$allMembers); +class Profile extends ServiceAPI +{ + /** + * Stores an Array representation of the current officers from the getCurrentOfficers function when it is first + * called. This is also cached using a CacheProvider. + * + * @var array + */ + private static $currentOfficers = null; + /** + * Stores an Array representation of the current officerships and members holding them from the getOfficers function + * when it is first called This is also cached using a CacheProvider. + * + * @var array + */ + private static $officers = null; + + /** + * Clears (well, deletes) the cache objects used in Profile. + */ + public static function clearCache() + { + self::$cache->delete('MyRadioProfile_allMembers'); + self::$cache->delete('MyRadioProfile_currentOfficers'); + self::$cache->delete('MyRadioProfile_officers'); } - - return self::$allMembers; - } - - /** - * Returns an Array representation of this year's URY Members. On first run, this is cached locally in the class, and - * shared in the CacheProvider until the Cache is cleared - * - * @return Array A two-dimensional Array, each element in the first dimension container the following details about - * a member, sorted by their name: - * - * memberid: The user's unique memberid - * name: The user's last and first names formatted as sname, fname - * college: The name of the member's college (not the ID!) - * paid: How much the member has paid this year - */ - public static function getThisYearsMembers() { - self::wakeup(); - return self::$db->fetch_all('SELECT member.memberid, sname || \', \' || fname AS name, l_college.descr AS college, paid - FROM member INNER JOIN (SELECT * FROM member_year WHERE year = $1) AS member_year - ON ( member.memberid = member_year.memberid ), l_college - WHERE member.college = l_college.collegeid - ORDER BY sname ASC', array(CoreUtils::getAcademicYear())); - } - /** - * Returns an Array representation of the current URY Officers. On first run, this is cached locally in the class, and - * shared in the CacheProvider until the Cache is cleared - * - * @return Array A two-dimensional Array, each element in the first dimension container the following details about - * officer, sorted by their officer ordering: - * - * team: The team the officer is in - * officership: The current position held - * name: The user's last and first names formatted as sname, fname - * memberid: The user's unique memberid - */ - public static function getCurrentOfficers() { - //Return the object if it is cached - self::$currentOfficers = self::$cache->get('MyRadioProfile_currentOfficers'); - if (self::$currentOfficers === false) { - self::wakeup(); - self::$currentOfficers = - self::$db->fetch_all('SELECT team.team_name AS team, officer.officer_name AS officership, sname || \', \' || fname AS name, member.memberid - FROM member, officer, member_officer, team - WHERE member_officer.memberid = member.memberid AND officer.officerid = member_officer.officerid AND officer.teamid = team.teamid AND member_officer.till_date IS NULL - ORDER BY team.ordering, officer.ordering, sname'); - self::$cache->set('MyRadioProfile_currentOfficers', self::$currentOfficers); + /** + * Returns an Array representation of this year's URY Members. + * + * @return array A two-dimensional Array, each element in the first dimension container the following details about + * a member, sorted by their name: + * + * memberid: The user's unique memberid + * name: The user's last and first names formatted as sname, fname + * college: The name of the member's college (not the ID!) + * paid: How much the member has paid this year + */ + public static function getThisYearsMembers() + { + return self::getMembersForYear(CoreUtils::getAcademicYear()); + } + + /** + * Returns an Array representation of the given year's URY Members. + * + * @return array A two-dimensional Array, each element in the first dimension container the following details about + * a member, sorted by their name: + * + * memberid: The user's unique memberid + * name: The user's last and first names formatted as sname, fname + * college: The name of the member's college (not the ID!) + * paid: How much the member has paid this year + */ + public static function getMembersForYear($year) + { + self::wakeup(); + + return self::$db->fetchAll( + 'SELECT member.memberid, sname || \', \' || fname AS name, l_college.descr AS college, paid, email, eduroam + FROM member INNER JOIN (SELECT * FROM member_year WHERE year = $1) AS member_year + ON ( member.memberid = member_year.memberid ), l_college + WHERE member.college = l_college.collegeid + ORDER BY sname ASC', + [$year] + ); } - - return self::$currentOfficers; - } - - - /** - * Returns an Array representation of the current URY officerships and the member holding them. On first run, this is cached locally in the class, and - * shared in the CacheProvider until the Cache is cleared - * - * @return Array A two-dimensional Array, each element in the first dimension container the following details about - * a member, sorted by their name: - * - * team: The team the officer is in - * officership: The current position held - * name: The user's last and first names formatted as fname sname (If officership is filled else NULL) - * memberid: The user's unique memberid (If officership is filled else NULL) - */ - public static function getOfficers() { - //Return the object if it is cached - self::$officers = self::$cache->get('MyRadioProfile_officers'); - if (self::$officers === false) { - self::wakeup(); - self::$officers = - self::$db->fetch_all('SELECT team.team_name AS team, officer.type, officer.officer_name AS officership, - fname || \' \' || sname AS name, member.memberid, officer.officerid - FROM team - LEFT JOIN officer ON team.teamid = officer.teamid AND officer.status = \'c\' - LEFT JOIN member_officer ON officer.officerid = member_officer.officerid AND member_officer.till_date IS NULL - LEFT JOIN member ON member_officer.memberid = member.memberid - WHERE team.status = \'c\' AND officer.type != \'m\' - ORDER BY team.ordering, officer.ordering, sname'); - self::$cache->set('MyRadioProfile_officers', self::$officers); + + /** + * Returns an Array representation of the current URY Officers. On first run, this is cached locally in the class, + * and shared in the CacheProvider until the Cache is cleared. + * + * @return array A two-dimensional Array, each element in the first dimension container the following details about + * officer, sorted by their officer ordering: + * + * team: The team the officer is in + * officership: The current position held + * name: The user's last and first names formatted as sname, fname + * memberid: The user's unique memberid + */ + public static function getCurrentOfficers() + { + //Return the object if it is cached + self::$currentOfficers = self::$cache->get('MyRadioProfile_currentOfficers'); + if (self::$currentOfficers === false) { + self::wakeup(); + self::$currentOfficers = self::$db->fetchAll( + 'SELECT team.team_name AS team, officer.officer_name AS officership, + sname || \', \' || fname AS name, member.memberid + FROM member, officer, member_officer, team + WHERE member_officer.memberid = member.memberid + AND officer.officerid = member_officer.officerid + AND officer.teamid = team.teamid + AND member_officer.till_date IS NULL + ORDER BY team.ordering, officer.ordering, sname' + ); + self::$cache->set('MyRadioProfile_currentOfficers', self::$currentOfficers); + } + + return self::$currentOfficers; + } + + /** + * Returns an Array representation of the current URY officerships and the member holding them. On first run, this + * is cached locally in the class, and shared in the CacheProvider until the Cache is cleared. + * + * @return array A two-dimensional Array, each element in the first dimension container the following details about + * a member, sorted by their name: + * + * team: The team the officer is in + * officership: The current position held + * name: The user's last and first names formatted as fname sname (If officership is filled else NULL) + * memberid: The user's unique memberid (If officership is filled else NULL) + */ + public static function getOfficers() + { + //Return the object if it is cached + self::$officers = self::$cache->get('MyRadioProfile_officers'); + if (self::$officers === false) { + self::wakeup(); + self::$officers = self::$db->fetchAll( + 'SELECT team.team_name AS team, officer.type, officer.officer_name AS officership, + fname || \' \' || sname AS name, member.memberid, officer.officerid + FROM team + LEFT JOIN officer ON team.teamid = officer.teamid AND officer.status = \'c\' + LEFT JOIN member_officer ON officer.officerid = member_officer.officerid + AND member_officer.till_date IS NULL + LEFT JOIN member ON member_officer.memberid = member.memberid + WHERE team.status = \'c\' AND officer.type != \'m\' + ORDER BY team.ordering, officer.ordering, sname' + ); + self::$cache->set('MyRadioProfile_officers', self::$officers); + } + + return self::$officers; } - - return self::$officers; - } -} \ No newline at end of file +} diff --git a/src/Classes/ServiceAPI/ServiceAPI.php b/src/Classes/ServiceAPI/ServiceAPI.php index 654a1b341..e2d2209ed 100644 --- a/src/Classes/ServiceAPI/ServiceAPI.php +++ b/src/Classes/ServiceAPI/ServiceAPI.php @@ -1,156 +1,215 @@ - * @version 20130707 - * @package MyRadio_Core - * @uses \Database - * @uses \CacheProvider + * @uses \Database + * @uses \CacheProvider */ -abstract class ServiceAPI implements IServiceAPI, MyRadio_DataSource { - - /** - * All ServiceAPI subclasses will contain a reference to the Database Singleton - * @var \Database - */ - protected static $db = null; - /** - * All ServiceAPI subclasses will contain a reference to the CacheProvider Singleton - * @var \CacheProvider - */ - protected static $cache = null; - - /** - * Start up the connection to the Database - */ - protected static function initDB() { - if (!self::$db) { - self::$db = Database::getInstance(); - } - } - - /** - * Start up the connection to the CacheProvider - */ - protected static function initCache() { - if (!self::$cache) { - $cache = Config::$cache_provider; - self::$cache = $cache::getInstance(); - } - } - - /** - * A magic function that will reload the Database and CacheProvider after the object has been loaded from Cache - */ - public function __wakeup() { - self::wakeup(); - } - - public static function wakeup() { - self::initDB(); - self::initCache(); - } - - public static function getInstance($itemid) { - self::initCache(); - self::initDB(); - - $class = get_called_class(); - $key = $class::getCacheKey($itemid); - $cache = self::$cache->get($key); - if (!$cache) { - $cache = new $class($itemid); - self::$cache->set($key, $cache, 86400); - } - - return $cache; - } - - public function toDataSource($full = false) { - throw new MyRadioException(get_called_class() . ' has not had a DataSource Conversion Method Defined!', 500); - } - - /** - * Iteratively calls the toDataSource method on all of the objects in the given array, returning the results as - * a new array. - * @param Array $array - * @param bool $full If true, will return expanded data if available. - * @return Array - * @throws MyRadioException Throws an Exception if a provided object is not a DataSource - */ - public static function setToDataSource($array, $full = false) { - if (!is_array($array)) { - return $array; - } - $result = array(); - foreach ($array as $element) { - //It must implement the toDataSource method! - if (!method_exists($element, 'toDataSource')) { - throw new MyRadioException('Attempted to convert '.get_class($element).' to a DataSource but it not a valid Data Object!', 500); - } else { - $result[] = $element->toDataSource($full); - } - } - return $result; - } - - public function __toString() { - return get_called_class().'-'.$this->getID(); - } - - /** - * Takes an array of IDs, and creates an array of the relevant objects - * @param int[] $ids - * @return ServiceAPI[] - */ - public static function resultSetToObjArray($ids) { - $response = array(); - $child = get_called_class(); - if (!is_array($ids) or empty($ids)) { - return []; - } - foreach ($ids as $id) { - $response[] = $child::getInstance($id); - } - - return $response; - } - - protected function __construct() {} - - /** - * Generates the Key string for caching services - * - * @param int $id The ID of the object to get the cache key for - * @return String - */ - public static function getCacheKey($id) { - return get_called_class() . '-' . $id; - } - - /** - * Sets the cache for this object to be the current object state. - * - * This should always be called after a setSomething. - */ - protected function updateCacheObject() { - self::$cache->set(self::getCacheKey($this->getID()), $this, 3600); - } - - /** - * Removes singleton instance. Used for memory optimisation for very large - * requests. - * @deprecated - */ - public function removeInstance() { - return true; - unset(self::$singletons[self::getCacheKey($this->getID())]); - } +abstract class ServiceAPI +{ + /** + * All ServiceAPI subclasses will contain a reference to the Database Singleton. + * + * @var Database + */ + protected static $db = null; + /** + * All ServiceAPI subclasses will contain a reference to the CacheProvider Singleton. + * + * @var CacheProvider + */ + protected static $cache = null; + + protected $change = false; + + /** + * Start up the connection to the Database. + */ + protected static function initDB() + { + if (!self::$db) { + self::$db = Database::getInstance(); + } + } + + /** + * Start up the connection to the CacheProvider. + */ + protected static function initCache() + { + if (!self::$cache) { + $cache = Config::$cache_provider; + self::$cache = $cache::getInstance(); + } + } + + /** + * A magic function that will reload the Database and CacheProvider after the object has been loaded from Cache. + */ + public function __wakeup() + { + self::wakeup(); + } + + public static function wakeup() + { + self::initDB(); + self::initCache(); + } + + /** + * Loads an instance of this object from either the cache or the DB. + * @param $itemid + * @return static + */ + public static function getInstance($itemid) + { + self::initCache(); + self::initDB(); + + $class = get_called_class(); + $key = self::getCacheKey($itemid); + $cache = self::$cache->get($key); + if (!$cache) { + $cache = $class::factory($itemid); + self::$cache->set($key, $cache); + } + + return $cache; + } + + protected static function factory($itemid) + { + return new static($itemid); + } + + protected function addMixins(&$data, $mixins, $mixin_funcs, $strict = true) + { + foreach ($mixins as $mixin) { + if (array_key_exists($mixin, $mixin_funcs)) { + $mixin_funcs[$mixin]($data); + } else { + throw new MyRadioException('Unsupported mixin '.$mixin, 400); + } + } + } + + /** + * Base method for serialising the class. + * @param array $mixins Mixins + * @return array + */ + public function toDataSource($mixins = []) + { + throw new MyRadioException( + get_called_class() . ' has not had a DataSource Conversion Method Defined!', + 500 + ); + } + + public function __toString() + { + return get_called_class().'-'.$this->getID(); + } + + /** + * Takes an array of IDs, and creates an array of the relevant objects. + * + * @param int[] $ids + * + * @return static[] + */ + public static function resultSetToObjArray($ids): array + { + $response = []; + $child = get_called_class(); + if (!is_array($ids) or empty($ids)) { + return []; + } + foreach ($ids as $id) { + $response[] = $child::getInstance($id); + } + + return $response; + } + + /** + * Get the name of this ServiceAPI in the GraphQL schema. + * @return string + */ + public static function getGraphQLTypeName() + { + throw new MyRadioException('Tried to call getGraphQLTypeName on a type that it shouldn\'t be called on!'); + } + + protected function __construct() + { + } + + public function __destruct() + { + if ($this->change) { + $this->write(); + } + } + + /** + * Generates the Key string for caching services. + * + * @param int $id The ID of the object to get the cache key for + * + * @return string + */ + public static function getCacheKey($id) + { + return get_called_class().'-'.$id; + } + + /** + * Sets the cache for this object to be the current object state. + * + * This should always be called after a setSomething. + * @param bool $forceWrite whether to immediately write to the cache, or wait until the object is destroyed + */ + protected function updateCacheObject(bool $forceWrite = false) + { + if ($forceWrite) { + $this->write(); + } else { + $this->change = true; + } + } + + /** + * Writes this object to the cache. + */ + private function write(): void + { + $this->change = false; + self::$cache->set(self::getCacheKey($this->getID()), $this); + } + + /** + * Removes singleton instance. Used for memory optimisation for very large + * requests. + * + * @deprecated + */ + public function removeInstance() + { + return true; + unset(self::$singletons[self::getCacheKey($this->getID())]); + } + } diff --git a/src/Classes/Vendor/getid3/extension.cache.dbm.php b/src/Classes/Vendor/getid3/extension.cache.dbm.php deleted file mode 100755 index 7ac4fce90..000000000 --- a/src/Classes/Vendor/getid3/extension.cache.dbm.php +++ /dev/null @@ -1,208 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// // -// extension.cache.dbm.php - part of getID3() // -// Please see readme.txt for more information // -// /// -///////////////////////////////////////////////////////////////// -// // -// This extension written by Allan Hansen // -// /// -///////////////////////////////////////////////////////////////// - - -/** -* This is a caching extension for getID3(). It works the exact same -* way as the getID3 class, but return cached information very fast -* -* Example: -* -* Normal getID3 usage (example): -* -* require_once 'getid3/getid3.php'; -* $getID3 = new getID3; -* $getID3->encoding = 'UTF-8'; -* $info1 = $getID3->analyze('file1.flac'); -* $info2 = $getID3->analyze('file2.wv'); -* -* getID3_cached usage: -* -* require_once 'getid3/getid3.php'; -* require_once 'getid3/getid3/extension.cache.dbm.php'; -* $getID3 = new getID3_cached('db3', '/tmp/getid3_cache.dbm', -* '/tmp/getid3_cache.lock'); -* $getID3->encoding = 'UTF-8'; -* $info1 = $getID3->analyze('file1.flac'); -* $info2 = $getID3->analyze('file2.wv'); -* -* -* Supported Cache Types -* -* SQL Databases: (use extension.cache.mysql) -* -* cache_type cache_options -* ------------------------------------------------------------------- -* mysql host, database, username, password -* -* -* DBM-Style Databases: (this extension) -* -* cache_type cache_options -* ------------------------------------------------------------------- -* gdbm dbm_filename, lock_filename -* ndbm dbm_filename, lock_filename -* db2 dbm_filename, lock_filename -* db3 dbm_filename, lock_filename -* db4 dbm_filename, lock_filename (PHP5 required) -* -* PHP must have write access to both dbm_filename and lock_filename. -* -* -* Recommended Cache Types -* -* Infrequent updates, many reads any DBM -* Frequent updates mysql -*/ - - -class getID3_cached_dbm extends getID3 -{ - - // public: constructor - see top of this file for cache type and cache_options - public function getID3_cached_dbm($cache_type, $dbm_filename, $lock_filename) { - - // Check for dba extension - if (!extension_loaded('dba')) { - throw new Exception('PHP is not compiled with dba support, required to use DBM style cache.'); - } - - // Check for specific dba driver - if (!function_exists('dba_handlers') || !in_array($cache_type, dba_handlers())) { - throw new Exception('PHP is not compiled --with '.$cache_type.' support, required to use DBM style cache.'); - } - - // Create lock file if needed - if (!file_exists($lock_filename)) { - if (!touch($lock_filename)) { - throw new Exception('failed to create lock file: '.$lock_filename); - } - } - - // Open lock file for writing - if (!is_writeable($lock_filename)) { - throw new Exception('lock file: '.$lock_filename.' is not writable'); - } - $this->lock = fopen($lock_filename, 'w'); - - // Acquire exclusive write lock to lock file - flock($this->lock, LOCK_EX); - - // Create dbm-file if needed - if (!file_exists($dbm_filename)) { - if (!touch($dbm_filename)) { - throw new Exception('failed to create dbm file: '.$dbm_filename); - } - } - - // Try to open dbm file for writing - $this->dba = dba_open($dbm_filename, 'w', $cache_type); - if (!$this->dba) { - - // Failed - create new dbm file - $this->dba = dba_open($dbm_filename, 'n', $cache_type); - - if (!$this->dba) { - throw new Exception('failed to create dbm file: '.$dbm_filename); - } - - // Insert getID3 version number - dba_insert(getID3::VERSION, getID3::VERSION, $this->dba); - } - - // Init misc values - $this->cache_type = $cache_type; - $this->dbm_filename = $dbm_filename; - - // Register destructor - register_shutdown_function(array($this, '__destruct')); - - // Check version number and clear cache if changed - if (dba_fetch(getID3::VERSION, $this->dba) != getID3::VERSION) { - $this->clear_cache(); - } - - parent::getID3(); - } - - - - // public: destructor - public function __destruct() { - - // Close dbm file - dba_close($this->dba); - - // Release exclusive lock - flock($this->lock, LOCK_UN); - - // Close lock file - fclose($this->lock); - } - - - - // public: clear cache - public function clear_cache() { - - // Close dbm file - dba_close($this->dba); - - // Create new dbm file - $this->dba = dba_open($this->dbm_filename, 'n', $this->cache_type); - - if (!$this->dba) { - throw new Exception('failed to clear cache/recreate dbm file: '.$this->dbm_filename); - } - - // Insert getID3 version number - dba_insert(getID3::VERSION, getID3::VERSION, $this->dba); - - // Re-register shutdown function - register_shutdown_function(array($this, '__destruct')); - } - - - - // public: analyze file - public function analyze($filename) { - - if (file_exists($filename)) { - - // Calc key filename::mod_time::size - should be unique - $key = $filename.'::'.filemtime($filename).'::'.filesize($filename); - - // Loopup key - $result = dba_fetch($key, $this->dba); - - // Hit - if ($result !== false) { - return unserialize($result); - } - } - - // Miss - $result = parent::analyze($filename); - - // Save result - if (file_exists($filename)) { - dba_insert($key, serialize($result), $this->dba); - } - - return $result; - } - -} diff --git a/src/Classes/Vendor/getid3/extension.cache.mysql.php b/src/Classes/Vendor/getid3/extension.cache.mysql.php deleted file mode 100755 index ef3f5045e..000000000 --- a/src/Classes/Vendor/getid3/extension.cache.mysql.php +++ /dev/null @@ -1,171 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// // -// extension.cache.mysql.php - part of getID3() // -// Please see readme.txt for more information // -// /// -///////////////////////////////////////////////////////////////// -// // -// This extension written by Allan Hansen // -// Table name mod by Carlo Capocasa // -// /// -///////////////////////////////////////////////////////////////// - - -/** -* This is a caching extension for getID3(). It works the exact same -* way as the getID3 class, but return cached information very fast -* -* Example: (see also demo.cache.mysql.php in /demo/) -* -* Normal getID3 usage (example): -* -* require_once 'getid3/getid3.php'; -* $getID3 = new getID3; -* $getID3->encoding = 'UTF-8'; -* $info1 = $getID3->analyze('file1.flac'); -* $info2 = $getID3->analyze('file2.wv'); -* -* getID3_cached usage: -* -* require_once 'getid3/getid3.php'; -* require_once 'getid3/getid3/extension.cache.mysql.php'; -* // 5th parameter (tablename) is optional, default is 'getid3_cache' -* $getID3 = new getID3_cached_mysql('localhost', 'database', 'username', 'password', 'tablename'); -* $getID3->encoding = 'UTF-8'; -* $info1 = $getID3->analyze('file1.flac'); -* $info2 = $getID3->analyze('file2.wv'); -* -* -* Supported Cache Types (this extension) -* -* SQL Databases: -* -* cache_type cache_options -* ------------------------------------------------------------------- -* mysql host, database, username, password -* -* -* DBM-Style Databases: (use extension.cache.dbm) -* -* cache_type cache_options -* ------------------------------------------------------------------- -* gdbm dbm_filename, lock_filename -* ndbm dbm_filename, lock_filename -* db2 dbm_filename, lock_filename -* db3 dbm_filename, lock_filename -* db4 dbm_filename, lock_filename (PHP5 required) -* -* PHP must have write access to both dbm_filename and lock_filename. -* -* -* Recommended Cache Types -* -* Infrequent updates, many reads any DBM -* Frequent updates mysql -*/ - - -class getID3_cached_mysql extends getID3 -{ - - // private vars - private $cursor; - private $connection; - - - // public: constructor - see top of this file for cache type and cache_options - public function getID3_cached_mysql($host, $database, $username, $password, $table='getid3_cache') { - - // Check for mysql support - if (!function_exists('mysql_pconnect')) { - throw new Exception('PHP not compiled with mysql support.'); - } - - // Connect to database - $this->connection = mysql_pconnect($host, $username, $password); - if (!$this->connection) { - throw new Exception('mysql_pconnect() failed - check permissions and spelling.'); - } - - // Select database - if (!mysql_select_db($database, $this->connection)) { - throw new Exception('Cannot use database '.$database); - } - - // Set table - $this->table = $table; - - // Create cache table if not exists - $this->create_table(); - - // Check version number and clear cache if changed - $version = ''; - if ($this->cursor = mysql_query("SELECT `value` FROM `".mysql_real_escape_string($this->table)."` WHERE (`filename` = '".mysql_real_escape_string(getID3::VERSION)."') AND (`filesize` = '-1') AND (`filetime` = '-1') AND (`analyzetime` = '-1')", $this->connection)) { - list($version) = mysql_fetch_array($this->cursor); - } - if ($version != getID3::VERSION) { - $this->clear_cache(); - } - - parent::getID3(); - } - - - - // public: clear cache - public function clear_cache() { - - $this->cursor = mysql_query("DELETE FROM `".mysql_real_escape_string($this->table)."`", $this->connection); - $this->cursor = mysql_query("INSERT INTO `".mysql_real_escape_string($this->table)."` VALUES ('".getID3::VERSION."', -1, -1, -1, '".getID3::VERSION."')", $this->connection); - } - - - - // public: analyze file - public function analyze($filename) { - - if (file_exists($filename)) { - - // Short-hands - $filetime = filemtime($filename); - $filesize = filesize($filename); - - // Lookup file - $this->cursor = mysql_query("SELECT `value` FROM `".mysql_real_escape_string($this->table)."` WHERE (`filename` = '".mysql_real_escape_string($filename)."') AND (`filesize` = '".mysql_real_escape_string($filesize)."') AND (`filetime` = '".mysql_real_escape_string($filetime)."')", $this->connection); - if (mysql_num_rows($this->cursor) > 0) { - // Hit - list($result) = mysql_fetch_array($this->cursor); - return unserialize(base64_decode($result)); - } - } - - // Miss - $analysis = parent::analyze($filename); - - // Save result - if (file_exists($filename)) { - $this->cursor = mysql_query("INSERT INTO `".mysql_real_escape_string($this->table)."` (`filename`, `filesize`, `filetime`, `analyzetime`, `value`) VALUES ('".mysql_real_escape_string($filename)."', '".mysql_real_escape_string($filesize)."', '".mysql_real_escape_string($filetime)."', '".mysql_real_escape_string(time())."', '".mysql_real_escape_string(base64_encode(serialize($analysis)))."')", $this->connection); - } - return $analysis; - } - - - - // private: (re)create sql table - private function create_table($drop=false) { - - $this->cursor = mysql_query("CREATE TABLE IF NOT EXISTS `".mysql_real_escape_string($this->table)."` ( - `filename` VARCHAR(255) NOT NULL DEFAULT '', - `filesize` INT(11) NOT NULL DEFAULT '0', - `filetime` INT(11) NOT NULL DEFAULT '0', - `analyzetime` INT(11) NOT NULL DEFAULT '0', - `value` TEXT NOT NULL, - PRIMARY KEY (`filename`,`filesize`,`filetime`)) ENGINE=MyISAM", $this->connection); - echo mysql_error($this->connection); - } -} diff --git a/src/Classes/Vendor/getid3/extension.cache.sqlite3.php b/src/Classes/Vendor/getid3/extension.cache.sqlite3.php deleted file mode 100755 index 3e0fd4dbb..000000000 --- a/src/Classes/Vendor/getid3/extension.cache.sqlite3.php +++ /dev/null @@ -1,264 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org /// -///////////////////////////////////////////////////////////////////////////////// -/// // -// extension.cache.sqlite3.php - part of getID3() // -// Please see readme.txt for more information // -// /// -///////////////////////////////////////////////////////////////////////////////// -/// // -// MySQL extension written by Allan Hansen // -// Table name mod by Carlo Capocasa // -// MySQL extension was reworked for SQLite3 by Karl G. Holz // -// /// -///////////////////////////////////////////////////////////////////////////////// -/** -* This is a caching extension for getID3(). It works the exact same -* way as the getID3 class, but return cached information much faster -* -* Normal getID3 usage (example): -* -* require_once 'getid3/getid3.php'; -* $getID3 = new getID3; -* $getID3->encoding = 'UTF-8'; -* $info1 = $getID3->analyze('file1.flac'); -* $info2 = $getID3->analyze('file2.wv'); -* -* getID3_cached usage: -* -* require_once 'getid3/getid3.php'; -* require_once 'getid3/extension.cache.sqlite3.php'; -* // all parameters are optional, defaults are: -* $getID3 = new getID3_cached_sqlite3($table='getid3_cache', $hide=FALSE); -* $getID3->encoding = 'UTF-8'; -* $info1 = $getID3->analyze('file1.flac'); -* $info2 = $getID3->analyze('file2.wv'); -* -* -* Supported Cache Types (this extension) -* -* SQL Databases: -* -* cache_type cache_options -* ------------------------------------------------------------------- -* mysql host, database, username, password -* -* sqlite3 table='getid3_cache', hide=false (PHP5) -* - -*** database file will be stored in the same directory as this script, -*** webserver must have write access to that directory! -*** set $hide to TRUE to prefix db file with .ht to pervent access from web client -*** this is a default setting in the Apache configuration: - -# The following lines prevent .htaccess and .htpasswd files from being viewed by Web clients. - - - Order allow,deny - Deny from all - Satisfy all - - -******************************************************************************** -* -* ------------------------------------------------------------------- -* DBM-Style Databases: (use extension.cache.dbm) -* -* cache_type cache_options -* ------------------------------------------------------------------- -* gdbm dbm_filename, lock_filename -* ndbm dbm_filename, lock_filename -* db2 dbm_filename, lock_filename -* db3 dbm_filename, lock_filename -* db4 dbm_filename, lock_filename (PHP5 required) -* -* PHP must have write access to both dbm_filename and lock_filename. -* -* Recommended Cache Types -* -* Infrequent updates, many reads any DBM -* Frequent updates mysql -******************************************************************************** -* -* IMHO this is still a bit slow, I'm using this with MP4/MOV/ M4v files -* there is a plan to add directory scanning and analyzing to make things work much faster -* -* -*/ -class getID3_cached_sqlite3 extends getID3 { - - /** - * __construct() - * @param string $table holds name of sqlite table - * @return type - */ - public function __construct($table='getid3_cache', $hide=false) { - $this->table = $table; // Set table - $file = dirname(__FILE__).'/'.basename(__FILE__, 'php').'sqlite'; - if ($hide) { - $file = dirname(__FILE__).'/.ht.'.basename(__FILE__, 'php').'sqlite'; - } - $this->db = new SQLite3($file); - $db = $this->db; - $this->create_table(); // Create cache table if not exists - $version = ''; - $sql = $this->version_check; - $stmt = $db->prepare($sql); - $stmt->bindValue(':filename', getID3::VERSION, SQLITE3_TEXT); - $result = $stmt->execute(); - list($version) = $result->fetchArray(); - if ($version != getID3::VERSION) { // Check version number and clear cache if changed - $this->clear_cache(); - } - return parent::__construct(); - } - - /** - * close the database connection - */ - public function __destruct() { - $db=$this->db; - $db->close(); - } - - /** - * hold the sqlite db - * @var SQLite Resource - */ - private $db; - - /** - * table to use for caching - * @var string $table - */ - private $table; - - /** - * clear the cache - * @access private - * @return type - */ - private function clear_cache() { - $db = $this->db; - $sql = $this->delete_cache; - $db->exec($sql); - $sql = $this->set_version; - $stmt = $db->prepare($sql); - $stmt->bindValue(':filename', getID3::VERSION, SQLITE3_TEXT); - $stmt->bindValue(':dirname', getID3::VERSION, SQLITE3_TEXT); - $stmt->bindValue(':val', getID3::VERSION, SQLITE3_TEXT); - return $stmt->execute(); - } - - /** - * analyze file and cache them, if cached pull from the db - * @param type $filename - * @return boolean - */ - public function analyze($filename) { - if (!file_exists($filename)) { - return false; - } - // items to track for caching - $filetime = filemtime($filename); - $filesize = filesize($filename); - // this will be saved for a quick directory lookup of analized files - // ... why do 50 seperate sql quries when you can do 1 for the same result - $dirname = dirname($filename); - // Lookup file - $db = $this->db; - $sql = $this->get_id3_data; - $stmt = $db->prepare($sql); - $stmt->bindValue(':filename', $filename, SQLITE3_TEXT); - $stmt->bindValue(':filesize', $filesize, SQLITE3_INTEGER); - $stmt->bindValue(':filetime', $filetime, SQLITE3_INTEGER); - $res = $stmt->execute(); - list($result) = $res->fetchArray(); - if (count($result) > 0 ) { - return unserialize(base64_decode($result)); - } - // if it hasn't been analyzed before, then do it now - $analysis = parent::analyze($filename); - // Save result - $sql = $this->cache_file; - $stmt = $db->prepare($sql); - $stmt->bindValue(':filename', $filename, SQLITE3_TEXT); - $stmt->bindValue(':dirname', $dirname, SQLITE3_TEXT); - $stmt->bindValue(':filesize', $filesize, SQLITE3_INTEGER); - $stmt->bindValue(':filetime', $filetime, SQLITE3_INTEGER); - $stmt->bindValue(':atime', time(), SQLITE3_INTEGER); - $stmt->bindValue(':val', base64_encode(serialize($analysis)), SQLITE3_TEXT); - $res = $stmt->execute(); - return $analysis; - } - - /** - * create data base table - * this is almost the same as MySQL, with the exception of the dirname being added - * @return type - */ - private function create_table() { - $db = $this->db; - $sql = $this->make_table; - return $db->exec($sql); - } - - /** - * get cached directory - * - * This function is not in the MySQL extention, it's ment to speed up requesting multiple files - * which is ideal for podcasting, playlists, etc. - * - * @access public - * @param string $dir directory to search the cache database for - * @return array return an array of matching id3 data - */ - public function get_cached_dir($dir) { - $db = $this->db; - $rows = array(); - $sql = $this->get_cached_dir; - $stmt = $db->prepare($sql); - $stmt->bindValue(':dirname', $dir, SQLITE3_TEXT); - $res = $stmt->execute(); - while ($row=$res->fetchArray()) { - $rows[] = unserialize(base64_decode($row)); - } - return $rows; - } - - /** - * use the magical __get() for sql queries - * - * access as easy as $this->{case name}, returns NULL if query is not found - */ - public function __get($name) { - switch($name) { - case 'version_check': - return "SELECT val FROM $this->table WHERE filename = :filename AND filesize = '-1' AND filetime = '-1' AND analyzetime = '-1'"; - break; - case 'delete_cache': - return "DELETE FROM $this->table"; - break; - case 'set_version': - return "INSERT INTO $this->table (filename, dirname, filesize, filetime, analyzetime, val) VALUES (:filename, :dirname, -1, -1, -1, :val)"; - break; - case 'get_id3_data': - return "SELECT val FROM $this->table WHERE filename = :filename AND filesize = :filesize AND filetime = :filetime"; - break; - case 'cache_file': - return "INSERT INTO $this->table (filename, dirname, filesize, filetime, analyzetime, val) VALUES (:filename, :dirname, :filesize, :filetime, :atime, :val)"; - break; - case 'make_table': - return "CREATE TABLE IF NOT EXISTS $this->table (filename VARCHAR(255) NOT NULL DEFAULT '', dirname VARCHAR(255) NOT NULL DEFAULT '', filesize INT(11) NOT NULL DEFAULT '0', filetime INT(11) NOT NULL DEFAULT '0', analyzetime INT(11) NOT NULL DEFAULT '0', val text not null, PRIMARY KEY (filename, filesize, filetime))"; - break; - case 'get_cached_dir': - return "SELECT val FROM $this->table WHERE dirname = :dirname"; - break; - } - return null; - } - -} diff --git a/src/Classes/Vendor/getid3/getid3.lib.php b/src/Classes/Vendor/getid3/getid3.lib.php deleted file mode 100755 index 08c3067da..000000000 --- a/src/Classes/Vendor/getid3/getid3.lib.php +++ /dev/null @@ -1,1342 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// // -// getid3.lib.php - part of getID3() // -// See readme.txt for more details // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_lib -{ - - public static function PrintHexBytes($string, $hex=true, $spaces=true, $htmlencoding='UTF-8') { - $returnstring = ''; - for ($i = 0; $i < strlen($string); $i++) { - if ($hex) { - $returnstring .= str_pad(dechex(ord($string{$i})), 2, '0', STR_PAD_LEFT); - } else { - $returnstring .= ' '.(preg_match("#[\x20-\x7E]#", $string{$i}) ? $string{$i} : '¤'); - } - if ($spaces) { - $returnstring .= ' '; - } - } - if (!empty($htmlencoding)) { - if ($htmlencoding === true) { - $htmlencoding = 'UTF-8'; // prior to getID3 v1.9.0 the function's 4th parameter was boolean - } - $returnstring = htmlentities($returnstring, ENT_QUOTES, $htmlencoding); - } - return $returnstring; - } - - public static function trunc($floatnumber) { - // truncates a floating-point number at the decimal point - // returns int (if possible, otherwise float) - if ($floatnumber >= 1) { - $truncatednumber = floor($floatnumber); - } elseif ($floatnumber <= -1) { - $truncatednumber = ceil($floatnumber); - } else { - $truncatednumber = 0; - } - if (self::intValueSupported($truncatednumber)) { - $truncatednumber = (int) $truncatednumber; - } - return $truncatednumber; - } - - - public static function safe_inc(&$variable, $increment=1) { - if (isset($variable)) { - $variable += $increment; - } else { - $variable = $increment; - } - return true; - } - - public static function CastAsInt($floatnum) { - // convert to float if not already - $floatnum = (float) $floatnum; - - // convert a float to type int, only if possible - if (self::trunc($floatnum) == $floatnum) { - // it's not floating point - if (self::intValueSupported($floatnum)) { - // it's within int range - $floatnum = (int) $floatnum; - } - } - return $floatnum; - } - - public static function intValueSupported($num) { - // check if integers are 64-bit - static $hasINT64 = null; - if ($hasINT64 === null) { // 10x faster than is_null() - $hasINT64 = is_int(pow(2, 31)); // 32-bit int are limited to (2^31)-1 - if (!$hasINT64 && !defined('PHP_INT_MIN')) { - define('PHP_INT_MIN', ~PHP_INT_MAX); - } - } - // if integers are 64-bit - no other check required - if ($hasINT64 || (($num <= PHP_INT_MAX) && ($num >= PHP_INT_MIN))) { - return true; - } - return false; - } - - public static function DecimalizeFraction($fraction) { - list($numerator, $denominator) = explode('/', $fraction); - return $numerator / ($denominator ? $denominator : 1); - } - - - public static function DecimalBinary2Float($binarynumerator) { - $numerator = self::Bin2Dec($binarynumerator); - $denominator = self::Bin2Dec('1'.str_repeat('0', strlen($binarynumerator))); - return ($numerator / $denominator); - } - - - public static function NormalizeBinaryPoint($binarypointnumber, $maxbits=52) { - // http://www.scri.fsu.edu/~jac/MAD3401/Backgrnd/binary.html - if (strpos($binarypointnumber, '.') === false) { - $binarypointnumber = '0.'.$binarypointnumber; - } elseif ($binarypointnumber{0} == '.') { - $binarypointnumber = '0'.$binarypointnumber; - } - $exponent = 0; - while (($binarypointnumber{0} != '1') || (substr($binarypointnumber, 1, 1) != '.')) { - if (substr($binarypointnumber, 1, 1) == '.') { - $exponent--; - $binarypointnumber = substr($binarypointnumber, 2, 1).'.'.substr($binarypointnumber, 3); - } else { - $pointpos = strpos($binarypointnumber, '.'); - $exponent += ($pointpos - 1); - $binarypointnumber = str_replace('.', '', $binarypointnumber); - $binarypointnumber = $binarypointnumber{0}.'.'.substr($binarypointnumber, 1); - } - } - $binarypointnumber = str_pad(substr($binarypointnumber, 0, $maxbits + 2), $maxbits + 2, '0', STR_PAD_RIGHT); - return array('normalized'=>$binarypointnumber, 'exponent'=>(int) $exponent); - } - - - public static function Float2BinaryDecimal($floatvalue) { - // http://www.scri.fsu.edu/~jac/MAD3401/Backgrnd/binary.html - $maxbits = 128; // to how many bits of precision should the calculations be taken? - $intpart = self::trunc($floatvalue); - $floatpart = abs($floatvalue - $intpart); - $pointbitstring = ''; - while (($floatpart != 0) && (strlen($pointbitstring) < $maxbits)) { - $floatpart *= 2; - $pointbitstring .= (string) self::trunc($floatpart); - $floatpart -= self::trunc($floatpart); - } - $binarypointnumber = decbin($intpart).'.'.$pointbitstring; - return $binarypointnumber; - } - - - public static function Float2String($floatvalue, $bits) { - // http://www.scri.fsu.edu/~jac/MAD3401/Backgrnd/ieee-expl.html - switch ($bits) { - case 32: - $exponentbits = 8; - $fractionbits = 23; - break; - - case 64: - $exponentbits = 11; - $fractionbits = 52; - break; - - default: - return false; - break; - } - if ($floatvalue >= 0) { - $signbit = '0'; - } else { - $signbit = '1'; - } - $normalizedbinary = self::NormalizeBinaryPoint(self::Float2BinaryDecimal($floatvalue), $fractionbits); - $biasedexponent = pow(2, $exponentbits - 1) - 1 + $normalizedbinary['exponent']; // (127 or 1023) +/- exponent - $exponentbitstring = str_pad(decbin($biasedexponent), $exponentbits, '0', STR_PAD_LEFT); - $fractionbitstring = str_pad(substr($normalizedbinary['normalized'], 2), $fractionbits, '0', STR_PAD_RIGHT); - - return self::BigEndian2String(self::Bin2Dec($signbit.$exponentbitstring.$fractionbitstring), $bits % 8, false); - } - - - public static function LittleEndian2Float($byteword) { - return self::BigEndian2Float(strrev($byteword)); - } - - - public static function BigEndian2Float($byteword) { - // ANSI/IEEE Standard 754-1985, Standard for Binary Floating Point Arithmetic - // http://www.psc.edu/general/software/packages/ieee/ieee.html - // http://www.scri.fsu.edu/~jac/MAD3401/Backgrnd/ieee.html - - $bitword = self::BigEndian2Bin($byteword); - if (!$bitword) { - return 0; - } - $signbit = $bitword{0}; - - switch (strlen($byteword) * 8) { - case 32: - $exponentbits = 8; - $fractionbits = 23; - break; - - case 64: - $exponentbits = 11; - $fractionbits = 52; - break; - - case 80: - // 80-bit Apple SANE format - // http://www.mactech.com/articles/mactech/Vol.06/06.01/SANENormalized/ - $exponentstring = substr($bitword, 1, 15); - $isnormalized = intval($bitword{16}); - $fractionstring = substr($bitword, 17, 63); - $exponent = pow(2, self::Bin2Dec($exponentstring) - 16383); - $fraction = $isnormalized + self::DecimalBinary2Float($fractionstring); - $floatvalue = $exponent * $fraction; - if ($signbit == '1') { - $floatvalue *= -1; - } - return $floatvalue; - break; - - default: - return false; - break; - } - $exponentstring = substr($bitword, 1, $exponentbits); - $fractionstring = substr($bitword, $exponentbits + 1, $fractionbits); - $exponent = self::Bin2Dec($exponentstring); - $fraction = self::Bin2Dec($fractionstring); - - if (($exponent == (pow(2, $exponentbits) - 1)) && ($fraction != 0)) { - // Not a Number - $floatvalue = false; - } elseif (($exponent == (pow(2, $exponentbits) - 1)) && ($fraction == 0)) { - if ($signbit == '1') { - $floatvalue = '-infinity'; - } else { - $floatvalue = '+infinity'; - } - } elseif (($exponent == 0) && ($fraction == 0)) { - if ($signbit == '1') { - $floatvalue = -0; - } else { - $floatvalue = 0; - } - $floatvalue = ($signbit ? 0 : -0); - } elseif (($exponent == 0) && ($fraction != 0)) { - // These are 'unnormalized' values - $floatvalue = pow(2, (-1 * (pow(2, $exponentbits - 1) - 2))) * self::DecimalBinary2Float($fractionstring); - if ($signbit == '1') { - $floatvalue *= -1; - } - } elseif ($exponent != 0) { - $floatvalue = pow(2, ($exponent - (pow(2, $exponentbits - 1) - 1))) * (1 + self::DecimalBinary2Float($fractionstring)); - if ($signbit == '1') { - $floatvalue *= -1; - } - } - return (float) $floatvalue; - } - - - public static function BigEndian2Int($byteword, $synchsafe=false, $signed=false) { - $intvalue = 0; - $bytewordlen = strlen($byteword); - if ($bytewordlen == 0) { - return false; - } - for ($i = 0; $i < $bytewordlen; $i++) { - if ($synchsafe) { // disregard MSB, effectively 7-bit bytes - //$intvalue = $intvalue | (ord($byteword{$i}) & 0x7F) << (($bytewordlen - 1 - $i) * 7); // faster, but runs into problems past 2^31 on 32-bit systems - $intvalue += (ord($byteword{$i}) & 0x7F) * pow(2, ($bytewordlen - 1 - $i) * 7); - } else { - $intvalue += ord($byteword{$i}) * pow(256, ($bytewordlen - 1 - $i)); - } - } - if ($signed && !$synchsafe) { - // synchsafe ints are not allowed to be signed - if ($bytewordlen <= PHP_INT_SIZE) { - $signMaskBit = 0x80 << (8 * ($bytewordlen - 1)); - if ($intvalue & $signMaskBit) { - $intvalue = 0 - ($intvalue & ($signMaskBit - 1)); - } - } else { - throw new Exception('ERROR: Cannot have signed integers larger than '.(8 * PHP_INT_SIZE).'-bits ('.strlen($byteword).') in self::BigEndian2Int()'); - break; - } - } - return self::CastAsInt($intvalue); - } - - - public static function LittleEndian2Int($byteword, $signed=false) { - return self::BigEndian2Int(strrev($byteword), false, $signed); - } - - - public static function BigEndian2Bin($byteword) { - $binvalue = ''; - $bytewordlen = strlen($byteword); - for ($i = 0; $i < $bytewordlen; $i++) { - $binvalue .= str_pad(decbin(ord($byteword{$i})), 8, '0', STR_PAD_LEFT); - } - return $binvalue; - } - - - public static function BigEndian2String($number, $minbytes=1, $synchsafe=false, $signed=false) { - if ($number < 0) { - throw new Exception('ERROR: self::BigEndian2String() does not support negative numbers'); - } - $maskbyte = (($synchsafe || $signed) ? 0x7F : 0xFF); - $intstring = ''; - if ($signed) { - if ($minbytes > PHP_INT_SIZE) { - throw new Exception('ERROR: Cannot have signed integers larger than '.(8 * PHP_INT_SIZE).'-bits in self::BigEndian2String()'); - } - $number = $number & (0x80 << (8 * ($minbytes - 1))); - } - while ($number != 0) { - $quotient = ($number / ($maskbyte + 1)); - $intstring = chr(ceil(($quotient - floor($quotient)) * $maskbyte)).$intstring; - $number = floor($quotient); - } - return str_pad($intstring, $minbytes, "\x00", STR_PAD_LEFT); - } - - - public static function Dec2Bin($number) { - while ($number >= 256) { - $bytes[] = (($number / 256) - (floor($number / 256))) * 256; - $number = floor($number / 256); - } - $bytes[] = $number; - $binstring = ''; - for ($i = 0; $i < count($bytes); $i++) { - $binstring = (($i == count($bytes) - 1) ? decbin($bytes[$i]) : str_pad(decbin($bytes[$i]), 8, '0', STR_PAD_LEFT)).$binstring; - } - return $binstring; - } - - - public static function Bin2Dec($binstring, $signed=false) { - $signmult = 1; - if ($signed) { - if ($binstring{0} == '1') { - $signmult = -1; - } - $binstring = substr($binstring, 1); - } - $decvalue = 0; - for ($i = 0; $i < strlen($binstring); $i++) { - $decvalue += ((int) substr($binstring, strlen($binstring) - $i - 1, 1)) * pow(2, $i); - } - return self::CastAsInt($decvalue * $signmult); - } - - - public static function Bin2String($binstring) { - // return 'hi' for input of '0110100001101001' - $string = ''; - $binstringreversed = strrev($binstring); - for ($i = 0; $i < strlen($binstringreversed); $i += 8) { - $string = chr(self::Bin2Dec(strrev(substr($binstringreversed, $i, 8)))).$string; - } - return $string; - } - - - public static function LittleEndian2String($number, $minbytes=1, $synchsafe=false) { - $intstring = ''; - while ($number > 0) { - if ($synchsafe) { - $intstring = $intstring.chr($number & 127); - $number >>= 7; - } else { - $intstring = $intstring.chr($number & 255); - $number >>= 8; - } - } - return str_pad($intstring, $minbytes, "\x00", STR_PAD_RIGHT); - } - - - public static function array_merge_clobber($array1, $array2) { - // written by kcØhireability*com - // taken from http://www.php.net/manual/en/function.array-merge-recursive.php - if (!is_array($array1) || !is_array($array2)) { - return false; - } - $newarray = $array1; - foreach ($array2 as $key => $val) { - if (is_array($val) && isset($newarray[$key]) && is_array($newarray[$key])) { - $newarray[$key] = self::array_merge_clobber($newarray[$key], $val); - } else { - $newarray[$key] = $val; - } - } - return $newarray; - } - - - public static function array_merge_noclobber($array1, $array2) { - if (!is_array($array1) || !is_array($array2)) { - return false; - } - $newarray = $array1; - foreach ($array2 as $key => $val) { - if (is_array($val) && isset($newarray[$key]) && is_array($newarray[$key])) { - $newarray[$key] = self::array_merge_noclobber($newarray[$key], $val); - } elseif (!isset($newarray[$key])) { - $newarray[$key] = $val; - } - } - return $newarray; - } - - - public static function ksort_recursive(&$theArray) { - ksort($theArray); - foreach ($theArray as $key => $value) { - if (is_array($value)) { - self::ksort_recursive($theArray[$key]); - } - } - return true; - } - - public static function fileextension($filename, $numextensions=1) { - if (strstr($filename, '.')) { - $reversedfilename = strrev($filename); - $offset = 0; - for ($i = 0; $i < $numextensions; $i++) { - $offset = strpos($reversedfilename, '.', $offset + 1); - if ($offset === false) { - return ''; - } - } - return strrev(substr($reversedfilename, 0, $offset)); - } - return ''; - } - - - public static function PlaytimeString($seconds) { - $sign = (($seconds < 0) ? '-' : ''); - $seconds = round(abs($seconds)); - $H = (int) floor( $seconds / 3600); - $M = (int) floor(($seconds - (3600 * $H) ) / 60); - $S = (int) round( $seconds - (3600 * $H) - (60 * $M) ); - return $sign.($H ? $H.':' : '').($H ? str_pad($M, 2, '0', STR_PAD_LEFT) : intval($M)).':'.str_pad($S, 2, 0, STR_PAD_LEFT); - } - - - public static function DateMac2Unix($macdate) { - // Macintosh timestamp: seconds since 00:00h January 1, 1904 - // UNIX timestamp: seconds since 00:00h January 1, 1970 - return self::CastAsInt($macdate - 2082844800); - } - - - public static function FixedPoint8_8($rawdata) { - return self::BigEndian2Int(substr($rawdata, 0, 1)) + (float) (self::BigEndian2Int(substr($rawdata, 1, 1)) / pow(2, 8)); - } - - - public static function FixedPoint16_16($rawdata) { - return self::BigEndian2Int(substr($rawdata, 0, 2)) + (float) (self::BigEndian2Int(substr($rawdata, 2, 2)) / pow(2, 16)); - } - - - public static function FixedPoint2_30($rawdata) { - $binarystring = self::BigEndian2Bin($rawdata); - return self::Bin2Dec(substr($binarystring, 0, 2)) + (float) (self::Bin2Dec(substr($binarystring, 2, 30)) / pow(2, 30)); - } - - - public static function CreateDeepArray($ArrayPath, $Separator, $Value) { - // assigns $Value to a nested array path: - // $foo = self::CreateDeepArray('/path/to/my', '/', 'file.txt') - // is the same as: - // $foo = array('path'=>array('to'=>'array('my'=>array('file.txt')))); - // or - // $foo['path']['to']['my'] = 'file.txt'; - $ArrayPath = ltrim($ArrayPath, $Separator); - if (($pos = strpos($ArrayPath, $Separator)) !== false) { - $ReturnedArray[substr($ArrayPath, 0, $pos)] = self::CreateDeepArray(substr($ArrayPath, $pos + 1), $Separator, $Value); - } else { - $ReturnedArray[$ArrayPath] = $Value; - } - return $ReturnedArray; - } - - public static function array_max($arraydata, $returnkey=false) { - $maxvalue = false; - $maxkey = false; - foreach ($arraydata as $key => $value) { - if (!is_array($value)) { - if ($value > $maxvalue) { - $maxvalue = $value; - $maxkey = $key; - } - } - } - return ($returnkey ? $maxkey : $maxvalue); - } - - public static function array_min($arraydata, $returnkey=false) { - $minvalue = false; - $minkey = false; - foreach ($arraydata as $key => $value) { - if (!is_array($value)) { - if ($value > $minvalue) { - $minvalue = $value; - $minkey = $key; - } - } - } - return ($returnkey ? $minkey : $minvalue); - } - - public static function XML2array($XMLstring) { - if (function_exists('simplexml_load_string')) { - if (function_exists('get_object_vars')) { - $XMLobject = simplexml_load_string($XMLstring); - return self::SimpleXMLelement2array($XMLobject); - } - } - return false; - } - - public static function SimpleXMLelement2array($XMLobject) { - if (!is_object($XMLobject) && !is_array($XMLobject)) { - return $XMLobject; - } - $XMLarray = (is_object($XMLobject) ? get_object_vars($XMLobject) : $XMLobject); - foreach ($XMLarray as $key => $value) { - $XMLarray[$key] = self::SimpleXMLelement2array($value); - } - return $XMLarray; - } - - - // Allan Hansen - // self::md5_data() - returns md5sum for a file from startuing position to absolute end position - public static function hash_data($file, $offset, $end, $algorithm) { - static $tempdir = ''; - if (!self::intValueSupported($end)) { - return false; - } - switch ($algorithm) { - case 'md5': - $hash_function = 'md5_file'; - $unix_call = 'md5sum'; - $windows_call = 'md5sum.exe'; - $hash_length = 32; - break; - - case 'sha1': - $hash_function = 'sha1_file'; - $unix_call = 'sha1sum'; - $windows_call = 'sha1sum.exe'; - $hash_length = 40; - break; - - default: - throw new Exception('Invalid algorithm ('.$algorithm.') in self::hash_data()'); - break; - } - $size = $end - $offset; - while (true) { - if (GETID3_OS_ISWINDOWS) { - - // It seems that sha1sum.exe for Windows only works on physical files, does not accept piped data - // Fall back to create-temp-file method: - if ($algorithm == 'sha1') { - break; - } - - $RequiredFiles = array('cygwin1.dll', 'head.exe', 'tail.exe', $windows_call); - foreach ($RequiredFiles as $required_file) { - if (!is_readable(GETID3_HELPERAPPSDIR.$required_file)) { - // helper apps not available - fall back to old method - break 2; - } - } - $commandline = GETID3_HELPERAPPSDIR.'head.exe -c '.$end.' '.escapeshellarg(str_replace('/', DIRECTORY_SEPARATOR, $file)).' | '; - $commandline .= GETID3_HELPERAPPSDIR.'tail.exe -c '.$size.' | '; - $commandline .= GETID3_HELPERAPPSDIR.$windows_call; - - } else { - - $commandline = 'head -c'.$end.' '.escapeshellarg($file).' | '; - $commandline .= 'tail -c'.$size.' | '; - $commandline .= $unix_call; - - } - if (preg_match('#(1|ON)#i', ini_get('safe_mode'))) { - //throw new Exception('PHP running in Safe Mode - backtick operator not available, using slower non-system-call '.$algorithm.' algorithm'); - break; - } - return substr(`$commandline`, 0, $hash_length); - } - - if (empty($tempdir)) { - // yes this is ugly, feel free to suggest a better way - require_once(dirname(__FILE__).'/getid3.php'); - $getid3_temp = new getID3(); - $tempdir = $getid3_temp->tempdir; - unset($getid3_temp); - } - // try to create a temporary file in the system temp directory - invalid dirname should force to system temp dir - if (($data_filename = tempnam($tempdir, 'gI3')) === false) { - // can't find anywhere to create a temp file, just fail - return false; - } - - // Init - $result = false; - - // copy parts of file - try { - self::CopyFileParts($file, $data_filename, $offset, $end - $offset); - $result = $hash_function($data_filename); - } catch (Exception $e) { - throw new Exception('self::CopyFileParts() failed in getid_lib::hash_data(): '.$e->getMessage()); - } - unlink($data_filename); - return $result; - } - - public static function CopyFileParts($filename_source, $filename_dest, $offset, $length) { - if (!self::intValueSupported($offset + $length)) { - throw new Exception('cannot copy file portion, it extends beyond the '.round(PHP_INT_MAX / 1073741824).'GB limit'); - } - if (is_readable($filename_source) && is_file($filename_source) && ($fp_src = fopen($filename_source, 'rb'))) { - if (($fp_dest = fopen($filename_dest, 'wb'))) { - if (fseek($fp_src, $offset, SEEK_SET) == 0) { - $byteslefttowrite = $length; - while (($byteslefttowrite > 0) && ($buffer = fread($fp_src, min($byteslefttowrite, getID3::FREAD_BUFFER_SIZE)))) { - $byteswritten = fwrite($fp_dest, $buffer, $byteslefttowrite); - $byteslefttowrite -= $byteswritten; - } - return true; - } else { - throw new Exception('failed to seek to offset '.$offset.' in '.$filename_source); - } - fclose($fp_dest); - } else { - throw new Exception('failed to create file for writing '.$filename_dest); - } - fclose($fp_src); - } else { - throw new Exception('failed to open file for reading '.$filename_source); - } - return false; - } - - public static function iconv_fallback_int_utf8($charval) { - if ($charval < 128) { - // 0bbbbbbb - $newcharstring = chr($charval); - } elseif ($charval < 2048) { - // 110bbbbb 10bbbbbb - $newcharstring = chr(($charval >> 6) | 0xC0); - $newcharstring .= chr(($charval & 0x3F) | 0x80); - } elseif ($charval < 65536) { - // 1110bbbb 10bbbbbb 10bbbbbb - $newcharstring = chr(($charval >> 12) | 0xE0); - $newcharstring .= chr(($charval >> 6) | 0xC0); - $newcharstring .= chr(($charval & 0x3F) | 0x80); - } else { - // 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb - $newcharstring = chr(($charval >> 18) | 0xF0); - $newcharstring .= chr(($charval >> 12) | 0xC0); - $newcharstring .= chr(($charval >> 6) | 0xC0); - $newcharstring .= chr(($charval & 0x3F) | 0x80); - } - return $newcharstring; - } - - // ISO-8859-1 => UTF-8 - public static function iconv_fallback_iso88591_utf8($string, $bom=false) { - if (function_exists('utf8_encode')) { - return utf8_encode($string); - } - // utf8_encode() unavailable, use getID3()'s iconv_fallback() conversions (possibly PHP is compiled without XML support) - $newcharstring = ''; - if ($bom) { - $newcharstring .= "\xEF\xBB\xBF"; - } - for ($i = 0; $i < strlen($string); $i++) { - $charval = ord($string{$i}); - $newcharstring .= self::iconv_fallback_int_utf8($charval); - } - return $newcharstring; - } - - // ISO-8859-1 => UTF-16BE - public static function iconv_fallback_iso88591_utf16be($string, $bom=false) { - $newcharstring = ''; - if ($bom) { - $newcharstring .= "\xFE\xFF"; - } - for ($i = 0; $i < strlen($string); $i++) { - $newcharstring .= "\x00".$string{$i}; - } - return $newcharstring; - } - - // ISO-8859-1 => UTF-16LE - public static function iconv_fallback_iso88591_utf16le($string, $bom=false) { - $newcharstring = ''; - if ($bom) { - $newcharstring .= "\xFF\xFE"; - } - for ($i = 0; $i < strlen($string); $i++) { - $newcharstring .= $string{$i}."\x00"; - } - return $newcharstring; - } - - // ISO-8859-1 => UTF-16LE (BOM) - public static function iconv_fallback_iso88591_utf16($string) { - return self::iconv_fallback_iso88591_utf16le($string, true); - } - - // UTF-8 => ISO-8859-1 - public static function iconv_fallback_utf8_iso88591($string) { - if (function_exists('utf8_decode')) { - return utf8_decode($string); - } - // utf8_decode() unavailable, use getID3()'s iconv_fallback() conversions (possibly PHP is compiled without XML support) - $newcharstring = ''; - $offset = 0; - $stringlength = strlen($string); - while ($offset < $stringlength) { - if ((ord($string{$offset}) | 0x07) == 0xF7) { - // 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x07) << 18) & - ((ord($string{($offset + 1)}) & 0x3F) << 12) & - ((ord($string{($offset + 2)}) & 0x3F) << 6) & - (ord($string{($offset + 3)}) & 0x3F); - $offset += 4; - } elseif ((ord($string{$offset}) | 0x0F) == 0xEF) { - // 1110bbbb 10bbbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x0F) << 12) & - ((ord($string{($offset + 1)}) & 0x3F) << 6) & - (ord($string{($offset + 2)}) & 0x3F); - $offset += 3; - } elseif ((ord($string{$offset}) | 0x1F) == 0xDF) { - // 110bbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x1F) << 6) & - (ord($string{($offset + 1)}) & 0x3F); - $offset += 2; - } elseif ((ord($string{$offset}) | 0x7F) == 0x7F) { - // 0bbbbbbb - $charval = ord($string{$offset}); - $offset += 1; - } else { - // error? throw some kind of warning here? - $charval = false; - $offset += 1; - } - if ($charval !== false) { - $newcharstring .= (($charval < 256) ? chr($charval) : '?'); - } - } - return $newcharstring; - } - - // UTF-8 => UTF-16BE - public static function iconv_fallback_utf8_utf16be($string, $bom=false) { - $newcharstring = ''; - if ($bom) { - $newcharstring .= "\xFE\xFF"; - } - $offset = 0; - $stringlength = strlen($string); - while ($offset < $stringlength) { - if ((ord($string{$offset}) | 0x07) == 0xF7) { - // 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x07) << 18) & - ((ord($string{($offset + 1)}) & 0x3F) << 12) & - ((ord($string{($offset + 2)}) & 0x3F) << 6) & - (ord($string{($offset + 3)}) & 0x3F); - $offset += 4; - } elseif ((ord($string{$offset}) | 0x0F) == 0xEF) { - // 1110bbbb 10bbbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x0F) << 12) & - ((ord($string{($offset + 1)}) & 0x3F) << 6) & - (ord($string{($offset + 2)}) & 0x3F); - $offset += 3; - } elseif ((ord($string{$offset}) | 0x1F) == 0xDF) { - // 110bbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x1F) << 6) & - (ord($string{($offset + 1)}) & 0x3F); - $offset += 2; - } elseif ((ord($string{$offset}) | 0x7F) == 0x7F) { - // 0bbbbbbb - $charval = ord($string{$offset}); - $offset += 1; - } else { - // error? throw some kind of warning here? - $charval = false; - $offset += 1; - } - if ($charval !== false) { - $newcharstring .= (($charval < 65536) ? self::BigEndian2String($charval, 2) : "\x00".'?'); - } - } - return $newcharstring; - } - - // UTF-8 => UTF-16LE - public static function iconv_fallback_utf8_utf16le($string, $bom=false) { - $newcharstring = ''; - if ($bom) { - $newcharstring .= "\xFF\xFE"; - } - $offset = 0; - $stringlength = strlen($string); - while ($offset < $stringlength) { - if ((ord($string{$offset}) | 0x07) == 0xF7) { - // 11110bbb 10bbbbbb 10bbbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x07) << 18) & - ((ord($string{($offset + 1)}) & 0x3F) << 12) & - ((ord($string{($offset + 2)}) & 0x3F) << 6) & - (ord($string{($offset + 3)}) & 0x3F); - $offset += 4; - } elseif ((ord($string{$offset}) | 0x0F) == 0xEF) { - // 1110bbbb 10bbbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x0F) << 12) & - ((ord($string{($offset + 1)}) & 0x3F) << 6) & - (ord($string{($offset + 2)}) & 0x3F); - $offset += 3; - } elseif ((ord($string{$offset}) | 0x1F) == 0xDF) { - // 110bbbbb 10bbbbbb - $charval = ((ord($string{($offset + 0)}) & 0x1F) << 6) & - (ord($string{($offset + 1)}) & 0x3F); - $offset += 2; - } elseif ((ord($string{$offset}) | 0x7F) == 0x7F) { - // 0bbbbbbb - $charval = ord($string{$offset}); - $offset += 1; - } else { - // error? maybe throw some warning here? - $charval = false; - $offset += 1; - } - if ($charval !== false) { - $newcharstring .= (($charval < 65536) ? self::LittleEndian2String($charval, 2) : '?'."\x00"); - } - } - return $newcharstring; - } - - // UTF-8 => UTF-16LE (BOM) - public static function iconv_fallback_utf8_utf16($string) { - return self::iconv_fallback_utf8_utf16le($string, true); - } - - // UTF-16BE => UTF-8 - public static function iconv_fallback_utf16be_utf8($string) { - if (substr($string, 0, 2) == "\xFE\xFF") { - // strip BOM - $string = substr($string, 2); - } - $newcharstring = ''; - for ($i = 0; $i < strlen($string); $i += 2) { - $charval = self::BigEndian2Int(substr($string, $i, 2)); - $newcharstring .= self::iconv_fallback_int_utf8($charval); - } - return $newcharstring; - } - - // UTF-16LE => UTF-8 - public static function iconv_fallback_utf16le_utf8($string) { - if (substr($string, 0, 2) == "\xFF\xFE") { - // strip BOM - $string = substr($string, 2); - } - $newcharstring = ''; - for ($i = 0; $i < strlen($string); $i += 2) { - $charval = self::LittleEndian2Int(substr($string, $i, 2)); - $newcharstring .= self::iconv_fallback_int_utf8($charval); - } - return $newcharstring; - } - - // UTF-16BE => ISO-8859-1 - public static function iconv_fallback_utf16be_iso88591($string) { - if (substr($string, 0, 2) == "\xFE\xFF") { - // strip BOM - $string = substr($string, 2); - } - $newcharstring = ''; - for ($i = 0; $i < strlen($string); $i += 2) { - $charval = self::BigEndian2Int(substr($string, $i, 2)); - $newcharstring .= (($charval < 256) ? chr($charval) : '?'); - } - return $newcharstring; - } - - // UTF-16LE => ISO-8859-1 - public static function iconv_fallback_utf16le_iso88591($string) { - if (substr($string, 0, 2) == "\xFF\xFE") { - // strip BOM - $string = substr($string, 2); - } - $newcharstring = ''; - for ($i = 0; $i < strlen($string); $i += 2) { - $charval = self::LittleEndian2Int(substr($string, $i, 2)); - $newcharstring .= (($charval < 256) ? chr($charval) : '?'); - } - return $newcharstring; - } - - // UTF-16 (BOM) => ISO-8859-1 - public static function iconv_fallback_utf16_iso88591($string) { - $bom = substr($string, 0, 2); - if ($bom == "\xFE\xFF") { - return self::iconv_fallback_utf16be_iso88591(substr($string, 2)); - } elseif ($bom == "\xFF\xFE") { - return self::iconv_fallback_utf16le_iso88591(substr($string, 2)); - } - return $string; - } - - // UTF-16 (BOM) => UTF-8 - public static function iconv_fallback_utf16_utf8($string) { - $bom = substr($string, 0, 2); - if ($bom == "\xFE\xFF") { - return self::iconv_fallback_utf16be_utf8(substr($string, 2)); - } elseif ($bom == "\xFF\xFE") { - return self::iconv_fallback_utf16le_utf8(substr($string, 2)); - } - return $string; - } - - public static function iconv_fallback($in_charset, $out_charset, $string) { - - if ($in_charset == $out_charset) { - return $string; - } - - // iconv() availble - if (function_exists('iconv')) { - if ($converted_string = @iconv($in_charset, $out_charset.'//TRANSLIT', $string)) { - switch ($out_charset) { - case 'ISO-8859-1': - $converted_string = rtrim($converted_string, "\x00"); - break; - } - return $converted_string; - } - - // iconv() may sometimes fail with "illegal character in input string" error message - // and return an empty string, but returning the unconverted string is more useful - return $string; - } - - - // iconv() not available - static $ConversionFunctionList = array(); - if (empty($ConversionFunctionList)) { - $ConversionFunctionList['ISO-8859-1']['UTF-8'] = 'iconv_fallback_iso88591_utf8'; - $ConversionFunctionList['ISO-8859-1']['UTF-16'] = 'iconv_fallback_iso88591_utf16'; - $ConversionFunctionList['ISO-8859-1']['UTF-16BE'] = 'iconv_fallback_iso88591_utf16be'; - $ConversionFunctionList['ISO-8859-1']['UTF-16LE'] = 'iconv_fallback_iso88591_utf16le'; - $ConversionFunctionList['UTF-8']['ISO-8859-1'] = 'iconv_fallback_utf8_iso88591'; - $ConversionFunctionList['UTF-8']['UTF-16'] = 'iconv_fallback_utf8_utf16'; - $ConversionFunctionList['UTF-8']['UTF-16BE'] = 'iconv_fallback_utf8_utf16be'; - $ConversionFunctionList['UTF-8']['UTF-16LE'] = 'iconv_fallback_utf8_utf16le'; - $ConversionFunctionList['UTF-16']['ISO-8859-1'] = 'iconv_fallback_utf16_iso88591'; - $ConversionFunctionList['UTF-16']['UTF-8'] = 'iconv_fallback_utf16_utf8'; - $ConversionFunctionList['UTF-16LE']['ISO-8859-1'] = 'iconv_fallback_utf16le_iso88591'; - $ConversionFunctionList['UTF-16LE']['UTF-8'] = 'iconv_fallback_utf16le_utf8'; - $ConversionFunctionList['UTF-16BE']['ISO-8859-1'] = 'iconv_fallback_utf16be_iso88591'; - $ConversionFunctionList['UTF-16BE']['UTF-8'] = 'iconv_fallback_utf16be_utf8'; - } - if (isset($ConversionFunctionList[strtoupper($in_charset)][strtoupper($out_charset)])) { - $ConversionFunction = $ConversionFunctionList[strtoupper($in_charset)][strtoupper($out_charset)]; - return self::$ConversionFunction($string); - } - throw new Exception('PHP does not have iconv() support - cannot convert from '.$in_charset.' to '.$out_charset); - } - - - public static function MultiByteCharString2HTML($string, $charset='ISO-8859-1') { - $string = (string) $string; // in case trying to pass a numeric (float, int) string, would otherwise return an empty string - $HTMLstring = ''; - - switch ($charset) { - case '1251': - case '1252': - case '866': - case '932': - case '936': - case '950': - case 'BIG5': - case 'BIG5-HKSCS': - case 'cp1251': - case 'cp1252': - case 'cp866': - case 'EUC-JP': - case 'EUCJP': - case 'GB2312': - case 'ibm866': - case 'ISO-8859-1': - case 'ISO-8859-15': - case 'ISO8859-1': - case 'ISO8859-15': - case 'KOI8-R': - case 'koi8-ru': - case 'koi8r': - case 'Shift_JIS': - case 'SJIS': - case 'win-1251': - case 'Windows-1251': - case 'Windows-1252': - $HTMLstring = htmlentities($string, ENT_COMPAT, $charset); - break; - - case 'UTF-8': - $strlen = strlen($string); - for ($i = 0; $i < $strlen; $i++) { - $char_ord_val = ord($string{$i}); - $charval = 0; - if ($char_ord_val < 0x80) { - $charval = $char_ord_val; - } elseif ((($char_ord_val & 0xF0) >> 4) == 0x0F && $i+3 < $strlen) { - $charval = (($char_ord_val & 0x07) << 18); - $charval += ((ord($string{++$i}) & 0x3F) << 12); - $charval += ((ord($string{++$i}) & 0x3F) << 6); - $charval += (ord($string{++$i}) & 0x3F); - } elseif ((($char_ord_val & 0xE0) >> 5) == 0x07 && $i+2 < $strlen) { - $charval = (($char_ord_val & 0x0F) << 12); - $charval += ((ord($string{++$i}) & 0x3F) << 6); - $charval += (ord($string{++$i}) & 0x3F); - } elseif ((($char_ord_val & 0xC0) >> 6) == 0x03 && $i+1 < $strlen) { - $charval = (($char_ord_val & 0x1F) << 6); - $charval += (ord($string{++$i}) & 0x3F); - } - if (($charval >= 32) && ($charval <= 127)) { - $HTMLstring .= htmlentities(chr($charval)); - } else { - $HTMLstring .= '&#'.$charval.';'; - } - } - break; - - case 'UTF-16LE': - for ($i = 0; $i < strlen($string); $i += 2) { - $charval = self::LittleEndian2Int(substr($string, $i, 2)); - if (($charval >= 32) && ($charval <= 127)) { - $HTMLstring .= chr($charval); - } else { - $HTMLstring .= '&#'.$charval.';'; - } - } - break; - - case 'UTF-16BE': - for ($i = 0; $i < strlen($string); $i += 2) { - $charval = self::BigEndian2Int(substr($string, $i, 2)); - if (($charval >= 32) && ($charval <= 127)) { - $HTMLstring .= chr($charval); - } else { - $HTMLstring .= '&#'.$charval.';'; - } - } - break; - - default: - $HTMLstring = 'ERROR: Character set "'.$charset.'" not supported in MultiByteCharString2HTML()'; - break; - } - return $HTMLstring; - } - - - - public static function RGADnameLookup($namecode) { - static $RGADname = array(); - if (empty($RGADname)) { - $RGADname[0] = 'not set'; - $RGADname[1] = 'Track Gain Adjustment'; - $RGADname[2] = 'Album Gain Adjustment'; - } - - return (isset($RGADname[$namecode]) ? $RGADname[$namecode] : ''); - } - - - public static function RGADoriginatorLookup($originatorcode) { - static $RGADoriginator = array(); - if (empty($RGADoriginator)) { - $RGADoriginator[0] = 'unspecified'; - $RGADoriginator[1] = 'pre-set by artist/producer/mastering engineer'; - $RGADoriginator[2] = 'set by user'; - $RGADoriginator[3] = 'determined automatically'; - } - - return (isset($RGADoriginator[$originatorcode]) ? $RGADoriginator[$originatorcode] : ''); - } - - - public static function RGADadjustmentLookup($rawadjustment, $signbit) { - $adjustment = $rawadjustment / 10; - if ($signbit == 1) { - $adjustment *= -1; - } - return (float) $adjustment; - } - - - public static function RGADgainString($namecode, $originatorcode, $replaygain) { - if ($replaygain < 0) { - $signbit = '1'; - } else { - $signbit = '0'; - } - $storedreplaygain = intval(round($replaygain * 10)); - $gainstring = str_pad(decbin($namecode), 3, '0', STR_PAD_LEFT); - $gainstring .= str_pad(decbin($originatorcode), 3, '0', STR_PAD_LEFT); - $gainstring .= $signbit; - $gainstring .= str_pad(decbin($storedreplaygain), 9, '0', STR_PAD_LEFT); - - return $gainstring; - } - - public static function RGADamplitude2dB($amplitude) { - return 20 * log10($amplitude); - } - - - public static function GetDataImageSize($imgData, &$imageinfo=array()) { - static $tempdir = ''; - if (empty($tempdir)) { - // yes this is ugly, feel free to suggest a better way - require_once(dirname(__FILE__).'/getid3.php'); - $getid3_temp = new getID3(); - $tempdir = $getid3_temp->tempdir; - unset($getid3_temp); - } - $GetDataImageSize = false; - if ($tempfilename = tempnam($tempdir, 'gI3')) { - if (is_writable($tempfilename) && is_file($tempfilename) && ($tmp = fopen($tempfilename, 'wb'))) { - fwrite($tmp, $imgData); - fclose($tmp); - $GetDataImageSize = @getimagesize($tempfilename, $imageinfo); - } - unlink($tempfilename); - } - return $GetDataImageSize; - } - - public static function ImageExtFromMime($mime_type) { - // temporary way, works OK for now, but should be reworked in the future - return str_replace(array('image/', 'x-', 'jpeg'), array('', '', 'jpg'), $mime_type); - } - - public static function ImageTypesLookup($imagetypeid) { - static $ImageTypesLookup = array(); - if (empty($ImageTypesLookup)) { - $ImageTypesLookup[1] = 'gif'; - $ImageTypesLookup[2] = 'jpeg'; - $ImageTypesLookup[3] = 'png'; - $ImageTypesLookup[4] = 'swf'; - $ImageTypesLookup[5] = 'psd'; - $ImageTypesLookup[6] = 'bmp'; - $ImageTypesLookup[7] = 'tiff (little-endian)'; - $ImageTypesLookup[8] = 'tiff (big-endian)'; - $ImageTypesLookup[9] = 'jpc'; - $ImageTypesLookup[10] = 'jp2'; - $ImageTypesLookup[11] = 'jpx'; - $ImageTypesLookup[12] = 'jb2'; - $ImageTypesLookup[13] = 'swc'; - $ImageTypesLookup[14] = 'iff'; - } - return (isset($ImageTypesLookup[$imagetypeid]) ? $ImageTypesLookup[$imagetypeid] : ''); - } - - public static function CopyTagsToComments(&$ThisFileInfo) { - - // Copy all entries from ['tags'] into common ['comments'] - if (!empty($ThisFileInfo['tags'])) { - foreach ($ThisFileInfo['tags'] as $tagtype => $tagarray) { - foreach ($tagarray as $tagname => $tagdata) { - foreach ($tagdata as $key => $value) { - if (!empty($value)) { - if (empty($ThisFileInfo['comments'][$tagname])) { - - // fall through and append value - - } elseif ($tagtype == 'id3v1') { - - $newvaluelength = strlen(trim($value)); - foreach ($ThisFileInfo['comments'][$tagname] as $existingkey => $existingvalue) { - $oldvaluelength = strlen(trim($existingvalue)); - if (($newvaluelength <= $oldvaluelength) && (substr($existingvalue, 0, $newvaluelength) == trim($value))) { - // new value is identical but shorter-than (or equal-length to) one already in comments - skip - break 2; - } - } - - } elseif (!is_array($value)) { - - $newvaluelength = strlen(trim($value)); - foreach ($ThisFileInfo['comments'][$tagname] as $existingkey => $existingvalue) { - $oldvaluelength = strlen(trim($existingvalue)); - if (($newvaluelength > $oldvaluelength) && (substr(trim($value), 0, strlen($existingvalue)) == $existingvalue)) { - $ThisFileInfo['comments'][$tagname][$existingkey] = trim($value); - break 2; - } - } - - } - if (is_array($value) || empty($ThisFileInfo['comments'][$tagname]) || !in_array(trim($value), $ThisFileInfo['comments'][$tagname])) { - $value = (is_string($value) ? trim($value) : $value); - $ThisFileInfo['comments'][$tagname][] = $value; - } - } - } - } - } - - // Copy to ['comments_html'] - foreach ($ThisFileInfo['comments'] as $field => $values) { - if ($field == 'picture') { - // pictures can take up a lot of space, and we don't need multiple copies of them - // let there be a single copy in [comments][picture], and not elsewhere - continue; - } - foreach ($values as $index => $value) { - if (is_array($value)) { - $ThisFileInfo['comments_html'][$field][$index] = $value; - } else { - $ThisFileInfo['comments_html'][$field][$index] = str_replace('�', '', self::MultiByteCharString2HTML($value, $ThisFileInfo['encoding'])); - } - } - } - } - return true; - } - - - public static function EmbeddedLookup($key, $begin, $end, $file, $name) { - - // Cached - static $cache; - if (isset($cache[$file][$name])) { - return (isset($cache[$file][$name][$key]) ? $cache[$file][$name][$key] : ''); - } - - // Init - $keylength = strlen($key); - $line_count = $end - $begin - 7; - - // Open php file - $fp = fopen($file, 'r'); - - // Discard $begin lines - for ($i = 0; $i < ($begin + 3); $i++) { - fgets($fp, 1024); - } - - // Loop thru line - while (0 < $line_count--) { - - // Read line - $line = ltrim(fgets($fp, 1024), "\t "); - - // METHOD A: only cache the matching key - less memory but slower on next lookup of not-previously-looked-up key - //$keycheck = substr($line, 0, $keylength); - //if ($key == $keycheck) { - // $cache[$file][$name][$keycheck] = substr($line, $keylength + 1); - // break; - //} - - // METHOD B: cache all keys in this lookup - more memory but faster on next lookup of not-previously-looked-up key - //$cache[$file][$name][substr($line, 0, $keylength)] = trim(substr($line, $keylength + 1)); - $explodedLine = explode("\t", $line, 2); - $ThisKey = (isset($explodedLine[0]) ? $explodedLine[0] : ''); - $ThisValue = (isset($explodedLine[1]) ? $explodedLine[1] : ''); - $cache[$file][$name][$ThisKey] = trim($ThisValue); - } - - // Close and return - fclose($fp); - return (isset($cache[$file][$name][$key]) ? $cache[$file][$name][$key] : ''); - } - - public static function IncludeDependency($filename, $sourcefile, $DieOnFailure=false) { - global $GETID3_ERRORARRAY; - - if (file_exists($filename)) { - if (include_once($filename)) { - return true; - } else { - $diemessage = basename($sourcefile).' depends on '.$filename.', which has errors'; - } - } else { - $diemessage = basename($sourcefile).' depends on '.$filename.', which is missing'; - } - if ($DieOnFailure) { - throw new Exception($diemessage); - } else { - $GETID3_ERRORARRAY[] = $diemessage; - } - return false; - } - - public static function trimNullByte($string) { - return trim($string, "\x00"); - } - - public static function getFileSizeSyscall($path) { - $filesize = false; - - if (GETID3_OS_ISWINDOWS) { - if (class_exists('COM')) { // From PHP 5.3.15 and 5.4.5, COM and DOTNET is no longer built into the php core.you have to add COM support in php.ini: - $filesystem = new COM('Scripting.FileSystemObject'); - $file = $filesystem->GetFile($path); - $filesize = $file->Size(); - unset($filesystem, $file); - } else { - $commandline = 'for %I in ('.escapeshellarg($path).') do @echo %~zI'; - } - } else { - $commandline = 'ls -l '.escapeshellarg($path).' | awk \'{print $5}\''; - } - if (isset($commandline)) { - $output = trim(`$commandline`); - if (ctype_digit($output)) { - $filesize = (float) $output; - } - } - return $filesize; - } - -} diff --git a/src/Classes/Vendor/getid3/getid3.php b/src/Classes/Vendor/getid3/getid3.php deleted file mode 100755 index 55eb8f03d..000000000 --- a/src/Classes/Vendor/getid3/getid3.php +++ /dev/null @@ -1,1776 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// // -// Please see readme.txt for more information // -// /// -///////////////////////////////////////////////////////////////// - -// define a constant rather than looking up every time it is needed -if (!defined('GETID3_OS_ISWINDOWS')) { - define('GETID3_OS_ISWINDOWS', (stripos(PHP_OS, 'WIN') === 0)); -} -// Get base path of getID3() - ONCE -if (!defined('GETID3_INCLUDEPATH')) { - define('GETID3_INCLUDEPATH', dirname(__FILE__).DIRECTORY_SEPARATOR); -} - -// attempt to define temp dir as something flexible but reliable -$temp_dir = ini_get('upload_tmp_dir'); -if ($temp_dir && (!is_dir($temp_dir) || !is_readable($temp_dir))) { - $temp_dir = ''; -} -if (!$temp_dir && function_exists('sys_get_temp_dir')) { - // PHP v5.2.1+ - // sys_get_temp_dir() may give inaccessible temp dir, e.g. with open_basedir on virtual hosts - $temp_dir = sys_get_temp_dir(); -} -$temp_dir = realpath($temp_dir); -$open_basedir = ini_get('open_basedir'); -if ($open_basedir) { - // e.g. "/var/www/vhosts/getid3.org/httpdocs/:/tmp/" - $temp_dir = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $temp_dir); - $open_basedir = str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $open_basedir); - if (substr($temp_dir, -1, 1) != DIRECTORY_SEPARATOR) { - $temp_dir .= DIRECTORY_SEPARATOR; - } - $found_valid_tempdir = false; - $open_basedirs = explode(PATH_SEPARATOR, $open_basedir); - foreach ($open_basedirs as $basedir) { - if (substr($basedir, -1, 1) != DIRECTORY_SEPARATOR) { - $basedir .= DIRECTORY_SEPARATOR; - } - if (preg_match('#^'.preg_quote($basedir).'#', $temp_dir)) { - $found_valid_tempdir = true; - break; - } - } - if (!$found_valid_tempdir) { - $temp_dir = ''; - } - unset($open_basedirs, $found_valid_tempdir, $basedir); -} -if (!$temp_dir) { - $temp_dir = '*'; // invalid directory name should force tempnam() to use system default temp dir -} -// $temp_dir = '/something/else/'; // feel free to override temp dir here if it works better for your system -define('GETID3_TEMP_DIR', $temp_dir); -unset($open_basedir, $temp_dir); - -// End: Defines - - -class getID3 -{ - // public: Settings - public $encoding = 'UTF-8'; // CASE SENSITIVE! - i.e. (must be supported by iconv()). Examples: ISO-8859-1 UTF-8 UTF-16 UTF-16BE - public $encoding_id3v1 = 'ISO-8859-1'; // Should always be 'ISO-8859-1', but some tags may be written in other encodings such as 'EUC-CN' or 'CP1252' - - // public: Optional tag checks - disable for speed. - public $option_tag_id3v1 = true; // Read and process ID3v1 tags - public $option_tag_id3v2 = true; // Read and process ID3v2 tags - public $option_tag_lyrics3 = true; // Read and process Lyrics3 tags - public $option_tag_apetag = true; // Read and process APE tags - public $option_tags_process = true; // Copy tags to root key 'tags' and encode to $this->encoding - public $option_tags_html = true; // Copy tags to root key 'tags_html' properly translated from various encodings to HTML entities - - // public: Optional tag/comment calucations - public $option_extra_info = true; // Calculate additional info such as bitrate, channelmode etc - - // public: Optional handling of embedded attachments (e.g. images) - public $option_save_attachments = true; // defaults to true (ATTACHMENTS_INLINE) for backward compatibility - - // public: Optional calculations - public $option_md5_data = false; // Get MD5 sum of data part - slow - public $option_md5_data_source = false; // Use MD5 of source file if availble - only FLAC and OptimFROG - public $option_sha1_data = false; // Get SHA1 sum of data part - slow - public $option_max_2gb_check = null; // Check whether file is larger than 2GB and thus not supported by 32-bit PHP (null: auto-detect based on PHP_INT_MAX) - - // public: Read buffer size in bytes - public $option_fread_buffer_size = 32768; - - // Public variables - public $filename; // Filename of file being analysed. - public $fp; // Filepointer to file being analysed. - public $info; // Result array. - public $tempdir = GETID3_TEMP_DIR; - - // Protected variables - protected $startup_error = ''; - protected $startup_warning = ''; - protected $memory_limit = 0; - - const VERSION = '1.9.5-20130220'; - const FREAD_BUFFER_SIZE = 32768; - - const ATTACHMENTS_NONE = false; - const ATTACHMENTS_INLINE = true; - - // public: constructor - public function __construct() { - - // Check for PHP version - $required_php_version = '5.0.5'; - if (version_compare(PHP_VERSION, $required_php_version, '<')) { - $this->startup_error .= 'getID3() requires PHP v'.$required_php_version.' or higher - you are running v'.PHP_VERSION; - return false; - } - - // Check memory - $this->memory_limit = ini_get('memory_limit'); - if (preg_match('#([0-9]+)M#i', $this->memory_limit, $matches)) { - // could be stored as "16M" rather than 16777216 for example - $this->memory_limit = $matches[1] * 1048576; - } elseif (preg_match('#([0-9]+)G#i', $this->memory_limit, $matches)) { // The 'G' modifier is available since PHP 5.1.0 - // could be stored as "2G" rather than 2147483648 for example - $this->memory_limit = $matches[1] * 1073741824; - } - if ($this->memory_limit <= 0) { - // memory limits probably disabled - } elseif ($this->memory_limit <= 4194304) { - $this->startup_error .= 'PHP has less than 4MB available memory and will very likely run out. Increase memory_limit in php.ini'; - } elseif ($this->memory_limit <= 12582912) { - $this->startup_warning .= 'PHP has less than 12MB available memory and might run out if all modules are loaded. Increase memory_limit in php.ini'; - } - - // Check safe_mode off - if (preg_match('#(1|ON)#i', ini_get('safe_mode'))) { - $this->warning('WARNING: Safe mode is on, shorten support disabled, md5data/sha1data for ogg vorbis disabled, ogg vorbos/flac tag writing disabled.'); - } - - if (intval(ini_get('mbstring.func_overload')) > 0) { - $this->warning('WARNING: php.ini contains "mbstring.func_overload = '.ini_get('mbstring.func_overload').'", this may break things.'); - } - - // Check for magic_quotes_runtime - if (function_exists('get_magic_quotes_runtime')) { - if (get_magic_quotes_runtime()) { - return $this->startup_error('magic_quotes_runtime must be disabled before running getID3(). Surround getid3 block by set_magic_quotes_runtime(0) and set_magic_quotes_runtime(1).'); - } - } - - // Check for magic_quotes_gpc - if (function_exists('magic_quotes_gpc')) { - if (get_magic_quotes_gpc()) { - return $this->startup_error('magic_quotes_gpc must be disabled before running getID3(). Surround getid3 block by set_magic_quotes_gpc(0) and set_magic_quotes_gpc(1).'); - } - } - - // Load support library - if (!include_once(GETID3_INCLUDEPATH.'getid3.lib.php')) { - $this->startup_error .= 'getid3.lib.php is missing or corrupt'; - } - - if ($this->option_max_2gb_check === null) { - $this->option_max_2gb_check = (PHP_INT_MAX <= 2147483647); - } - - - // Needed for Windows only: - // Define locations of helper applications for Shorten, VorbisComment, MetaFLAC - // as well as other helper functions such as head, tail, md5sum, etc - // This path cannot contain spaces, but the below code will attempt to get the - // 8.3-equivalent path automatically - // IMPORTANT: This path must include the trailing slash - if (GETID3_OS_ISWINDOWS && !defined('GETID3_HELPERAPPSDIR')) { - - $helperappsdir = GETID3_INCLUDEPATH.'..'.DIRECTORY_SEPARATOR.'helperapps'; // must not have any space in this path - - if (!is_dir($helperappsdir)) { - $this->startup_warning .= '"'.$helperappsdir.'" cannot be defined as GETID3_HELPERAPPSDIR because it does not exist'; - } elseif (strpos(realpath($helperappsdir), ' ') !== false) { - $DirPieces = explode(DIRECTORY_SEPARATOR, realpath($helperappsdir)); - $path_so_far = array(); - foreach ($DirPieces as $key => $value) { - if (strpos($value, ' ') !== false) { - if (!empty($path_so_far)) { - $commandline = 'dir /x '.escapeshellarg(implode(DIRECTORY_SEPARATOR, $path_so_far)); - $dir_listing = `$commandline`; - $lines = explode("\n", $dir_listing); - foreach ($lines as $line) { - $line = trim($line); - if (preg_match('#^([0-9/]{10}) +([0-9:]{4,5}( [AP]M)?) +(|[0-9,]+) +([^ ]{0,11}) +(.+)$#', $line, $matches)) { - list($dummy, $date, $time, $ampm, $filesize, $shortname, $filename) = $matches; - if ((strtoupper($filesize) == '') && (strtolower($filename) == strtolower($value))) { - $value = $shortname; - } - } - } - } else { - $this->startup_warning .= 'GETID3_HELPERAPPSDIR must not have any spaces in it - use 8dot3 naming convention if neccesary. You can run "dir /x" from the commandline to see the correct 8.3-style names.'; - } - } - $path_so_far[] = $value; - } - $helperappsdir = implode(DIRECTORY_SEPARATOR, $path_so_far); - } - define('GETID3_HELPERAPPSDIR', $helperappsdir.DIRECTORY_SEPARATOR); - } - - return true; - } - - public function version() { - return self::VERSION; - } - - public function fread_buffer_size() { - return $this->option_fread_buffer_size; - } - - - // public: setOption - public function setOption($optArray) { - if (!is_array($optArray) || empty($optArray)) { - return false; - } - foreach ($optArray as $opt => $val) { - if (isset($this->$opt) === false) { - continue; - } - $this->$opt = $val; - } - return true; - } - - - public function openfile($filename) { - try { - if (!empty($this->startup_error)) { - throw new getid3_exception($this->startup_error); - } - if (!empty($this->startup_warning)) { - $this->warning($this->startup_warning); - } - - // init result array and set parameters - $this->filename = $filename; - $this->info = array(); - $this->info['GETID3_VERSION'] = $this->version(); - $this->info['php_memory_limit'] = $this->memory_limit; - - // remote files not supported - if (preg_match('/^(ht|f)tp:\/\//', $filename)) { - throw new getid3_exception('Remote files are not supported - please copy the file locally first'); - } - - $filename = str_replace('/', DIRECTORY_SEPARATOR, $filename); - $filename = preg_replace('#(.+)'.preg_quote(DIRECTORY_SEPARATOR).'{2,}#U', '\1'.DIRECTORY_SEPARATOR, $filename); - - // open local file - if (is_readable($filename) && is_file($filename) && ($this->fp = fopen($filename, 'rb'))) { - // great - } else { - throw new getid3_exception('Could not open "'.$filename.'" (does not exist, or is not a file)'); - } - - $this->info['filesize'] = filesize($filename); - // set redundant parameters - might be needed in some include file - $this->info['filename'] = basename($filename); - $this->info['filepath'] = str_replace('\\', '/', realpath(dirname($filename))); - $this->info['filenamepath'] = $this->info['filepath'].'/'.$this->info['filename']; - - - // option_max_2gb_check - if ($this->option_max_2gb_check) { - // PHP (32-bit all, and 64-bit Windows) doesn't support integers larger than 2^31 (~2GB) - // filesize() simply returns (filesize % (pow(2, 32)), no matter the actual filesize - // ftell() returns 0 if seeking to the end is beyond the range of unsigned integer - $fseek = fseek($this->fp, 0, SEEK_END); - if (($fseek < 0) || (($this->info['filesize'] != 0) && (ftell($this->fp) == 0)) || - ($this->info['filesize'] < 0) || - (ftell($this->fp) < 0)) { - $real_filesize = getid3_lib::getFileSizeSyscall($this->info['filenamepath']); - - if ($real_filesize === false) { - unset($this->info['filesize']); - fclose($this->fp); - throw new getid3_exception('Unable to determine actual filesize. File is most likely larger than '.round(PHP_INT_MAX / 1073741824).'GB and is not supported by PHP.'); - } elseif (getid3_lib::intValueSupported($real_filesize)) { - unset($this->info['filesize']); - fclose($this->fp); - throw new getid3_exception('PHP seems to think the file is larger than '.round(PHP_INT_MAX / 1073741824).'GB, but filesystem reports it as '.number_format($real_filesize, 3).'GB, please report to info@getid3.org'); - } - $this->info['filesize'] = $real_filesize; - $this->error('File is larger than '.round(PHP_INT_MAX / 1073741824).'GB (filesystem reports it as '.number_format($real_filesize, 3).'GB) and is not properly supported by PHP.'); - } - } - - // set more parameters - $this->info['avdataoffset'] = 0; - $this->info['avdataend'] = $this->info['filesize']; - $this->info['fileformat'] = ''; // filled in later - $this->info['audio']['dataformat'] = ''; // filled in later, unset if not used - $this->info['video']['dataformat'] = ''; // filled in later, unset if not used - $this->info['tags'] = array(); // filled in later, unset if not used - $this->info['error'] = array(); // filled in later, unset if not used - $this->info['warning'] = array(); // filled in later, unset if not used - $this->info['comments'] = array(); // filled in later, unset if not used - $this->info['encoding'] = $this->encoding; // required by id3v2 and iso modules - can be unset at the end if desired - - return true; - - } catch (Exception $e) { - $this->error($e->getMessage()); - } - return false; - } - - // public: analyze file - public function analyze($filename) { - try { - if (!$this->openfile($filename)) { - return $this->info; - } - - // Handle tags - foreach (array('id3v2'=>'id3v2', 'id3v1'=>'id3v1', 'apetag'=>'ape', 'lyrics3'=>'lyrics3') as $tag_name => $tag_key) { - $option_tag = 'option_tag_'.$tag_name; - if ($this->$option_tag) { - $this->include_module('tag.'.$tag_name); - try { - $tag_class = 'getid3_'.$tag_name; - $tag = new $tag_class($this); - $tag->Analyze(); - } - catch (getid3_exception $e) { - throw $e; - } - } - } - if (isset($this->info['id3v2']['tag_offset_start'])) { - $this->info['avdataoffset'] = max($this->info['avdataoffset'], $this->info['id3v2']['tag_offset_end']); - } - foreach (array('id3v1'=>'id3v1', 'apetag'=>'ape', 'lyrics3'=>'lyrics3') as $tag_name => $tag_key) { - if (isset($this->info[$tag_key]['tag_offset_start'])) { - $this->info['avdataend'] = min($this->info['avdataend'], $this->info[$tag_key]['tag_offset_start']); - } - } - - // ID3v2 detection (NOT parsing), even if ($this->option_tag_id3v2 == false) done to make fileformat easier - if (!$this->option_tag_id3v2) { - fseek($this->fp, 0, SEEK_SET); - $header = fread($this->fp, 10); - if ((substr($header, 0, 3) == 'ID3') && (strlen($header) == 10)) { - $this->info['id3v2']['header'] = true; - $this->info['id3v2']['majorversion'] = ord($header{3}); - $this->info['id3v2']['minorversion'] = ord($header{4}); - $this->info['avdataoffset'] += getid3_lib::BigEndian2Int(substr($header, 6, 4), 1) + 10; // length of ID3v2 tag in 10-byte header doesn't include 10-byte header length - } - } - - // read 32 kb file data - fseek($this->fp, $this->info['avdataoffset'], SEEK_SET); - $formattest = fread($this->fp, 32774); - - // determine format - $determined_format = $this->GetFileFormat($formattest, $filename); - - // unable to determine file format - if (!$determined_format) { - fclose($this->fp); - return $this->error('unable to determine file format'); - } - - // check for illegal ID3 tags - if (isset($determined_format['fail_id3']) && (in_array('id3v1', $this->info['tags']) || in_array('id3v2', $this->info['tags']))) { - if ($determined_format['fail_id3'] === 'ERROR') { - fclose($this->fp); - return $this->error('ID3 tags not allowed on this file type.'); - } elseif ($determined_format['fail_id3'] === 'WARNING') { - $this->warning('ID3 tags not allowed on this file type.'); - } - } - - // check for illegal APE tags - if (isset($determined_format['fail_ape']) && in_array('ape', $this->info['tags'])) { - if ($determined_format['fail_ape'] === 'ERROR') { - fclose($this->fp); - return $this->error('APE tags not allowed on this file type.'); - } elseif ($determined_format['fail_ape'] === 'WARNING') { - $this->warning('APE tags not allowed on this file type.'); - } - } - - // set mime type - $this->info['mime_type'] = $determined_format['mime_type']; - - // supported format signature pattern detected, but module deleted - if (!file_exists(GETID3_INCLUDEPATH.$determined_format['include'])) { - fclose($this->fp); - return $this->error('Format not supported, module "'.$determined_format['include'].'" was removed.'); - } - - // module requires iconv support - // Check encoding/iconv support - if (!empty($determined_format['iconv_req']) && !function_exists('iconv') && !in_array($this->encoding, array('ISO-8859-1', 'UTF-8', 'UTF-16LE', 'UTF-16BE', 'UTF-16'))) { - $errormessage = 'iconv() support is required for this module ('.$determined_format['include'].') for encodings other than ISO-8859-1, UTF-8, UTF-16LE, UTF16-BE, UTF-16. '; - if (GETID3_OS_ISWINDOWS) { - $errormessage .= 'PHP does not have iconv() support. Please enable php_iconv.dll in php.ini, and copy iconv.dll from c:/php/dlls to c:/windows/system32'; - } else { - $errormessage .= 'PHP is not compiled with iconv() support. Please recompile with the --with-iconv switch'; - } - return $this->error($errormessage); - } - - // include module - include_once(GETID3_INCLUDEPATH.$determined_format['include']); - - // instantiate module class - $class_name = 'getid3_'.$determined_format['module']; - if (!class_exists($class_name)) { - return $this->error('Format not supported, module "'.$determined_format['include'].'" is corrupt.'); - } - $class = new $class_name($this); - $class->Analyze(); - unset($class); - - // close file - fclose($this->fp); - - // process all tags - copy to 'tags' and convert charsets - if ($this->option_tags_process) { - $this->HandleAllTags(); - } - - // perform more calculations - if ($this->option_extra_info) { - $this->ChannelsBitratePlaytimeCalculations(); - $this->CalculateCompressionRatioVideo(); - $this->CalculateCompressionRatioAudio(); - $this->CalculateReplayGain(); - $this->ProcessAudioStreams(); - } - - // get the MD5 sum of the audio/video portion of the file - without ID3/APE/Lyrics3/etc header/footer tags - if ($this->option_md5_data) { - // do not calc md5_data if md5_data_source is present - set by flac only - future MPC/SV8 too - if (!$this->option_md5_data_source || empty($this->info['md5_data_source'])) { - $this->getHashdata('md5'); - } - } - - // get the SHA1 sum of the audio/video portion of the file - without ID3/APE/Lyrics3/etc header/footer tags - if ($this->option_sha1_data) { - $this->getHashdata('sha1'); - } - - // remove undesired keys - $this->CleanUp(); - - } catch (Exception $e) { - $this->error('Caught exception: '.$e->getMessage()); - } - - // return info array - return $this->info; - } - - - // private: error handling - public function error($message) { - $this->CleanUp(); - if (!isset($this->info['error'])) { - $this->info['error'] = array(); - } - $this->info['error'][] = $message; - return $this->info; - } - - - // private: warning handling - public function warning($message) { - $this->info['warning'][] = $message; - return true; - } - - - // private: CleanUp - private function CleanUp() { - - // remove possible empty keys - $AVpossibleEmptyKeys = array('dataformat', 'bits_per_sample', 'encoder_options', 'streams', 'bitrate'); - foreach ($AVpossibleEmptyKeys as $dummy => $key) { - if (empty($this->info['audio'][$key]) && isset($this->info['audio'][$key])) { - unset($this->info['audio'][$key]); - } - if (empty($this->info['video'][$key]) && isset($this->info['video'][$key])) { - unset($this->info['video'][$key]); - } - } - - // remove empty root keys - if (!empty($this->info)) { - foreach ($this->info as $key => $value) { - if (empty($this->info[$key]) && ($this->info[$key] !== 0) && ($this->info[$key] !== '0')) { - unset($this->info[$key]); - } - } - } - - // remove meaningless entries from unknown-format files - if (empty($this->info['fileformat'])) { - if (isset($this->info['avdataoffset'])) { - unset($this->info['avdataoffset']); - } - if (isset($this->info['avdataend'])) { - unset($this->info['avdataend']); - } - } - - // remove possible duplicated identical entries - if (!empty($this->info['error'])) { - $this->info['error'] = array_values(array_unique($this->info['error'])); - } - if (!empty($this->info['warning'])) { - $this->info['warning'] = array_values(array_unique($this->info['warning'])); - } - - // remove "global variable" type keys - unset($this->info['php_memory_limit']); - - return true; - } - - - // return array containing information about all supported formats - public function GetFileFormatArray() { - static $format_info = array(); - if (empty($format_info)) { - $format_info = array( - - // Audio formats - - // AC-3 - audio - Dolby AC-3 / Dolby Digital - 'ac3' => array( - 'pattern' => '^\x0B\x77', - 'group' => 'audio', - 'module' => 'ac3', - 'mime_type' => 'audio/ac3', - ), - - // AAC - audio - Advanced Audio Coding (AAC) - ADIF format - 'adif' => array( - 'pattern' => '^ADIF', - 'group' => 'audio', - 'module' => 'aac', - 'mime_type' => 'application/octet-stream', - 'fail_ape' => 'WARNING', - ), - -/* - // AA - audio - Audible Audiobook - 'aa' => array( - 'pattern' => '^.{4}\x57\x90\x75\x36', - 'group' => 'audio', - 'module' => 'aa', - 'mime_type' => 'audio/audible', - ), -*/ - // AAC - audio - Advanced Audio Coding (AAC) - ADTS format (very similar to MP3) - 'adts' => array( - 'pattern' => '^\xFF[\xF0-\xF1\xF8-\xF9]', - 'group' => 'audio', - 'module' => 'aac', - 'mime_type' => 'application/octet-stream', - 'fail_ape' => 'WARNING', - ), - - - // AU - audio - NeXT/Sun AUdio (AU) - 'au' => array( - 'pattern' => '^\.snd', - 'group' => 'audio', - 'module' => 'au', - 'mime_type' => 'audio/basic', - ), - - // AVR - audio - Audio Visual Research - 'avr' => array( - 'pattern' => '^2BIT', - 'group' => 'audio', - 'module' => 'avr', - 'mime_type' => 'application/octet-stream', - ), - - // BONK - audio - Bonk v0.9+ - 'bonk' => array( - 'pattern' => '^\x00(BONK|INFO|META| ID3)', - 'group' => 'audio', - 'module' => 'bonk', - 'mime_type' => 'audio/xmms-bonk', - ), - - // DSS - audio - Digital Speech Standard - 'dss' => array( - 'pattern' => '^[\x02-\x03]ds[s2]', - 'group' => 'audio', - 'module' => 'dss', - 'mime_type' => 'application/octet-stream', - ), - - // DTS - audio - Dolby Theatre System - 'dts' => array( - 'pattern' => '^\x7F\xFE\x80\x01', - 'group' => 'audio', - 'module' => 'dts', - 'mime_type' => 'audio/dts', - ), - - // FLAC - audio - Free Lossless Audio Codec - 'flac' => array( - 'pattern' => '^fLaC', - 'group' => 'audio', - 'module' => 'flac', - 'mime_type' => 'audio/x-flac', - ), - - // LA - audio - Lossless Audio (LA) - 'la' => array( - 'pattern' => '^LA0[2-4]', - 'group' => 'audio', - 'module' => 'la', - 'mime_type' => 'application/octet-stream', - ), - - // LPAC - audio - Lossless Predictive Audio Compression (LPAC) - 'lpac' => array( - 'pattern' => '^LPAC', - 'group' => 'audio', - 'module' => 'lpac', - 'mime_type' => 'application/octet-stream', - ), - - // MIDI - audio - MIDI (Musical Instrument Digital Interface) - 'midi' => array( - 'pattern' => '^MThd', - 'group' => 'audio', - 'module' => 'midi', - 'mime_type' => 'audio/midi', - ), - - // MAC - audio - Monkey's Audio Compressor - 'mac' => array( - 'pattern' => '^MAC ', - 'group' => 'audio', - 'module' => 'monkey', - 'mime_type' => 'application/octet-stream', - ), - -// has been known to produce false matches in random files (e.g. JPEGs), leave out until more precise matching available -// // MOD - audio - MODule (assorted sub-formats) -// 'mod' => array( -// 'pattern' => '^.{1080}(M\\.K\\.|M!K!|FLT4|FLT8|[5-9]CHN|[1-3][0-9]CH)', -// 'group' => 'audio', -// 'module' => 'mod', -// 'option' => 'mod', -// 'mime_type' => 'audio/mod', -// ), - - // MOD - audio - MODule (Impulse Tracker) - 'it' => array( - 'pattern' => '^IMPM', - 'group' => 'audio', - 'module' => 'mod', - //'option' => 'it', - 'mime_type' => 'audio/it', - ), - - // MOD - audio - MODule (eXtended Module, various sub-formats) - 'xm' => array( - 'pattern' => '^Extended Module', - 'group' => 'audio', - 'module' => 'mod', - //'option' => 'xm', - 'mime_type' => 'audio/xm', - ), - - // MOD - audio - MODule (ScreamTracker) - 's3m' => array( - 'pattern' => '^.{44}SCRM', - 'group' => 'audio', - 'module' => 'mod', - //'option' => 's3m', - 'mime_type' => 'audio/s3m', - ), - - // MPC - audio - Musepack / MPEGplus - 'mpc' => array( - 'pattern' => '^(MPCK|MP\+|[\x00\x01\x10\x11\x40\x41\x50\x51\x80\x81\x90\x91\xC0\xC1\xD0\xD1][\x20-37][\x00\x20\x40\x60\x80\xA0\xC0\xE0])', - 'group' => 'audio', - 'module' => 'mpc', - 'mime_type' => 'audio/x-musepack', - ), - - // MP3 - audio - MPEG-audio Layer 3 (very similar to AAC-ADTS) - 'mp3' => array( - 'pattern' => '^\xFF[\xE2-\xE7\xF2-\xF7\xFA-\xFF][\x00-\x0B\x10-\x1B\x20-\x2B\x30-\x3B\x40-\x4B\x50-\x5B\x60-\x6B\x70-\x7B\x80-\x8B\x90-\x9B\xA0-\xAB\xB0-\xBB\xC0-\xCB\xD0-\xDB\xE0-\xEB\xF0-\xFB]', - 'group' => 'audio', - 'module' => 'mp3', - 'mime_type' => 'audio/mpeg', - ), - - // OFR - audio - OptimFROG - 'ofr' => array( - 'pattern' => '^(\*RIFF|OFR)', - 'group' => 'audio', - 'module' => 'optimfrog', - 'mime_type' => 'application/octet-stream', - ), - - // RKAU - audio - RKive AUdio compressor - 'rkau' => array( - 'pattern' => '^RKA', - 'group' => 'audio', - 'module' => 'rkau', - 'mime_type' => 'application/octet-stream', - ), - - // SHN - audio - Shorten - 'shn' => array( - 'pattern' => '^ajkg', - 'group' => 'audio', - 'module' => 'shorten', - 'mime_type' => 'audio/xmms-shn', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - ), - - // TTA - audio - TTA Lossless Audio Compressor (http://tta.corecodec.org) - 'tta' => array( - 'pattern' => '^TTA', // could also be '^TTA(\x01|\x02|\x03|2|1)' - 'group' => 'audio', - 'module' => 'tta', - 'mime_type' => 'application/octet-stream', - ), - - // VOC - audio - Creative Voice (VOC) - 'voc' => array( - 'pattern' => '^Creative Voice File', - 'group' => 'audio', - 'module' => 'voc', - 'mime_type' => 'audio/voc', - ), - - // VQF - audio - transform-domain weighted interleave Vector Quantization Format (VQF) - 'vqf' => array( - 'pattern' => '^TWIN', - 'group' => 'audio', - 'module' => 'vqf', - 'mime_type' => 'application/octet-stream', - ), - - // WV - audio - WavPack (v4.0+) - 'wv' => array( - 'pattern' => '^wvpk', - 'group' => 'audio', - 'module' => 'wavpack', - 'mime_type' => 'application/octet-stream', - ), - - - // Audio-Video formats - - // ASF - audio/video - Advanced Streaming Format, Windows Media Video, Windows Media Audio - 'asf' => array( - 'pattern' => '^\x30\x26\xB2\x75\x8E\x66\xCF\x11\xA6\xD9\x00\xAA\x00\x62\xCE\x6C', - 'group' => 'audio-video', - 'module' => 'asf', - 'mime_type' => 'video/x-ms-asf', - 'iconv_req' => false, - ), - - // BINK - audio/video - Bink / Smacker - 'bink' => array( - 'pattern' => '^(BIK|SMK)', - 'group' => 'audio-video', - 'module' => 'bink', - 'mime_type' => 'application/octet-stream', - ), - - // FLV - audio/video - FLash Video - 'flv' => array( - 'pattern' => '^FLV\x01', - 'group' => 'audio-video', - 'module' => 'flv', - 'mime_type' => 'video/x-flv', - ), - - // MKAV - audio/video - Mastroka - 'matroska' => array( - 'pattern' => '^\x1A\x45\xDF\xA3', - 'group' => 'audio-video', - 'module' => 'matroska', - 'mime_type' => 'video/x-matroska', // may also be audio/x-matroska - ), - - // MPEG - audio/video - MPEG (Moving Pictures Experts Group) - 'mpeg' => array( - 'pattern' => '^\x00\x00\x01(\xBA|\xB3)', - 'group' => 'audio-video', - 'module' => 'mpeg', - 'mime_type' => 'video/mpeg', - ), - - // NSV - audio/video - Nullsoft Streaming Video (NSV) - 'nsv' => array( - 'pattern' => '^NSV[sf]', - 'group' => 'audio-video', - 'module' => 'nsv', - 'mime_type' => 'application/octet-stream', - ), - - // Ogg - audio/video - Ogg (Ogg-Vorbis, Ogg-FLAC, Speex, Ogg-Theora(*), Ogg-Tarkin(*)) - 'ogg' => array( - 'pattern' => '^OggS', - 'group' => 'audio', - 'module' => 'ogg', - 'mime_type' => 'application/ogg', - 'fail_id3' => 'WARNING', - 'fail_ape' => 'WARNING', - ), - - // QT - audio/video - Quicktime - 'quicktime' => array( - 'pattern' => '^.{4}(cmov|free|ftyp|mdat|moov|pnot|skip|wide)', - 'group' => 'audio-video', - 'module' => 'quicktime', - 'mime_type' => 'video/quicktime', - ), - - // RIFF - audio/video - Resource Interchange File Format (RIFF) / WAV / AVI / CD-audio / SDSS = renamed variant used by SmartSound QuickTracks (www.smartsound.com) / FORM = Audio Interchange File Format (AIFF) - 'riff' => array( - 'pattern' => '^(RIFF|SDSS|FORM)', - 'group' => 'audio-video', - 'module' => 'riff', - 'mime_type' => 'audio/x-wave', - 'fail_ape' => 'WARNING', - ), - - // Real - audio/video - RealAudio, RealVideo - 'real' => array( - 'pattern' => '^(\\.RMF|\\.ra)', - 'group' => 'audio-video', - 'module' => 'real', - 'mime_type' => 'audio/x-realaudio', - ), - - // SWF - audio/video - ShockWave Flash - 'swf' => array( - 'pattern' => '^(F|C)WS', - 'group' => 'audio-video', - 'module' => 'swf', - 'mime_type' => 'application/x-shockwave-flash', - ), - - // TS - audio/video - MPEG-2 Transport Stream - 'ts' => array( - 'pattern' => '^(\x47.{187}){10,}', // packets are 188 bytes long and start with 0x47 "G". Check for at least 10 packets matching this pattern - 'group' => 'audio-video', - 'module' => 'ts', - 'mime_type' => 'video/MP2T', - ), - - - // Still-Image formats - - // BMP - still image - Bitmap (Windows, OS/2; uncompressed, RLE8, RLE4) - 'bmp' => array( - 'pattern' => '^BM', - 'group' => 'graphic', - 'module' => 'bmp', - 'mime_type' => 'image/bmp', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - ), - - // GIF - still image - Graphics Interchange Format - 'gif' => array( - 'pattern' => '^GIF', - 'group' => 'graphic', - 'module' => 'gif', - 'mime_type' => 'image/gif', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - ), - - // JPEG - still image - Joint Photographic Experts Group (JPEG) - 'jpg' => array( - 'pattern' => '^\xFF\xD8\xFF', - 'group' => 'graphic', - 'module' => 'jpg', - 'mime_type' => 'image/jpeg', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - ), - - // PCD - still image - Kodak Photo CD - 'pcd' => array( - 'pattern' => '^.{2048}PCD_IPI\x00', - 'group' => 'graphic', - 'module' => 'pcd', - 'mime_type' => 'image/x-photo-cd', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - ), - - - // PNG - still image - Portable Network Graphics (PNG) - 'png' => array( - 'pattern' => '^\x89\x50\x4E\x47\x0D\x0A\x1A\x0A', - 'group' => 'graphic', - 'module' => 'png', - 'mime_type' => 'image/png', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - ), - - - // SVG - still image - Scalable Vector Graphics (SVG) - 'svg' => array( - 'pattern' => '( 'graphic', - 'module' => 'svg', - 'mime_type' => 'image/svg+xml', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - ), - - - // TIFF - still image - Tagged Information File Format (TIFF) - 'tiff' => array( - 'pattern' => '^(II\x2A\x00|MM\x00\x2A)', - 'group' => 'graphic', - 'module' => 'tiff', - 'mime_type' => 'image/tiff', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - ), - - - // EFAX - still image - eFax (TIFF derivative) - 'efax' => array( - 'pattern' => '^\xDC\xFE', - 'group' => 'graphic', - 'module' => 'efax', - 'mime_type' => 'image/efax', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - ), - - - // Data formats - - // ISO - data - International Standards Organization (ISO) CD-ROM Image - 'iso' => array( - 'pattern' => '^.{32769}CD001', - 'group' => 'misc', - 'module' => 'iso', - 'mime_type' => 'application/octet-stream', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - 'iconv_req' => false, - ), - - // RAR - data - RAR compressed data - 'rar' => array( - 'pattern' => '^Rar\!', - 'group' => 'archive', - 'module' => 'rar', - 'mime_type' => 'application/octet-stream', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - ), - - // SZIP - audio/data - SZIP compressed data - 'szip' => array( - 'pattern' => '^SZ\x0A\x04', - 'group' => 'archive', - 'module' => 'szip', - 'mime_type' => 'application/octet-stream', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - ), - - // TAR - data - TAR compressed data - 'tar' => array( - 'pattern' => '^.{100}[0-9\x20]{7}\x00[0-9\x20]{7}\x00[0-9\x20]{7}\x00[0-9\x20\x00]{12}[0-9\x20\x00]{12}', - 'group' => 'archive', - 'module' => 'tar', - 'mime_type' => 'application/x-tar', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - ), - - // GZIP - data - GZIP compressed data - 'gz' => array( - 'pattern' => '^\x1F\x8B\x08', - 'group' => 'archive', - 'module' => 'gzip', - 'mime_type' => 'application/x-gzip', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - ), - - // ZIP - data - ZIP compressed data - 'zip' => array( - 'pattern' => '^PK\x03\x04', - 'group' => 'archive', - 'module' => 'zip', - 'mime_type' => 'application/zip', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - ), - - - // Misc other formats - - // PAR2 - data - Parity Volume Set Specification 2.0 - 'par2' => array ( - 'pattern' => '^PAR2\x00PKT', - 'group' => 'misc', - 'module' => 'par2', - 'mime_type' => 'application/octet-stream', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - ), - - // PDF - data - Portable Document Format - 'pdf' => array( - 'pattern' => '^\x25PDF', - 'group' => 'misc', - 'module' => 'pdf', - 'mime_type' => 'application/pdf', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - ), - - // MSOFFICE - data - ZIP compressed data - 'msoffice' => array( - 'pattern' => '^\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1', // D0CF11E == DOCFILE == Microsoft Office Document - 'group' => 'misc', - 'module' => 'msoffice', - 'mime_type' => 'application/octet-stream', - 'fail_id3' => 'ERROR', - 'fail_ape' => 'ERROR', - ), - - // CUE - data - CUEsheet (index to single-file disc images) - 'cue' => array( - 'pattern' => '', // empty pattern means cannot be automatically detected, will fall through all other formats and match based on filename and very basic file contents - 'group' => 'misc', - 'module' => 'cue', - 'mime_type' => 'application/octet-stream', - ), - - ); - } - - return $format_info; - } - - - - public function GetFileFormat(&$filedata, $filename='') { - // this function will determine the format of a file based on usually - // the first 2-4 bytes of the file (8 bytes for PNG, 16 bytes for JPG, - // and in the case of ISO CD image, 6 bytes offset 32kb from the start - // of the file). - - // Identify file format - loop through $format_info and detect with reg expr - foreach ($this->GetFileFormatArray() as $format_name => $info) { - // The /s switch on preg_match() forces preg_match() NOT to treat - // newline (0x0A) characters as special chars but do a binary match - if (!empty($info['pattern']) && preg_match('#'.$info['pattern'].'#s', $filedata)) { - $info['include'] = 'module.'.$info['group'].'.'.$info['module'].'.php'; - return $info; - } - } - - - if (preg_match('#\.mp[123a]$#i', $filename)) { - // Too many mp3 encoders on the market put gabage in front of mpeg files - // use assume format on these if format detection failed - $GetFileFormatArray = $this->GetFileFormatArray(); - $info = $GetFileFormatArray['mp3']; - $info['include'] = 'module.'.$info['group'].'.'.$info['module'].'.php'; - return $info; - } elseif (preg_match('/\.cue$/i', $filename) && preg_match('#FILE "[^"]+" (BINARY|MOTOROLA|AIFF|WAVE|MP3)#', $filedata)) { - // there's not really a useful consistent "magic" at the beginning of .cue files to identify them - // so until I think of something better, just go by filename if all other format checks fail - // and verify there's at least one instance of "TRACK xx AUDIO" in the file - $GetFileFormatArray = $this->GetFileFormatArray(); - $info = $GetFileFormatArray['cue']; - $info['include'] = 'module.'.$info['group'].'.'.$info['module'].'.php'; - return $info; - } - - return false; - } - - - // converts array to $encoding charset from $this->encoding - public function CharConvert(&$array, $encoding) { - - // identical encoding - end here - if ($encoding == $this->encoding) { - return; - } - - // loop thru array - foreach ($array as $key => $value) { - - // go recursive - if (is_array($value)) { - $this->CharConvert($array[$key], $encoding); - } - - // convert string - elseif (is_string($value)) { - $array[$key] = trim(getid3_lib::iconv_fallback($encoding, $this->encoding, $value)); - } - } - } - - - public function HandleAllTags() { - - // key name => array (tag name, character encoding) - static $tags; - if (empty($tags)) { - $tags = array( - 'asf' => array('asf' , 'UTF-16LE'), - 'midi' => array('midi' , 'ISO-8859-1'), - 'nsv' => array('nsv' , 'ISO-8859-1'), - 'ogg' => array('vorbiscomment' , 'UTF-8'), - 'png' => array('png' , 'UTF-8'), - 'tiff' => array('tiff' , 'ISO-8859-1'), - 'quicktime' => array('quicktime' , 'UTF-8'), - 'real' => array('real' , 'ISO-8859-1'), - 'vqf' => array('vqf' , 'ISO-8859-1'), - 'zip' => array('zip' , 'ISO-8859-1'), - 'riff' => array('riff' , 'ISO-8859-1'), - 'lyrics3' => array('lyrics3' , 'ISO-8859-1'), - 'id3v1' => array('id3v1' , $this->encoding_id3v1), - 'id3v2' => array('id3v2' , 'UTF-8'), // not according to the specs (every frame can have a different encoding), but getID3() force-converts all encodings to UTF-8 - 'ape' => array('ape' , 'UTF-8'), - 'cue' => array('cue' , 'ISO-8859-1'), - 'matroska' => array('matroska' , 'UTF-8'), - 'flac' => array('vorbiscomment' , 'UTF-8'), - 'divxtag' => array('divx' , 'ISO-8859-1'), - ); - } - - // loop through comments array - foreach ($tags as $comment_name => $tagname_encoding_array) { - list($tag_name, $encoding) = $tagname_encoding_array; - - // fill in default encoding type if not already present - if (isset($this->info[$comment_name]) && !isset($this->info[$comment_name]['encoding'])) { - $this->info[$comment_name]['encoding'] = $encoding; - } - - // copy comments if key name set - if (!empty($this->info[$comment_name]['comments'])) { - foreach ($this->info[$comment_name]['comments'] as $tag_key => $valuearray) { - foreach ($valuearray as $key => $value) { - if (is_string($value)) { - $value = trim($value, " \r\n\t"); // do not trim nulls from $value!! Unicode characters will get mangled if trailing nulls are removed! - } - if ($value) { - $this->info['tags'][trim($tag_name)][trim($tag_key)][] = $value; - } - } - if ($tag_key == 'picture') { - unset($this->info[$comment_name]['comments'][$tag_key]); - } - } - - if (!isset($this->info['tags'][$tag_name])) { - // comments are set but contain nothing but empty strings, so skip - continue; - } - - if ($this->option_tags_html) { - foreach ($this->info['tags'][$tag_name] as $tag_key => $valuearray) { - foreach ($valuearray as $key => $value) { - if (is_string($value)) { - //$this->info['tags_html'][$tag_name][$tag_key][$key] = getid3_lib::MultiByteCharString2HTML($value, $encoding); - $this->info['tags_html'][$tag_name][$tag_key][$key] = str_replace('�', '', trim(getid3_lib::MultiByteCharString2HTML($value, $encoding))); - } else { - $this->info['tags_html'][$tag_name][$tag_key][$key] = $value; - } - } - } - } - - $this->CharConvert($this->info['tags'][$tag_name], $encoding); // only copy gets converted! - } - - } - - // pictures can take up a lot of space, and we don't need multiple copies of them - // let there be a single copy in [comments][picture], and not elsewhere - if (!empty($this->info['tags'])) { - $unset_keys = array('tags', 'tags_html'); - foreach ($this->info['tags'] as $tagtype => $tagarray) { - foreach ($tagarray as $tagname => $tagdata) { - if ($tagname == 'picture') { - foreach ($tagdata as $key => $tagarray) { - $this->info['comments']['picture'][] = $tagarray; - if (isset($tagarray['data']) && isset($tagarray['image_mime'])) { - if (isset($this->info['tags'][$tagtype][$tagname][$key])) { - unset($this->info['tags'][$tagtype][$tagname][$key]); - } - if (isset($this->info['tags_html'][$tagtype][$tagname][$key])) { - unset($this->info['tags_html'][$tagtype][$tagname][$key]); - } - } - } - } - } - foreach ($unset_keys as $unset_key) { - // remove possible empty keys from (e.g. [tags][id3v2][picture]) - if (empty($this->info[$unset_key][$tagtype]['picture'])) { - unset($this->info[$unset_key][$tagtype]['picture']); - } - if (empty($this->info[$unset_key][$tagtype])) { - unset($this->info[$unset_key][$tagtype]); - } - if (empty($this->info[$unset_key])) { - unset($this->info[$unset_key]); - } - } - // remove duplicate copy of picture data from (e.g. [id3v2][comments][picture]) - if (isset($this->info[$tagtype]['comments']['picture'])) { - unset($this->info[$tagtype]['comments']['picture']); - } - if (empty($this->info[$tagtype]['comments'])) { - unset($this->info[$tagtype]['comments']); - } - if (empty($this->info[$tagtype])) { - unset($this->info[$tagtype]); - } - } - } - return true; - } - - - public function getHashdata($algorithm) { - switch ($algorithm) { - case 'md5': - case 'sha1': - break; - - default: - return $this->error('bad algorithm "'.$algorithm.'" in getHashdata()'); - break; - } - - if (!empty($this->info['fileformat']) && !empty($this->info['dataformat']) && ($this->info['fileformat'] == 'ogg') && ($this->info['audio']['dataformat'] == 'vorbis')) { - - // We cannot get an identical md5_data value for Ogg files where the comments - // span more than 1 Ogg page (compared to the same audio data with smaller - // comments) using the normal getID3() method of MD5'ing the data between the - // end of the comments and the end of the file (minus any trailing tags), - // because the page sequence numbers of the pages that the audio data is on - // do not match. Under normal circumstances, where comments are smaller than - // the nominal 4-8kB page size, then this is not a problem, but if there are - // very large comments, the only way around it is to strip off the comment - // tags with vorbiscomment and MD5 that file. - // This procedure must be applied to ALL Ogg files, not just the ones with - // comments larger than 1 page, because the below method simply MD5's the - // whole file with the comments stripped, not just the portion after the - // comments block (which is the standard getID3() method. - - // The above-mentioned problem of comments spanning multiple pages and changing - // page sequence numbers likely happens for OggSpeex and OggFLAC as well, but - // currently vorbiscomment only works on OggVorbis files. - - if (preg_match('#(1|ON)#i', ini_get('safe_mode'))) { - - $this->warning('Failed making system call to vorbiscomment.exe - '.$algorithm.'_data is incorrect - error returned: PHP running in Safe Mode (backtick operator not available)'); - $this->info[$algorithm.'_data'] = false; - - } else { - - // Prevent user from aborting script - $old_abort = ignore_user_abort(true); - - // Create empty file - $empty = tempnam(GETID3_TEMP_DIR, 'getID3'); - touch($empty); - - // Use vorbiscomment to make temp file without comments - $temp = tempnam(GETID3_TEMP_DIR, 'getID3'); - $file = $this->info['filenamepath']; - - if (GETID3_OS_ISWINDOWS) { - - if (file_exists(GETID3_HELPERAPPSDIR.'vorbiscomment.exe')) { - - $commandline = '"'.GETID3_HELPERAPPSDIR.'vorbiscomment.exe" -w -c "'.$empty.'" "'.$file.'" "'.$temp.'"'; - $VorbisCommentError = `$commandline`; - - } else { - - $VorbisCommentError = 'vorbiscomment.exe not found in '.GETID3_HELPERAPPSDIR; - - } - - } else { - - $commandline = 'vorbiscomment -w -c "'.$empty.'" "'.$file.'" "'.$temp.'" 2>&1'; - $commandline = 'vorbiscomment -w -c '.escapeshellarg($empty).' '.escapeshellarg($file).' '.escapeshellarg($temp).' 2>&1'; - $VorbisCommentError = `$commandline`; - - } - - if (!empty($VorbisCommentError)) { - - $this->info['warning'][] = 'Failed making system call to vorbiscomment(.exe) - '.$algorithm.'_data will be incorrect. If vorbiscomment is unavailable, please download from http://www.vorbis.com/download.psp and put in the getID3() directory. Error returned: '.$VorbisCommentError; - $this->info[$algorithm.'_data'] = false; - - } else { - - // Get hash of newly created file - switch ($algorithm) { - case 'md5': - $this->info[$algorithm.'_data'] = md5_file($temp); - break; - - case 'sha1': - $this->info[$algorithm.'_data'] = sha1_file($temp); - break; - } - } - - // Clean up - unlink($empty); - unlink($temp); - - // Reset abort setting - ignore_user_abort($old_abort); - - } - - } else { - - if (!empty($this->info['avdataoffset']) || (isset($this->info['avdataend']) && ($this->info['avdataend'] < $this->info['filesize']))) { - - // get hash from part of file - $this->info[$algorithm.'_data'] = getid3_lib::hash_data($this->info['filenamepath'], $this->info['avdataoffset'], $this->info['avdataend'], $algorithm); - - } else { - - // get hash from whole file - switch ($algorithm) { - case 'md5': - $this->info[$algorithm.'_data'] = md5_file($this->info['filenamepath']); - break; - - case 'sha1': - $this->info[$algorithm.'_data'] = sha1_file($this->info['filenamepath']); - break; - } - } - - } - return true; - } - - - public function ChannelsBitratePlaytimeCalculations() { - - // set channelmode on audio - if (!empty($this->info['audio']['channelmode']) || !isset($this->info['audio']['channels'])) { - // ignore - } elseif ($this->info['audio']['channels'] == 1) { - $this->info['audio']['channelmode'] = 'mono'; - } elseif ($this->info['audio']['channels'] == 2) { - $this->info['audio']['channelmode'] = 'stereo'; - } - - // Calculate combined bitrate - audio + video - $CombinedBitrate = 0; - $CombinedBitrate += (isset($this->info['audio']['bitrate']) ? $this->info['audio']['bitrate'] : 0); - $CombinedBitrate += (isset($this->info['video']['bitrate']) ? $this->info['video']['bitrate'] : 0); - if (($CombinedBitrate > 0) && empty($this->info['bitrate'])) { - $this->info['bitrate'] = $CombinedBitrate; - } - //if ((isset($this->info['video']) && !isset($this->info['video']['bitrate'])) || (isset($this->info['audio']) && !isset($this->info['audio']['bitrate']))) { - // // for example, VBR MPEG video files cannot determine video bitrate: - // // should not set overall bitrate and playtime from audio bitrate only - // unset($this->info['bitrate']); - //} - - // video bitrate undetermined, but calculable - if (isset($this->info['video']['dataformat']) && $this->info['video']['dataformat'] && (!isset($this->info['video']['bitrate']) || ($this->info['video']['bitrate'] == 0))) { - // if video bitrate not set - if (isset($this->info['audio']['bitrate']) && ($this->info['audio']['bitrate'] > 0) && ($this->info['audio']['bitrate'] == $this->info['bitrate'])) { - // AND if audio bitrate is set to same as overall bitrate - if (isset($this->info['playtime_seconds']) && ($this->info['playtime_seconds'] > 0)) { - // AND if playtime is set - if (isset($this->info['avdataend']) && isset($this->info['avdataoffset'])) { - // AND if AV data offset start/end is known - // THEN we can calculate the video bitrate - $this->info['bitrate'] = round((($this->info['avdataend'] - $this->info['avdataoffset']) * 8) / $this->info['playtime_seconds']); - $this->info['video']['bitrate'] = $this->info['bitrate'] - $this->info['audio']['bitrate']; - } - } - } - } - - if ((!isset($this->info['playtime_seconds']) || ($this->info['playtime_seconds'] <= 0)) && !empty($this->info['bitrate'])) { - $this->info['playtime_seconds'] = (($this->info['avdataend'] - $this->info['avdataoffset']) * 8) / $this->info['bitrate']; - } - - if (!isset($this->info['bitrate']) && !empty($this->info['playtime_seconds'])) { - $this->info['bitrate'] = (($this->info['avdataend'] - $this->info['avdataoffset']) * 8) / $this->info['playtime_seconds']; - } - if (isset($this->info['bitrate']) && empty($this->info['audio']['bitrate']) && empty($this->info['video']['bitrate'])) { - if (isset($this->info['audio']['dataformat']) && empty($this->info['video']['resolution_x'])) { - // audio only - $this->info['audio']['bitrate'] = $this->info['bitrate']; - } elseif (isset($this->info['video']['resolution_x']) && empty($this->info['audio']['dataformat'])) { - // video only - $this->info['video']['bitrate'] = $this->info['bitrate']; - } - } - - // Set playtime string - if (!empty($this->info['playtime_seconds']) && empty($this->info['playtime_string'])) { - $this->info['playtime_string'] = getid3_lib::PlaytimeString($this->info['playtime_seconds']); - } - } - - - public function CalculateCompressionRatioVideo() { - if (empty($this->info['video'])) { - return false; - } - if (empty($this->info['video']['resolution_x']) || empty($this->info['video']['resolution_y'])) { - return false; - } - if (empty($this->info['video']['bits_per_sample'])) { - return false; - } - - switch ($this->info['video']['dataformat']) { - case 'bmp': - case 'gif': - case 'jpeg': - case 'jpg': - case 'png': - case 'tiff': - $FrameRate = 1; - $PlaytimeSeconds = 1; - $BitrateCompressed = $this->info['filesize'] * 8; - break; - - default: - if (!empty($this->info['video']['frame_rate'])) { - $FrameRate = $this->info['video']['frame_rate']; - } else { - return false; - } - if (!empty($this->info['playtime_seconds'])) { - $PlaytimeSeconds = $this->info['playtime_seconds']; - } else { - return false; - } - if (!empty($this->info['video']['bitrate'])) { - $BitrateCompressed = $this->info['video']['bitrate']; - } else { - return false; - } - break; - } - $BitrateUncompressed = $this->info['video']['resolution_x'] * $this->info['video']['resolution_y'] * $this->info['video']['bits_per_sample'] * $FrameRate; - - $this->info['video']['compression_ratio'] = $BitrateCompressed / $BitrateUncompressed; - return true; - } - - - public function CalculateCompressionRatioAudio() { - if (empty($this->info['audio']['bitrate']) || empty($this->info['audio']['channels']) || empty($this->info['audio']['sample_rate']) || !is_numeric($this->info['audio']['sample_rate'])) { - return false; - } - $this->info['audio']['compression_ratio'] = $this->info['audio']['bitrate'] / ($this->info['audio']['channels'] * $this->info['audio']['sample_rate'] * (!empty($this->info['audio']['bits_per_sample']) ? $this->info['audio']['bits_per_sample'] : 16)); - - if (!empty($this->info['audio']['streams'])) { - foreach ($this->info['audio']['streams'] as $streamnumber => $streamdata) { - if (!empty($streamdata['bitrate']) && !empty($streamdata['channels']) && !empty($streamdata['sample_rate'])) { - $this->info['audio']['streams'][$streamnumber]['compression_ratio'] = $streamdata['bitrate'] / ($streamdata['channels'] * $streamdata['sample_rate'] * (!empty($streamdata['bits_per_sample']) ? $streamdata['bits_per_sample'] : 16)); - } - } - } - return true; - } - - - public function CalculateReplayGain() { - if (isset($this->info['replay_gain'])) { - if (!isset($this->info['replay_gain']['reference_volume'])) { - $this->info['replay_gain']['reference_volume'] = (double) 89.0; - } - if (isset($this->info['replay_gain']['track']['adjustment'])) { - $this->info['replay_gain']['track']['volume'] = $this->info['replay_gain']['reference_volume'] - $this->info['replay_gain']['track']['adjustment']; - } - if (isset($this->info['replay_gain']['album']['adjustment'])) { - $this->info['replay_gain']['album']['volume'] = $this->info['replay_gain']['reference_volume'] - $this->info['replay_gain']['album']['adjustment']; - } - - if (isset($this->info['replay_gain']['track']['peak'])) { - $this->info['replay_gain']['track']['max_noclip_gain'] = 0 - getid3_lib::RGADamplitude2dB($this->info['replay_gain']['track']['peak']); - } - if (isset($this->info['replay_gain']['album']['peak'])) { - $this->info['replay_gain']['album']['max_noclip_gain'] = 0 - getid3_lib::RGADamplitude2dB($this->info['replay_gain']['album']['peak']); - } - } - return true; - } - - public function ProcessAudioStreams() { - if (!empty($this->info['audio']['bitrate']) || !empty($this->info['audio']['channels']) || !empty($this->info['audio']['sample_rate'])) { - if (!isset($this->info['audio']['streams'])) { - foreach ($this->info['audio'] as $key => $value) { - if ($key != 'streams') { - $this->info['audio']['streams'][0][$key] = $value; - } - } - } - } - return true; - } - - public function getid3_tempnam() { - return tempnam($this->tempdir, 'gI3'); - } - - public function include_module($name) { - //if (!file_exists($this->include_path.'module.'.$name.'.php')) { - if (!file_exists(GETID3_INCLUDEPATH.'module.'.$name.'.php')) { - throw new getid3_exception('Required module.'.$name.'.php is missing.'); - } - include_once(GETID3_INCLUDEPATH.'module.'.$name.'.php'); - return true; - } - -} - - -abstract class getid3_handler -{ - protected $getid3; // pointer - - protected $data_string_flag = false; // analyzing filepointer or string - protected $data_string = ''; // string to analyze - protected $data_string_position = 0; // seek position in string - protected $data_string_length = 0; // string length - - private $dependency_to = null; - - - public function __construct(getID3 $getid3, $call_module=null) { - $this->getid3 = $getid3; - - if ($call_module) { - $this->dependency_to = str_replace('getid3_', '', $call_module); - } - } - - - // Analyze from file pointer - abstract public function Analyze(); - - - // Analyze from string instead - public function AnalyzeString($string) { - // Enter string mode - $this->setStringMode($string); - - // Save info - $saved_avdataoffset = $this->getid3->info['avdataoffset']; - $saved_avdataend = $this->getid3->info['avdataend']; - $saved_filesize = (isset($this->getid3->info['filesize']) ? $this->getid3->info['filesize'] : null); // may be not set if called as dependency without openfile() call - - // Reset some info - $this->getid3->info['avdataoffset'] = 0; - $this->getid3->info['avdataend'] = $this->getid3->info['filesize'] = $this->data_string_length; - - // Analyze - $this->Analyze(); - - // Restore some info - $this->getid3->info['avdataoffset'] = $saved_avdataoffset; - $this->getid3->info['avdataend'] = $saved_avdataend; - $this->getid3->info['filesize'] = $saved_filesize; - - // Exit string mode - $this->data_string_flag = false; - } - - public function setStringMode($string) { - $this->data_string_flag = true; - $this->data_string = $string; - $this->data_string_length = strlen($string); - } - - protected function ftell() { - if ($this->data_string_flag) { - return $this->data_string_position; - } - return ftell($this->getid3->fp); - } - - protected function fread($bytes) { - if ($this->data_string_flag) { - $this->data_string_position += $bytes; - return substr($this->data_string, $this->data_string_position - $bytes, $bytes); - } - $pos = $this->ftell() + $bytes; - if (!getid3_lib::intValueSupported($pos)) { - throw new getid3_exception('cannot fread('.$bytes.' from '.$this->ftell().') because beyond PHP filesystem limit', 10); - } - return fread($this->getid3->fp, $bytes); - } - - protected function fseek($bytes, $whence=SEEK_SET) { - if ($this->data_string_flag) { - switch ($whence) { - case SEEK_SET: - $this->data_string_position = $bytes; - break; - - case SEEK_CUR: - $this->data_string_position += $bytes; - break; - - case SEEK_END: - $this->data_string_position = $this->data_string_length + $bytes; - break; - } - return 0; - } else { - $pos = $bytes; - if ($whence == SEEK_CUR) { - $pos = $this->ftell() + $bytes; - } elseif ($whence == SEEK_END) { - $pos = $this->info['filesize'] + $bytes; - } - if (!getid3_lib::intValueSupported($pos)) { - throw new getid3_exception('cannot fseek('.$pos.') because beyond PHP filesystem limit', 10); - } - } - return fseek($this->getid3->fp, $bytes, $whence); - } - - protected function feof() { - if ($this->data_string_flag) { - return $this->data_string_position >= $this->data_string_length; - } - return feof($this->getid3->fp); - } - - final protected function isDependencyFor($module) { - return $this->dependency_to == $module; - } - - protected function error($text) - { - $this->getid3->info['error'][] = $text; - - return false; - } - - protected function warning($text) - { - return $this->getid3->warning($text); - } - - protected function notice($text) - { - // does nothing for now - } - - public function saveAttachment($name, $offset, $length, $image_mime=null) { - try { - - // do not extract at all - if ($this->getid3->option_save_attachments === getID3::ATTACHMENTS_NONE) { - - $attachment = null; // do not set any - - // extract to return array - } elseif ($this->getid3->option_save_attachments === getID3::ATTACHMENTS_INLINE) { - - $this->fseek($offset); - $attachment = $this->fread($length); // get whole data in one pass, till it is anyway stored in memory - if ($attachment === false || strlen($attachment) != $length) { - throw new Exception('failed to read attachment data'); - } - - // assume directory path is given - } else { - - // set up destination path - $dir = rtrim(str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->getid3->option_save_attachments), DIRECTORY_SEPARATOR); - if (!is_dir($dir) || !is_writable($dir)) { // check supplied directory - throw new Exception('supplied path ('.$dir.') does not exist, or is not writable'); - } - $dest = $dir.DIRECTORY_SEPARATOR.$name.($image_mime ? '.'.getid3_lib::ImageExtFromMime($image_mime) : ''); - - // create dest file - if (($fp_dest = fopen($dest, 'wb')) == false) { - throw new Exception('failed to create file '.$dest); - } - - // copy data - $this->fseek($offset); - $buffersize = ($this->data_string_flag ? $length : $this->getid3->fread_buffer_size()); - $bytesleft = $length; - while ($bytesleft > 0) { - if (($buffer = $this->fread(min($buffersize, $bytesleft))) === false || ($byteswritten = fwrite($fp_dest, $buffer)) === false || ($byteswritten === 0)) { - throw new Exception($buffer === false ? 'not enough data to read' : 'failed to write to destination file, may be not enough disk space'); - } - $bytesleft -= $byteswritten; - } - - fclose($fp_dest); - $attachment = $dest; - - } - - } catch (Exception $e) { - - // close and remove dest file if created - if (isset($fp_dest) && is_resource($fp_dest)) { - fclose($fp_dest); - unlink($dest); - } - - // do not set any is case of error - $attachment = null; - $this->warning('Failed to extract attachment '.$name.': '.$e->getMessage()); - - } - - // seek to the end of attachment - $this->fseek($offset + $length); - - return $attachment; - } - -} - - -class getid3_exception extends Exception -{ - public $message; -} diff --git a/src/Classes/Vendor/getid3/module.archive.gzip.php b/src/Classes/Vendor/getid3/module.archive.gzip.php deleted file mode 100755 index 182351f99..000000000 --- a/src/Classes/Vendor/getid3/module.archive.gzip.php +++ /dev/null @@ -1,280 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.archive.gzip.php // -// module for analyzing GZIP files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// -// // -// Module originally written by // -// Mike Mozolin // -// // -///////////////////////////////////////////////////////////////// - - -class getid3_gzip extends getid3_handler { - - // public: Optional file list - disable for speed. - public $option_gzip_parse_contents = false; // decode gzipped files, if possible, and parse recursively (.tar.gz for example) - - public function Analyze() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'gzip'; - - $start_length = 10; - $unpack_header = 'a1id1/a1id2/a1cmethod/a1flags/a4mtime/a1xflags/a1os'; - //+---+---+---+---+---+---+---+---+---+---+ - //|ID1|ID2|CM |FLG| MTIME |XFL|OS | - //+---+---+---+---+---+---+---+---+---+---+ - - if ($info['filesize'] > $info['php_memory_limit']) { - $info['error'][] = 'File is too large ('.number_format($info['filesize']).' bytes) to read into memory (limit: '.number_format($info['php_memory_limit'] / 1048576).'MB)'; - return false; - } - fseek($this->getid3->fp, 0); - $buffer = fread($this->getid3->fp, $info['filesize']); - - $arr_members = explode("\x1F\x8B\x08", $buffer); - while (true) { - $is_wrong_members = false; - $num_members = intval(count($arr_members)); - for ($i = 0; $i < $num_members; $i++) { - if (strlen($arr_members[$i]) == 0) { - continue; - } - $buf = "\x1F\x8B\x08".$arr_members[$i]; - - $attr = unpack($unpack_header, substr($buf, 0, $start_length)); - if (!$this->get_os_type(ord($attr['os']))) { - // Merge member with previous if wrong OS type - $arr_members[$i - 1] .= $buf; - $arr_members[$i] = ''; - $is_wrong_members = true; - continue; - } - } - if (!$is_wrong_members) { - break; - } - } - - $info['gzip']['files'] = array(); - - $fpointer = 0; - $idx = 0; - for ($i = 0; $i < $num_members; $i++) { - if (strlen($arr_members[$i]) == 0) { - continue; - } - $thisInfo = &$info['gzip']['member_header'][++$idx]; - - $buff = "\x1F\x8B\x08".$arr_members[$i]; - - $attr = unpack($unpack_header, substr($buff, 0, $start_length)); - $thisInfo['filemtime'] = getid3_lib::LittleEndian2Int($attr['mtime']); - $thisInfo['raw']['id1'] = ord($attr['cmethod']); - $thisInfo['raw']['id2'] = ord($attr['cmethod']); - $thisInfo['raw']['cmethod'] = ord($attr['cmethod']); - $thisInfo['raw']['os'] = ord($attr['os']); - $thisInfo['raw']['xflags'] = ord($attr['xflags']); - $thisInfo['raw']['flags'] = ord($attr['flags']); - - $thisInfo['flags']['crc16'] = (bool) ($thisInfo['raw']['flags'] & 0x02); - $thisInfo['flags']['extra'] = (bool) ($thisInfo['raw']['flags'] & 0x04); - $thisInfo['flags']['filename'] = (bool) ($thisInfo['raw']['flags'] & 0x08); - $thisInfo['flags']['comment'] = (bool) ($thisInfo['raw']['flags'] & 0x10); - - $thisInfo['compression'] = $this->get_xflag_type($thisInfo['raw']['xflags']); - - $thisInfo['os'] = $this->get_os_type($thisInfo['raw']['os']); - if (!$thisInfo['os']) { - $info['error'][] = 'Read error on gzip file'; - return false; - } - - $fpointer = 10; - $arr_xsubfield = array(); - // bit 2 - FLG.FEXTRA - //+---+---+=================================+ - //| XLEN |...XLEN bytes of "extra field"...| - //+---+---+=================================+ - if ($thisInfo['flags']['extra']) { - $w_xlen = substr($buff, $fpointer, 2); - $xlen = getid3_lib::LittleEndian2Int($w_xlen); - $fpointer += 2; - - $thisInfo['raw']['xfield'] = substr($buff, $fpointer, $xlen); - // Extra SubFields - //+---+---+---+---+==================================+ - //|SI1|SI2| LEN |... LEN bytes of subfield data ...| - //+---+---+---+---+==================================+ - $idx = 0; - while (true) { - if ($idx >= $xlen) { - break; - } - $si1 = ord(substr($buff, $fpointer + $idx++, 1)); - $si2 = ord(substr($buff, $fpointer + $idx++, 1)); - if (($si1 == 0x41) && ($si2 == 0x70)) { - $w_xsublen = substr($buff, $fpointer + $idx, 2); - $xsublen = getid3_lib::LittleEndian2Int($w_xsublen); - $idx += 2; - $arr_xsubfield[] = substr($buff, $fpointer + $idx, $xsublen); - $idx += $xsublen; - } else { - break; - } - } - $fpointer += $xlen; - } - // bit 3 - FLG.FNAME - //+=========================================+ - //|...original file name, zero-terminated...| - //+=========================================+ - // GZIP files may have only one file, with no filename, so assume original filename is current filename without .gz - $thisInfo['filename'] = preg_replace('#\\.gz$#i', '', $info['filename']); - if ($thisInfo['flags']['filename']) { - $thisInfo['filename'] = ''; - while (true) { - if (ord($buff[$fpointer]) == 0) { - $fpointer++; - break; - } - $thisInfo['filename'] .= $buff[$fpointer]; - $fpointer++; - } - } - // bit 4 - FLG.FCOMMENT - //+===================================+ - //|...file comment, zero-terminated...| - //+===================================+ - if ($thisInfo['flags']['comment']) { - while (true) { - if (ord($buff[$fpointer]) == 0) { - $fpointer++; - break; - } - $thisInfo['comment'] .= $buff[$fpointer]; - $fpointer++; - } - } - // bit 1 - FLG.FHCRC - //+---+---+ - //| CRC16 | - //+---+---+ - if ($thisInfo['flags']['crc16']) { - $w_crc = substr($buff, $fpointer, 2); - $thisInfo['crc16'] = getid3_lib::LittleEndian2Int($w_crc); - $fpointer += 2; - } - // bit 0 - FLG.FTEXT - //if ($thisInfo['raw']['flags'] & 0x01) { - // Ignored... - //} - // bits 5, 6, 7 - reserved - - $thisInfo['crc32'] = getid3_lib::LittleEndian2Int(substr($buff, strlen($buff) - 8, 4)); - $thisInfo['filesize'] = getid3_lib::LittleEndian2Int(substr($buff, strlen($buff) - 4)); - - $info['gzip']['files'] = getid3_lib::array_merge_clobber($info['gzip']['files'], getid3_lib::CreateDeepArray($thisInfo['filename'], '/', $thisInfo['filesize'])); - - if ($this->option_gzip_parse_contents) { - // Try to inflate GZip - $csize = 0; - $inflated = ''; - $chkcrc32 = ''; - if (function_exists('gzinflate')) { - $cdata = substr($buff, $fpointer); - $cdata = substr($cdata, 0, strlen($cdata) - 8); - $csize = strlen($cdata); - $inflated = gzinflate($cdata); - - // Calculate CRC32 for inflated content - $thisInfo['crc32_valid'] = (bool) (sprintf('%u', crc32($inflated)) == $thisInfo['crc32']); - - // determine format - $formattest = substr($inflated, 0, 32774); - $getid3_temp = new getID3(); - $determined_format = $getid3_temp->GetFileFormat($formattest); - unset($getid3_temp); - - // file format is determined - $determined_format['module'] = (isset($determined_format['module']) ? $determined_format['module'] : ''); - switch ($determined_format['module']) { - case 'tar': - // view TAR-file info - if (file_exists(GETID3_INCLUDEPATH.$determined_format['include']) && include_once(GETID3_INCLUDEPATH.$determined_format['include'])) { - if (($temp_tar_filename = tempnam(GETID3_TEMP_DIR, 'getID3')) === false) { - // can't find anywhere to create a temp file, abort - $info['error'][] = 'Unable to create temp file to parse TAR inside GZIP file'; - break; - } - if ($fp_temp_tar = fopen($temp_tar_filename, 'w+b')) { - fwrite($fp_temp_tar, $inflated); - fclose($fp_temp_tar); - $getid3_temp = new getID3(); - $getid3_temp->openfile($temp_tar_filename); - $getid3_tar = new getid3_tar($getid3_temp); - $getid3_tar->Analyze(); - $info['gzip']['member_header'][$idx]['tar'] = $getid3_temp->info['tar']; - unset($getid3_temp, $getid3_tar); - unlink($temp_tar_filename); - } else { - $info['error'][] = 'Unable to fopen() temp file to parse TAR inside GZIP file'; - break; - } - } - break; - - case '': - default: - // unknown or unhandled format - break; - } - } - } - } - return true; - } - - // Converts the OS type - public function get_os_type($key) { - static $os_type = array( - '0' => 'FAT filesystem (MS-DOS, OS/2, NT/Win32)', - '1' => 'Amiga', - '2' => 'VMS (or OpenVMS)', - '3' => 'Unix', - '4' => 'VM/CMS', - '5' => 'Atari TOS', - '6' => 'HPFS filesystem (OS/2, NT)', - '7' => 'Macintosh', - '8' => 'Z-System', - '9' => 'CP/M', - '10' => 'TOPS-20', - '11' => 'NTFS filesystem (NT)', - '12' => 'QDOS', - '13' => 'Acorn RISCOS', - '255' => 'unknown' - ); - return (isset($os_type[$key]) ? $os_type[$key] : ''); - } - - // Converts the eXtra FLags - public function get_xflag_type($key) { - static $xflag_type = array( - '0' => 'unknown', - '2' => 'maximum compression', - '4' => 'fastest algorithm' - ); - return (isset($xflag_type[$key]) ? $xflag_type[$key] : ''); - } -} - diff --git a/src/Classes/Vendor/getid3/module.archive.rar.php b/src/Classes/Vendor/getid3/module.archive.rar.php deleted file mode 100755 index e3155e40f..000000000 --- a/src/Classes/Vendor/getid3/module.archive.rar.php +++ /dev/null @@ -1,50 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.archive.rar.php // -// module for analyzing RAR files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_rar extends getid3_handler -{ - - public $option_use_rar_extension = false; - - public function Analyze() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'rar'; - - if ($this->option_use_rar_extension === true) { - if (function_exists('rar_open')) { - if ($rp = rar_open($info['filenamepath'])) { - $info['rar']['files'] = array(); - $entries = rar_list($rp); - foreach ($entries as $entry) { - $info['rar']['files'] = getid3_lib::array_merge_clobber($info['rar']['files'], getid3_lib::CreateDeepArray($entry->getName(), '/', $entry->getUnpackedSize())); - } - rar_close($rp); - return true; - } else { - $info['error'][] = 'failed to rar_open('.$info['filename'].')'; - } - } else { - $info['error'][] = 'RAR support does not appear to be available in this PHP installation'; - } - } else { - $info['error'][] = 'PHP-RAR processing has been disabled (set $getid3_rar->option_use_rar_extension=true to enable)'; - } - return false; - - } - -} diff --git a/src/Classes/Vendor/getid3/module.archive.szip.php b/src/Classes/Vendor/getid3/module.archive.szip.php deleted file mode 100755 index 6f41d2f37..000000000 --- a/src/Classes/Vendor/getid3/module.archive.szip.php +++ /dev/null @@ -1,96 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.archive.szip.php // -// module for analyzing SZIP compressed files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_szip extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - $this->fseek($info['avdataoffset']); - $SZIPHeader = $this->fread(6); - if (substr($SZIPHeader, 0, 4) != "SZ\x0A\x04") { - $info['error'][] = 'Expecting "53 5A 0A 04" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($SZIPHeader, 0, 4)).'"'; - return false; - } - $info['fileformat'] = 'szip'; - $info['szip']['major_version'] = getid3_lib::BigEndian2Int(substr($SZIPHeader, 4, 1)); - $info['szip']['minor_version'] = getid3_lib::BigEndian2Int(substr($SZIPHeader, 5, 1)); -$info['error'][] = 'SZIP parsing not enabled in this version of getID3() ['.$this->getid3->version().']'; -return false; - - while (!$this->feof()) { - $NextBlockID = $this->fread(2); - switch ($NextBlockID) { - case 'SZ': - // Note that szip files can be concatenated, this has the same effect as - // concatenating the files. this also means that global header blocks - // might be present between directory/data blocks. - $this->fseek(4, SEEK_CUR); - break; - - case 'BH': - $BHheaderbytes = getid3_lib::BigEndian2Int($this->fread(3)); - $BHheaderdata = $this->fread($BHheaderbytes); - $BHheaderoffset = 0; - while (strpos($BHheaderdata, "\x00", $BHheaderoffset) > 0) { - //filename as \0 terminated string (empty string indicates end) - //owner as \0 terminated string (empty is same as last file) - //group as \0 terminated string (empty is same as last file) - //3 byte filelength in this block - //2 byte access flags - //4 byte creation time (like in unix) - //4 byte modification time (like in unix) - //4 byte access time (like in unix) - - $BHdataArray['filename'] = substr($BHheaderdata, $BHheaderoffset, strcspn($BHheaderdata, "\x00")); - $BHheaderoffset += (strlen($BHdataArray['filename']) + 1); - - $BHdataArray['owner'] = substr($BHheaderdata, $BHheaderoffset, strcspn($BHheaderdata, "\x00")); - $BHheaderoffset += (strlen($BHdataArray['owner']) + 1); - - $BHdataArray['group'] = substr($BHheaderdata, $BHheaderoffset, strcspn($BHheaderdata, "\x00")); - $BHheaderoffset += (strlen($BHdataArray['group']) + 1); - - $BHdataArray['filelength'] = getid3_lib::BigEndian2Int(substr($BHheaderdata, $BHheaderoffset, 3)); - $BHheaderoffset += 3; - - $BHdataArray['access_flags'] = getid3_lib::BigEndian2Int(substr($BHheaderdata, $BHheaderoffset, 2)); - $BHheaderoffset += 2; - - $BHdataArray['creation_time'] = getid3_lib::BigEndian2Int(substr($BHheaderdata, $BHheaderoffset, 4)); - $BHheaderoffset += 4; - - $BHdataArray['modification_time'] = getid3_lib::BigEndian2Int(substr($BHheaderdata, $BHheaderoffset, 4)); - $BHheaderoffset += 4; - - $BHdataArray['access_time'] = getid3_lib::BigEndian2Int(substr($BHheaderdata, $BHheaderoffset, 4)); - $BHheaderoffset += 4; - - $info['szip']['BH'][] = $BHdataArray; - } - break; - - default: - break 2; - } - } - - return true; - - } - -} diff --git a/src/Classes/Vendor/getid3/module.archive.tar.php b/src/Classes/Vendor/getid3/module.archive.tar.php deleted file mode 100755 index ef4672ca1..000000000 --- a/src/Classes/Vendor/getid3/module.archive.tar.php +++ /dev/null @@ -1,176 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.archive.tar.php // -// module for analyzing TAR files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// -// // -// Module originally written by // -// Mike Mozolin // -// // -///////////////////////////////////////////////////////////////// - - -class getid3_tar extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'tar'; - $info['tar']['files'] = array(); - - $unpack_header = 'a100fname/a8mode/a8uid/a8gid/a12size/a12mtime/a8chksum/a1typflag/a100lnkname/a6magic/a2ver/a32uname/a32gname/a8devmaj/a8devmin/a155prefix'; - $null_512k = str_repeat("\x00", 512); // end-of-file marker - - fseek($this->getid3->fp, 0); - while (!feof($this->getid3->fp)) { - $buffer = fread($this->getid3->fp, 512); - if (strlen($buffer) < 512) { - break; - } - - // check the block - $checksum = 0; - for ($i = 0; $i < 148; $i++) { - $checksum += ord($buffer{$i}); - } - for ($i = 148; $i < 156; $i++) { - $checksum += ord(' '); - } - for ($i = 156; $i < 512; $i++) { - $checksum += ord($buffer{$i}); - } - $attr = unpack($unpack_header, $buffer); - $name = (isset($attr['fname'] ) ? trim($attr['fname'] ) : ''); - $mode = octdec(isset($attr['mode'] ) ? trim($attr['mode'] ) : ''); - $uid = octdec(isset($attr['uid'] ) ? trim($attr['uid'] ) : ''); - $gid = octdec(isset($attr['gid'] ) ? trim($attr['gid'] ) : ''); - $size = octdec(isset($attr['size'] ) ? trim($attr['size'] ) : ''); - $mtime = octdec(isset($attr['mtime'] ) ? trim($attr['mtime'] ) : ''); - $chksum = octdec(isset($attr['chksum'] ) ? trim($attr['chksum'] ) : ''); - $typflag = (isset($attr['typflag']) ? trim($attr['typflag']) : ''); - $lnkname = (isset($attr['lnkname']) ? trim($attr['lnkname']) : ''); - $magic = (isset($attr['magic'] ) ? trim($attr['magic'] ) : ''); - $ver = (isset($attr['ver'] ) ? trim($attr['ver'] ) : ''); - $uname = (isset($attr['uname'] ) ? trim($attr['uname'] ) : ''); - $gname = (isset($attr['gname'] ) ? trim($attr['gname'] ) : ''); - $devmaj = octdec(isset($attr['devmaj'] ) ? trim($attr['devmaj'] ) : ''); - $devmin = octdec(isset($attr['devmin'] ) ? trim($attr['devmin'] ) : ''); - $prefix = (isset($attr['prefix'] ) ? trim($attr['prefix'] ) : ''); - if (($checksum == 256) && ($chksum == 0)) { - // EOF Found - break; - } - if ($prefix) { - $name = $prefix.'/'.$name; - } - if ((preg_match('#/$#', $name)) && !$name) { - $typeflag = 5; - } - if ($buffer == $null_512k) { - // it's the end of the tar-file... - break; - } - - // Read to the next chunk - fseek($this->getid3->fp, $size, SEEK_CUR); - - $diff = $size % 512; - if ($diff != 0) { - // Padding, throw away - fseek($this->getid3->fp, (512 - $diff), SEEK_CUR); - } - // Protect against tar-files with garbage at the end - if ($name == '') { - break; - } - $info['tar']['file_details'][$name] = array ( - 'name' => $name, - 'mode_raw' => $mode, - 'mode' => self::display_perms($mode), - 'uid' => $uid, - 'gid' => $gid, - 'size' => $size, - 'mtime' => $mtime, - 'chksum' => $chksum, - 'typeflag' => self::get_flag_type($typflag), - 'linkname' => $lnkname, - 'magic' => $magic, - 'version' => $ver, - 'uname' => $uname, - 'gname' => $gname, - 'devmajor' => $devmaj, - 'devminor' => $devmin - ); - $info['tar']['files'] = getid3_lib::array_merge_clobber($info['tar']['files'], getid3_lib::CreateDeepArray($info['tar']['file_details'][$name]['name'], '/', $size)); - } - return true; - } - - // Parses the file mode to file permissions - public function display_perms($mode) { - // Determine Type - if ($mode & 0x1000) $type='p'; // FIFO pipe - elseif ($mode & 0x2000) $type='c'; // Character special - elseif ($mode & 0x4000) $type='d'; // Directory - elseif ($mode & 0x6000) $type='b'; // Block special - elseif ($mode & 0x8000) $type='-'; // Regular - elseif ($mode & 0xA000) $type='l'; // Symbolic Link - elseif ($mode & 0xC000) $type='s'; // Socket - else $type='u'; // UNKNOWN - - // Determine permissions - $owner['read'] = (($mode & 00400) ? 'r' : '-'); - $owner['write'] = (($mode & 00200) ? 'w' : '-'); - $owner['execute'] = (($mode & 00100) ? 'x' : '-'); - $group['read'] = (($mode & 00040) ? 'r' : '-'); - $group['write'] = (($mode & 00020) ? 'w' : '-'); - $group['execute'] = (($mode & 00010) ? 'x' : '-'); - $world['read'] = (($mode & 00004) ? 'r' : '-'); - $world['write'] = (($mode & 00002) ? 'w' : '-'); - $world['execute'] = (($mode & 00001) ? 'x' : '-'); - - // Adjust for SUID, SGID and sticky bit - if ($mode & 0x800) $owner['execute'] = ($owner['execute'] == 'x') ? 's' : 'S'; - if ($mode & 0x400) $group['execute'] = ($group['execute'] == 'x') ? 's' : 'S'; - if ($mode & 0x200) $world['execute'] = ($world['execute'] == 'x') ? 't' : 'T'; - - $s = sprintf('%1s', $type); - $s .= sprintf('%1s%1s%1s', $owner['read'], $owner['write'], $owner['execute']); - $s .= sprintf('%1s%1s%1s', $group['read'], $group['write'], $group['execute']); - $s .= sprintf('%1s%1s%1s'."\n", $world['read'], $world['write'], $world['execute']); - return $s; - } - - // Converts the file type - public function get_flag_type($typflag) { - static $flag_types = array( - '0' => 'LF_NORMAL', - '1' => 'LF_LINK', - '2' => 'LF_SYNLINK', - '3' => 'LF_CHR', - '4' => 'LF_BLK', - '5' => 'LF_DIR', - '6' => 'LF_FIFO', - '7' => 'LF_CONFIG', - 'D' => 'LF_DUMPDIR', - 'K' => 'LF_LONGLINK', - 'L' => 'LF_LONGNAME', - 'M' => 'LF_MULTIVOL', - 'N' => 'LF_NAMES', - 'S' => 'LF_SPARSE', - 'V' => 'LF_VOLHDR' - ); - return (isset($flag_types[$typflag]) ? $flag_types[$typflag] : ''); - } - -} diff --git a/src/Classes/Vendor/getid3/module.archive.zip.php b/src/Classes/Vendor/getid3/module.archive.zip.php deleted file mode 100755 index 296a7490e..000000000 --- a/src/Classes/Vendor/getid3/module.archive.zip.php +++ /dev/null @@ -1,512 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.archive.zip.php // -// module for analyzing pkZip files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_zip extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'zip'; - $info['zip']['encoding'] = 'ISO-8859-1'; - $info['zip']['files'] = array(); - - $info['zip']['compressed_size'] = 0; - $info['zip']['uncompressed_size'] = 0; - $info['zip']['entries_count'] = 0; - - if (!getid3_lib::intValueSupported($info['filesize'])) { - $info['error'][] = 'File is larger than '.round(PHP_INT_MAX / 1073741824).'GB, not supported by PHP'; - return false; - } else { - $EOCDsearchData = ''; - $EOCDsearchCounter = 0; - while ($EOCDsearchCounter++ < 512) { - - fseek($this->getid3->fp, -128 * $EOCDsearchCounter, SEEK_END); - $EOCDsearchData = fread($this->getid3->fp, 128).$EOCDsearchData; - - if (strstr($EOCDsearchData, 'PK'."\x05\x06")) { - - $EOCDposition = strpos($EOCDsearchData, 'PK'."\x05\x06"); - fseek($this->getid3->fp, (-128 * $EOCDsearchCounter) + $EOCDposition, SEEK_END); - $info['zip']['end_central_directory'] = $this->ZIPparseEndOfCentralDirectory(); - - fseek($this->getid3->fp, $info['zip']['end_central_directory']['directory_offset'], SEEK_SET); - $info['zip']['entries_count'] = 0; - while ($centraldirectoryentry = $this->ZIPparseCentralDirectory($this->getid3->fp)) { - $info['zip']['central_directory'][] = $centraldirectoryentry; - $info['zip']['entries_count']++; - $info['zip']['compressed_size'] += $centraldirectoryentry['compressed_size']; - $info['zip']['uncompressed_size'] += $centraldirectoryentry['uncompressed_size']; - - //if ($centraldirectoryentry['uncompressed_size'] > 0) { zero-byte files are valid - if (!empty($centraldirectoryentry['filename'])) { - $info['zip']['files'] = getid3_lib::array_merge_clobber($info['zip']['files'], getid3_lib::CreateDeepArray($centraldirectoryentry['filename'], '/', $centraldirectoryentry['uncompressed_size'])); - } - } - - if ($info['zip']['entries_count'] == 0) { - $info['error'][] = 'No Central Directory entries found (truncated file?)'; - return false; - } - - if (!empty($info['zip']['end_central_directory']['comment'])) { - $info['zip']['comments']['comment'][] = $info['zip']['end_central_directory']['comment']; - } - - if (isset($info['zip']['central_directory'][0]['compression_method'])) { - $info['zip']['compression_method'] = $info['zip']['central_directory'][0]['compression_method']; - } - if (isset($info['zip']['central_directory'][0]['flags']['compression_speed'])) { - $info['zip']['compression_speed'] = $info['zip']['central_directory'][0]['flags']['compression_speed']; - } - if (isset($info['zip']['compression_method']) && ($info['zip']['compression_method'] == 'store') && !isset($info['zip']['compression_speed'])) { - $info['zip']['compression_speed'] = 'store'; - } - - // secondary check - we (should) already have all the info we NEED from the Central Directory above, but scanning each - // Local File Header entry will - foreach ($info['zip']['central_directory'] as $central_directory_entry) { - fseek($this->getid3->fp, $central_directory_entry['entry_offset'], SEEK_SET); - if ($fileentry = $this->ZIPparseLocalFileHeader()) { - $info['zip']['entries'][] = $fileentry; - } else { - $info['warning'][] = 'Error parsing Local File Header at offset '.$central_directory_entry['entry_offset']; - } - } - - if (!empty($info['zip']['files']['[Content_Types].xml']) && - !empty($info['zip']['files']['_rels']['.rels']) && - !empty($info['zip']['files']['docProps']['app.xml']) && - !empty($info['zip']['files']['docProps']['core.xml'])) { - // http://technet.microsoft.com/en-us/library/cc179224.aspx - $info['fileformat'] = 'zip.msoffice'; - if (!empty($ThisFileInfo['zip']['files']['ppt'])) { - $info['mime_type'] = 'application/vnd.openxmlformats-officedocument.presentationml.presentation'; - } elseif (!empty($ThisFileInfo['zip']['files']['xl'])) { - $info['mime_type'] = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'; - } elseif (!empty($ThisFileInfo['zip']['files']['word'])) { - $info['mime_type'] = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; - } - } - - return true; - } - } - } - - if (!$this->getZIPentriesFilepointer()) { - unset($info['zip']); - $info['fileformat'] = ''; - $info['error'][] = 'Cannot find End Of Central Directory (truncated file?)'; - return false; - } - - // central directory couldn't be found and/or parsed - // scan through actual file data entries, recover as much as possible from probable trucated file - if ($info['zip']['compressed_size'] > ($info['filesize'] - 46 - 22)) { - $info['error'][] = 'Warning: Truncated file! - Total compressed file sizes ('.$info['zip']['compressed_size'].' bytes) is greater than filesize minus Central Directory and End Of Central Directory structures ('.($info['filesize'] - 46 - 22).' bytes)'; - } - $info['error'][] = 'Cannot find End Of Central Directory - returned list of files in [zip][entries] array may not be complete'; - foreach ($info['zip']['entries'] as $key => $valuearray) { - $info['zip']['files'][$valuearray['filename']] = $valuearray['uncompressed_size']; - } - return true; - } - - - public function getZIPHeaderFilepointerTopDown() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'zip'; - - $info['zip']['compressed_size'] = 0; - $info['zip']['uncompressed_size'] = 0; - $info['zip']['entries_count'] = 0; - - rewind($this->getid3->fp); - while ($fileentry = $this->ZIPparseLocalFileHeader()) { - $info['zip']['entries'][] = $fileentry; - $info['zip']['entries_count']++; - } - if ($info['zip']['entries_count'] == 0) { - $info['error'][] = 'No Local File Header entries found'; - return false; - } - - $info['zip']['entries_count'] = 0; - while ($centraldirectoryentry = $this->ZIPparseCentralDirectory($this->getid3->fp)) { - $info['zip']['central_directory'][] = $centraldirectoryentry; - $info['zip']['entries_count']++; - $info['zip']['compressed_size'] += $centraldirectoryentry['compressed_size']; - $info['zip']['uncompressed_size'] += $centraldirectoryentry['uncompressed_size']; - } - if ($info['zip']['entries_count'] == 0) { - $info['error'][] = 'No Central Directory entries found (truncated file?)'; - return false; - } - - if ($EOCD = $this->ZIPparseEndOfCentralDirectory()) { - $info['zip']['end_central_directory'] = $EOCD; - } else { - $info['error'][] = 'No End Of Central Directory entry found (truncated file?)'; - return false; - } - - if (!empty($info['zip']['end_central_directory']['comment'])) { - $info['zip']['comments']['comment'][] = $info['zip']['end_central_directory']['comment']; - } - - return true; - } - - - public function getZIPentriesFilepointer() { - $info = &$this->getid3->info; - - $info['zip']['compressed_size'] = 0; - $info['zip']['uncompressed_size'] = 0; - $info['zip']['entries_count'] = 0; - - rewind($this->getid3->fp); - while ($fileentry = $this->ZIPparseLocalFileHeader()) { - $info['zip']['entries'][] = $fileentry; - $info['zip']['entries_count']++; - $info['zip']['compressed_size'] += $fileentry['compressed_size']; - $info['zip']['uncompressed_size'] += $fileentry['uncompressed_size']; - } - if ($info['zip']['entries_count'] == 0) { - $info['error'][] = 'No Local File Header entries found'; - return false; - } - - return true; - } - - - public function ZIPparseLocalFileHeader() { - $LocalFileHeader['offset'] = ftell($this->getid3->fp); - - $ZIPlocalFileHeader = fread($this->getid3->fp, 30); - - $LocalFileHeader['raw']['signature'] = getid3_lib::LittleEndian2Int(substr($ZIPlocalFileHeader, 0, 4)); - if ($LocalFileHeader['raw']['signature'] != 0x04034B50) { // "PK\x03\x04" - // invalid Local File Header Signature - fseek($this->getid3->fp, $LocalFileHeader['offset'], SEEK_SET); // seek back to where filepointer originally was so it can be handled properly - return false; - } - $LocalFileHeader['raw']['extract_version'] = getid3_lib::LittleEndian2Int(substr($ZIPlocalFileHeader, 4, 2)); - $LocalFileHeader['raw']['general_flags'] = getid3_lib::LittleEndian2Int(substr($ZIPlocalFileHeader, 6, 2)); - $LocalFileHeader['raw']['compression_method'] = getid3_lib::LittleEndian2Int(substr($ZIPlocalFileHeader, 8, 2)); - $LocalFileHeader['raw']['last_mod_file_time'] = getid3_lib::LittleEndian2Int(substr($ZIPlocalFileHeader, 10, 2)); - $LocalFileHeader['raw']['last_mod_file_date'] = getid3_lib::LittleEndian2Int(substr($ZIPlocalFileHeader, 12, 2)); - $LocalFileHeader['raw']['crc_32'] = getid3_lib::LittleEndian2Int(substr($ZIPlocalFileHeader, 14, 4)); - $LocalFileHeader['raw']['compressed_size'] = getid3_lib::LittleEndian2Int(substr($ZIPlocalFileHeader, 18, 4)); - $LocalFileHeader['raw']['uncompressed_size'] = getid3_lib::LittleEndian2Int(substr($ZIPlocalFileHeader, 22, 4)); - $LocalFileHeader['raw']['filename_length'] = getid3_lib::LittleEndian2Int(substr($ZIPlocalFileHeader, 26, 2)); - $LocalFileHeader['raw']['extra_field_length'] = getid3_lib::LittleEndian2Int(substr($ZIPlocalFileHeader, 28, 2)); - - $LocalFileHeader['extract_version'] = sprintf('%1.1f', $LocalFileHeader['raw']['extract_version'] / 10); - $LocalFileHeader['host_os'] = $this->ZIPversionOSLookup(($LocalFileHeader['raw']['extract_version'] & 0xFF00) >> 8); - $LocalFileHeader['compression_method'] = $this->ZIPcompressionMethodLookup($LocalFileHeader['raw']['compression_method']); - $LocalFileHeader['compressed_size'] = $LocalFileHeader['raw']['compressed_size']; - $LocalFileHeader['uncompressed_size'] = $LocalFileHeader['raw']['uncompressed_size']; - $LocalFileHeader['flags'] = $this->ZIPparseGeneralPurposeFlags($LocalFileHeader['raw']['general_flags'], $LocalFileHeader['raw']['compression_method']); - $LocalFileHeader['last_modified_timestamp'] = $this->DOStime2UNIXtime($LocalFileHeader['raw']['last_mod_file_date'], $LocalFileHeader['raw']['last_mod_file_time']); - - $FilenameExtrafieldLength = $LocalFileHeader['raw']['filename_length'] + $LocalFileHeader['raw']['extra_field_length']; - if ($FilenameExtrafieldLength > 0) { - $ZIPlocalFileHeader .= fread($this->getid3->fp, $FilenameExtrafieldLength); - - if ($LocalFileHeader['raw']['filename_length'] > 0) { - $LocalFileHeader['filename'] = substr($ZIPlocalFileHeader, 30, $LocalFileHeader['raw']['filename_length']); - } - if ($LocalFileHeader['raw']['extra_field_length'] > 0) { - $LocalFileHeader['raw']['extra_field_data'] = substr($ZIPlocalFileHeader, 30 + $LocalFileHeader['raw']['filename_length'], $LocalFileHeader['raw']['extra_field_length']); - } - } - - if ($LocalFileHeader['compressed_size'] == 0) { - // *Could* be a zero-byte file - // But could also be a file written on the fly that didn't know compressed filesize beforehand. - // Correct compressed filesize should be in the data_descriptor located after this file data, and also in Central Directory (at end of zip file) - if (!empty($this->getid3->info['zip']['central_directory'])) { - foreach ($this->getid3->info['zip']['central_directory'] as $central_directory_entry) { - if ($central_directory_entry['entry_offset'] == $LocalFileHeader['offset']) { - if ($central_directory_entry['compressed_size'] > 0) { - // overwrite local zero value (but not ['raw']'compressed_size']) so that seeking for data_descriptor (and next file entry) works correctly - $LocalFileHeader['compressed_size'] = $central_directory_entry['compressed_size']; - } - break; - } - } - } - - } - $LocalFileHeader['data_offset'] = ftell($this->getid3->fp); - fseek($this->getid3->fp, $LocalFileHeader['compressed_size'], SEEK_CUR); // this should (but may not) match value in $LocalFileHeader['raw']['compressed_size'] -- $LocalFileHeader['compressed_size'] could have been overwritten above with value from Central Directory - - if ($LocalFileHeader['flags']['data_descriptor_used']) { - $DataDescriptor = fread($this->getid3->fp, 16); - $LocalFileHeader['data_descriptor']['signature'] = getid3_lib::LittleEndian2Int(substr($DataDescriptor, 0, 4)); - if ($LocalFileHeader['data_descriptor']['signature'] != 0x08074B50) { // "PK\x07\x08" - $this->getid3->warning[] = 'invalid Local File Header Data Descriptor Signature at offset '.(ftell($this->getid3->fp) - 16).' - expecting 08 07 4B 50, found '.getid3_lib::PrintHexBytes($LocalFileHeader['data_descriptor']['signature']); - fseek($this->getid3->fp, $LocalFileHeader['offset'], SEEK_SET); // seek back to where filepointer originally was so it can be handled properly - return false; - } - $LocalFileHeader['data_descriptor']['crc_32'] = getid3_lib::LittleEndian2Int(substr($DataDescriptor, 4, 4)); - $LocalFileHeader['data_descriptor']['compressed_size'] = getid3_lib::LittleEndian2Int(substr($DataDescriptor, 8, 4)); - $LocalFileHeader['data_descriptor']['uncompressed_size'] = getid3_lib::LittleEndian2Int(substr($DataDescriptor, 12, 4)); - if (!$LocalFileHeader['raw']['compressed_size'] && $LocalFileHeader['data_descriptor']['compressed_size']) { - foreach ($this->getid3->info['zip']['central_directory'] as $central_directory_entry) { - if ($central_directory_entry['entry_offset'] == $LocalFileHeader['offset']) { - if ($LocalFileHeader['data_descriptor']['compressed_size'] == $central_directory_entry['compressed_size']) { - // $LocalFileHeader['compressed_size'] already set from Central Directory - } else { - $this->getid3->info['warning'][] = 'conflicting compressed_size from data_descriptor ('.$LocalFileHeader['data_descriptor']['compressed_size'].') vs Central Directory ('.$central_directory_entry['compressed_size'].') for file at offset '.$LocalFileHeader['offset']; - } - - if ($LocalFileHeader['data_descriptor']['uncompressed_size'] == $central_directory_entry['uncompressed_size']) { - $LocalFileHeader['uncompressed_size'] = $LocalFileHeader['data_descriptor']['uncompressed_size']; - } else { - $this->getid3->info['warning'][] = 'conflicting uncompressed_size from data_descriptor ('.$LocalFileHeader['data_descriptor']['uncompressed_size'].') vs Central Directory ('.$central_directory_entry['uncompressed_size'].') for file at offset '.$LocalFileHeader['offset']; - } - break; - } - } - } - } - return $LocalFileHeader; - } - - - public function ZIPparseCentralDirectory() { - $CentralDirectory['offset'] = ftell($this->getid3->fp); - - $ZIPcentralDirectory = fread($this->getid3->fp, 46); - - $CentralDirectory['raw']['signature'] = getid3_lib::LittleEndian2Int(substr($ZIPcentralDirectory, 0, 4)); - if ($CentralDirectory['raw']['signature'] != 0x02014B50) { - // invalid Central Directory Signature - fseek($this->getid3->fp, $CentralDirectory['offset'], SEEK_SET); // seek back to where filepointer originally was so it can be handled properly - return false; - } - $CentralDirectory['raw']['create_version'] = getid3_lib::LittleEndian2Int(substr($ZIPcentralDirectory, 4, 2)); - $CentralDirectory['raw']['extract_version'] = getid3_lib::LittleEndian2Int(substr($ZIPcentralDirectory, 6, 2)); - $CentralDirectory['raw']['general_flags'] = getid3_lib::LittleEndian2Int(substr($ZIPcentralDirectory, 8, 2)); - $CentralDirectory['raw']['compression_method'] = getid3_lib::LittleEndian2Int(substr($ZIPcentralDirectory, 10, 2)); - $CentralDirectory['raw']['last_mod_file_time'] = getid3_lib::LittleEndian2Int(substr($ZIPcentralDirectory, 12, 2)); - $CentralDirectory['raw']['last_mod_file_date'] = getid3_lib::LittleEndian2Int(substr($ZIPcentralDirectory, 14, 2)); - $CentralDirectory['raw']['crc_32'] = getid3_lib::LittleEndian2Int(substr($ZIPcentralDirectory, 16, 4)); - $CentralDirectory['raw']['compressed_size'] = getid3_lib::LittleEndian2Int(substr($ZIPcentralDirectory, 20, 4)); - $CentralDirectory['raw']['uncompressed_size'] = getid3_lib::LittleEndian2Int(substr($ZIPcentralDirectory, 24, 4)); - $CentralDirectory['raw']['filename_length'] = getid3_lib::LittleEndian2Int(substr($ZIPcentralDirectory, 28, 2)); - $CentralDirectory['raw']['extra_field_length'] = getid3_lib::LittleEndian2Int(substr($ZIPcentralDirectory, 30, 2)); - $CentralDirectory['raw']['file_comment_length'] = getid3_lib::LittleEndian2Int(substr($ZIPcentralDirectory, 32, 2)); - $CentralDirectory['raw']['disk_number_start'] = getid3_lib::LittleEndian2Int(substr($ZIPcentralDirectory, 34, 2)); - $CentralDirectory['raw']['internal_file_attrib'] = getid3_lib::LittleEndian2Int(substr($ZIPcentralDirectory, 36, 2)); - $CentralDirectory['raw']['external_file_attrib'] = getid3_lib::LittleEndian2Int(substr($ZIPcentralDirectory, 38, 4)); - $CentralDirectory['raw']['local_header_offset'] = getid3_lib::LittleEndian2Int(substr($ZIPcentralDirectory, 42, 4)); - - $CentralDirectory['entry_offset'] = $CentralDirectory['raw']['local_header_offset']; - $CentralDirectory['create_version'] = sprintf('%1.1f', $CentralDirectory['raw']['create_version'] / 10); - $CentralDirectory['extract_version'] = sprintf('%1.1f', $CentralDirectory['raw']['extract_version'] / 10); - $CentralDirectory['host_os'] = $this->ZIPversionOSLookup(($CentralDirectory['raw']['extract_version'] & 0xFF00) >> 8); - $CentralDirectory['compression_method'] = $this->ZIPcompressionMethodLookup($CentralDirectory['raw']['compression_method']); - $CentralDirectory['compressed_size'] = $CentralDirectory['raw']['compressed_size']; - $CentralDirectory['uncompressed_size'] = $CentralDirectory['raw']['uncompressed_size']; - $CentralDirectory['flags'] = $this->ZIPparseGeneralPurposeFlags($CentralDirectory['raw']['general_flags'], $CentralDirectory['raw']['compression_method']); - $CentralDirectory['last_modified_timestamp'] = $this->DOStime2UNIXtime($CentralDirectory['raw']['last_mod_file_date'], $CentralDirectory['raw']['last_mod_file_time']); - - $FilenameExtrafieldCommentLength = $CentralDirectory['raw']['filename_length'] + $CentralDirectory['raw']['extra_field_length'] + $CentralDirectory['raw']['file_comment_length']; - if ($FilenameExtrafieldCommentLength > 0) { - $FilenameExtrafieldComment = fread($this->getid3->fp, $FilenameExtrafieldCommentLength); - - if ($CentralDirectory['raw']['filename_length'] > 0) { - $CentralDirectory['filename'] = substr($FilenameExtrafieldComment, 0, $CentralDirectory['raw']['filename_length']); - } - if ($CentralDirectory['raw']['extra_field_length'] > 0) { - $CentralDirectory['raw']['extra_field_data'] = substr($FilenameExtrafieldComment, $CentralDirectory['raw']['filename_length'], $CentralDirectory['raw']['extra_field_length']); - } - if ($CentralDirectory['raw']['file_comment_length'] > 0) { - $CentralDirectory['file_comment'] = substr($FilenameExtrafieldComment, $CentralDirectory['raw']['filename_length'] + $CentralDirectory['raw']['extra_field_length'], $CentralDirectory['raw']['file_comment_length']); - } - } - - return $CentralDirectory; - } - - public function ZIPparseEndOfCentralDirectory() { - $EndOfCentralDirectory['offset'] = ftell($this->getid3->fp); - - $ZIPendOfCentralDirectory = fread($this->getid3->fp, 22); - - $EndOfCentralDirectory['signature'] = getid3_lib::LittleEndian2Int(substr($ZIPendOfCentralDirectory, 0, 4)); - if ($EndOfCentralDirectory['signature'] != 0x06054B50) { - // invalid End Of Central Directory Signature - fseek($this->getid3->fp, $EndOfCentralDirectory['offset'], SEEK_SET); // seek back to where filepointer originally was so it can be handled properly - return false; - } - $EndOfCentralDirectory['disk_number_current'] = getid3_lib::LittleEndian2Int(substr($ZIPendOfCentralDirectory, 4, 2)); - $EndOfCentralDirectory['disk_number_start_directory'] = getid3_lib::LittleEndian2Int(substr($ZIPendOfCentralDirectory, 6, 2)); - $EndOfCentralDirectory['directory_entries_this_disk'] = getid3_lib::LittleEndian2Int(substr($ZIPendOfCentralDirectory, 8, 2)); - $EndOfCentralDirectory['directory_entries_total'] = getid3_lib::LittleEndian2Int(substr($ZIPendOfCentralDirectory, 10, 2)); - $EndOfCentralDirectory['directory_size'] = getid3_lib::LittleEndian2Int(substr($ZIPendOfCentralDirectory, 12, 4)); - $EndOfCentralDirectory['directory_offset'] = getid3_lib::LittleEndian2Int(substr($ZIPendOfCentralDirectory, 16, 4)); - $EndOfCentralDirectory['comment_length'] = getid3_lib::LittleEndian2Int(substr($ZIPendOfCentralDirectory, 20, 2)); - - if ($EndOfCentralDirectory['comment_length'] > 0) { - $EndOfCentralDirectory['comment'] = fread($this->getid3->fp, $EndOfCentralDirectory['comment_length']); - } - - return $EndOfCentralDirectory; - } - - - public static function ZIPparseGeneralPurposeFlags($flagbytes, $compressionmethod) { - // https://users.cs.jmu.edu/buchhofp/forensics/formats/pkzip-printable.html - $ParsedFlags['encrypted'] = (bool) ($flagbytes & 0x0001); - // 0x0002 -- see below - // 0x0004 -- see below - $ParsedFlags['data_descriptor_used'] = (bool) ($flagbytes & 0x0008); - $ParsedFlags['enhanced_deflation'] = (bool) ($flagbytes & 0x0010); - $ParsedFlags['compressed_patched_data'] = (bool) ($flagbytes & 0x0020); - $ParsedFlags['strong_encryption'] = (bool) ($flagbytes & 0x0040); - // 0x0080 - unused - // 0x0100 - unused - // 0x0200 - unused - // 0x0400 - unused - $ParsedFlags['language_encoding'] = (bool) ($flagbytes & 0x0800); - // 0x1000 - reserved - $ParsedFlags['mask_header_values'] = (bool) ($flagbytes & 0x2000); - // 0x4000 - reserved - // 0x8000 - reserved - - switch ($compressionmethod) { - case 6: - $ParsedFlags['dictionary_size'] = (($flagbytes & 0x0002) ? 8192 : 4096); - $ParsedFlags['shannon_fano_trees'] = (($flagbytes & 0x0004) ? 3 : 2); - break; - - case 8: - case 9: - switch (($flagbytes & 0x0006) >> 1) { - case 0: - $ParsedFlags['compression_speed'] = 'normal'; - break; - case 1: - $ParsedFlags['compression_speed'] = 'maximum'; - break; - case 2: - $ParsedFlags['compression_speed'] = 'fast'; - break; - case 3: - $ParsedFlags['compression_speed'] = 'superfast'; - break; - } - break; - } - - return $ParsedFlags; - } - - - public static function ZIPversionOSLookup($index) { - static $ZIPversionOSLookup = array( - 0 => 'MS-DOS and OS/2 (FAT / VFAT / FAT32 file systems)', - 1 => 'Amiga', - 2 => 'OpenVMS', - 3 => 'Unix', - 4 => 'VM/CMS', - 5 => 'Atari ST', - 6 => 'OS/2 H.P.F.S.', - 7 => 'Macintosh', - 8 => 'Z-System', - 9 => 'CP/M', - 10 => 'Windows NTFS', - 11 => 'MVS', - 12 => 'VSE', - 13 => 'Acorn Risc', - 14 => 'VFAT', - 15 => 'Alternate MVS', - 16 => 'BeOS', - 17 => 'Tandem', - 18 => 'OS/400', - 19 => 'OS/X (Darwin)', - ); - - return (isset($ZIPversionOSLookup[$index]) ? $ZIPversionOSLookup[$index] : '[unknown]'); - } - - public static function ZIPcompressionMethodLookup($index) { - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/ZIP.html - static $ZIPcompressionMethodLookup = array( - 0 => 'store', - 1 => 'shrink', - 2 => 'reduce-1', - 3 => 'reduce-2', - 4 => 'reduce-3', - 5 => 'reduce-4', - 6 => 'implode', - 7 => 'tokenize', - 8 => 'deflate', - 9 => 'deflate64', - 10 => 'Imploded (old IBM TERSE)', - 11 => 'RESERVED[11]', - 12 => 'BZIP2', - 13 => 'RESERVED[13]', - 14 => 'LZMA (EFS)', - 15 => 'RESERVED[15]', - 16 => 'RESERVED[16]', - 17 => 'RESERVED[17]', - 18 => 'IBM TERSE (new)', - 19 => 'IBM LZ77 z Architecture (PFS)', - 96 => 'JPEG recompressed', - 97 => 'WavPack compressed', - 98 => 'PPMd version I, Rev 1', - ); - - return (isset($ZIPcompressionMethodLookup[$index]) ? $ZIPcompressionMethodLookup[$index] : '[unknown]'); - } - - public static function DOStime2UNIXtime($DOSdate, $DOStime) { - // wFatDate - // Specifies the MS-DOS date. The date is a packed 16-bit value with the following format: - // Bits Contents - // 0-4 Day of the month (1-31) - // 5-8 Month (1 = January, 2 = February, and so on) - // 9-15 Year offset from 1980 (add 1980 to get actual year) - - $UNIXday = ($DOSdate & 0x001F); - $UNIXmonth = (($DOSdate & 0x01E0) >> 5); - $UNIXyear = (($DOSdate & 0xFE00) >> 9) + 1980; - - // wFatTime - // Specifies the MS-DOS time. The time is a packed 16-bit value with the following format: - // Bits Contents - // 0-4 Second divided by 2 - // 5-10 Minute (0-59) - // 11-15 Hour (0-23 on a 24-hour clock) - - $UNIXsecond = ($DOStime & 0x001F) * 2; - $UNIXminute = (($DOStime & 0x07E0) >> 5); - $UNIXhour = (($DOStime & 0xF800) >> 11); - - return gmmktime($UNIXhour, $UNIXminute, $UNIXsecond, $UNIXmonth, $UNIXday, $UNIXyear); - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio-video.asf.php b/src/Classes/Vendor/getid3/module.audio-video.asf.php deleted file mode 100755 index cfc60a780..000000000 --- a/src/Classes/Vendor/getid3/module.audio-video.asf.php +++ /dev/null @@ -1,2019 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio-video.asf.php // -// module for analyzing ASF, WMA and WMV files // -// dependencies: module.audio-video.riff.php // -// /// -///////////////////////////////////////////////////////////////// - -getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); - -class getid3_asf extends getid3_handler -{ - - public function __construct(getID3 $getid3) { - parent::__construct($getid3); // extends getid3_handler::__construct() - - // initialize all GUID constants - $GUIDarray = $this->KnownGUIDs(); - foreach ($GUIDarray as $GUIDname => $hexstringvalue) { - if (!defined($GUIDname)) { - define($GUIDname, $this->GUIDtoBytestring($hexstringvalue)); - } - } - } - - public function Analyze() { - $info = &$this->getid3->info; - - // Shortcuts - $thisfile_audio = &$info['audio']; - $thisfile_video = &$info['video']; - $info['asf'] = array(); - $thisfile_asf = &$info['asf']; - $thisfile_asf['comments'] = array(); - $thisfile_asf_comments = &$thisfile_asf['comments']; - $thisfile_asf['header_object'] = array(); - $thisfile_asf_headerobject = &$thisfile_asf['header_object']; - - - // ASF structure: - // * Header Object [required] - // * File Properties Object [required] (global file attributes) - // * Stream Properties Object [required] (defines media stream & characteristics) - // * Header Extension Object [required] (additional functionality) - // * Content Description Object (bibliographic information) - // * Script Command Object (commands for during playback) - // * Marker Object (named jumped points within the file) - // * Data Object [required] - // * Data Packets - // * Index Object - - // Header Object: (mandatory, one only) - // Field Name Field Type Size (bits) - // Object ID GUID 128 // GUID for header object - GETID3_ASF_Header_Object - // Object Size QWORD 64 // size of header object, including 30 bytes of Header Object header - // Number of Header Objects DWORD 32 // number of objects in header object - // Reserved1 BYTE 8 // hardcoded: 0x01 - // Reserved2 BYTE 8 // hardcoded: 0x02 - - $info['fileformat'] = 'asf'; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $HeaderObjectData = fread($this->getid3->fp, 30); - - $thisfile_asf_headerobject['objectid'] = substr($HeaderObjectData, 0, 16); - $thisfile_asf_headerobject['objectid_guid'] = $this->BytestringToGUID($thisfile_asf_headerobject['objectid']); - if ($thisfile_asf_headerobject['objectid'] != GETID3_ASF_Header_Object) { - $info['warning'][] = 'ASF header GUID {'.$this->BytestringToGUID($thisfile_asf_headerobject['objectid']).'} does not match expected "GETID3_ASF_Header_Object" GUID {'.$this->BytestringToGUID(GETID3_ASF_Header_Object).'}'; - unset($info['fileformat']); - unset($info['asf']); - return false; - break; - } - $thisfile_asf_headerobject['objectsize'] = getid3_lib::LittleEndian2Int(substr($HeaderObjectData, 16, 8)); - $thisfile_asf_headerobject['headerobjects'] = getid3_lib::LittleEndian2Int(substr($HeaderObjectData, 24, 4)); - $thisfile_asf_headerobject['reserved1'] = getid3_lib::LittleEndian2Int(substr($HeaderObjectData, 28, 1)); - $thisfile_asf_headerobject['reserved2'] = getid3_lib::LittleEndian2Int(substr($HeaderObjectData, 29, 1)); - - $NextObjectOffset = ftell($this->getid3->fp); - $ASFHeaderData = fread($this->getid3->fp, $thisfile_asf_headerobject['objectsize'] - 30); - $offset = 0; - - for ($HeaderObjectsCounter = 0; $HeaderObjectsCounter < $thisfile_asf_headerobject['headerobjects']; $HeaderObjectsCounter++) { - $NextObjectGUID = substr($ASFHeaderData, $offset, 16); - $offset += 16; - $NextObjectGUIDtext = $this->BytestringToGUID($NextObjectGUID); - $NextObjectSize = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 8)); - $offset += 8; - switch ($NextObjectGUID) { - - case GETID3_ASF_File_Properties_Object: - // File Properties Object: (mandatory, one only) - // Field Name Field Type Size (bits) - // Object ID GUID 128 // GUID for file properties object - GETID3_ASF_File_Properties_Object - // Object Size QWORD 64 // size of file properties object, including 104 bytes of File Properties Object header - // File ID GUID 128 // unique ID - identical to File ID in Data Object - // File Size QWORD 64 // entire file in bytes. Invalid if Broadcast Flag == 1 - // Creation Date QWORD 64 // date & time of file creation. Maybe invalid if Broadcast Flag == 1 - // Data Packets Count QWORD 64 // number of data packets in Data Object. Invalid if Broadcast Flag == 1 - // Play Duration QWORD 64 // playtime, in 100-nanosecond units. Invalid if Broadcast Flag == 1 - // Send Duration QWORD 64 // time needed to send file, in 100-nanosecond units. Players can ignore this value. Invalid if Broadcast Flag == 1 - // Preroll QWORD 64 // time to buffer data before starting to play file, in 1-millisecond units. If <> 0, PlayDuration and PresentationTime have been offset by this amount - // Flags DWORD 32 // - // * Broadcast Flag bits 1 (0x01) // file is currently being written, some header values are invalid - // * Seekable Flag bits 1 (0x02) // is file seekable - // * Reserved bits 30 (0xFFFFFFFC) // reserved - set to zero - // Minimum Data Packet Size DWORD 32 // in bytes. should be same as Maximum Data Packet Size. Invalid if Broadcast Flag == 1 - // Maximum Data Packet Size DWORD 32 // in bytes. should be same as Minimum Data Packet Size. Invalid if Broadcast Flag == 1 - // Maximum Bitrate DWORD 32 // maximum instantaneous bitrate in bits per second for entire file, including all data streams and ASF overhead - - // shortcut - $thisfile_asf['file_properties_object'] = array(); - $thisfile_asf_filepropertiesobject = &$thisfile_asf['file_properties_object']; - - $thisfile_asf_filepropertiesobject['offset'] = $NextObjectOffset + $offset; - $thisfile_asf_filepropertiesobject['objectid'] = $NextObjectGUID; - $thisfile_asf_filepropertiesobject['objectid_guid'] = $NextObjectGUIDtext; - $thisfile_asf_filepropertiesobject['objectsize'] = $NextObjectSize; - $thisfile_asf_filepropertiesobject['fileid'] = substr($ASFHeaderData, $offset, 16); - $offset += 16; - $thisfile_asf_filepropertiesobject['fileid_guid'] = $this->BytestringToGUID($thisfile_asf_filepropertiesobject['fileid']); - $thisfile_asf_filepropertiesobject['filesize'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 8)); - $offset += 8; - $thisfile_asf_filepropertiesobject['creation_date'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 8)); - $thisfile_asf_filepropertiesobject['creation_date_unix'] = $this->FILETIMEtoUNIXtime($thisfile_asf_filepropertiesobject['creation_date']); - $offset += 8; - $thisfile_asf_filepropertiesobject['data_packets'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 8)); - $offset += 8; - $thisfile_asf_filepropertiesobject['play_duration'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 8)); - $offset += 8; - $thisfile_asf_filepropertiesobject['send_duration'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 8)); - $offset += 8; - $thisfile_asf_filepropertiesobject['preroll'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 8)); - $offset += 8; - $thisfile_asf_filepropertiesobject['flags_raw'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 4)); - $offset += 4; - $thisfile_asf_filepropertiesobject['flags']['broadcast'] = (bool) ($thisfile_asf_filepropertiesobject['flags_raw'] & 0x0001); - $thisfile_asf_filepropertiesobject['flags']['seekable'] = (bool) ($thisfile_asf_filepropertiesobject['flags_raw'] & 0x0002); - - $thisfile_asf_filepropertiesobject['min_packet_size'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 4)); - $offset += 4; - $thisfile_asf_filepropertiesobject['max_packet_size'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 4)); - $offset += 4; - $thisfile_asf_filepropertiesobject['max_bitrate'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 4)); - $offset += 4; - - if ($thisfile_asf_filepropertiesobject['flags']['broadcast']) { - - // broadcast flag is set, some values invalid - unset($thisfile_asf_filepropertiesobject['filesize']); - unset($thisfile_asf_filepropertiesobject['data_packets']); - unset($thisfile_asf_filepropertiesobject['play_duration']); - unset($thisfile_asf_filepropertiesobject['send_duration']); - unset($thisfile_asf_filepropertiesobject['min_packet_size']); - unset($thisfile_asf_filepropertiesobject['max_packet_size']); - - } else { - - // broadcast flag NOT set, perform calculations - $info['playtime_seconds'] = ($thisfile_asf_filepropertiesobject['play_duration'] / 10000000) - ($thisfile_asf_filepropertiesobject['preroll'] / 1000); - - //$info['bitrate'] = $thisfile_asf_filepropertiesobject['max_bitrate']; - $info['bitrate'] = ((isset($thisfile_asf_filepropertiesobject['filesize']) ? $thisfile_asf_filepropertiesobject['filesize'] : $info['filesize']) * 8) / $info['playtime_seconds']; - } - break; - - case GETID3_ASF_Stream_Properties_Object: - // Stream Properties Object: (mandatory, one per media stream) - // Field Name Field Type Size (bits) - // Object ID GUID 128 // GUID for stream properties object - GETID3_ASF_Stream_Properties_Object - // Object Size QWORD 64 // size of stream properties object, including 78 bytes of Stream Properties Object header - // Stream Type GUID 128 // GETID3_ASF_Audio_Media, GETID3_ASF_Video_Media or GETID3_ASF_Command_Media - // Error Correction Type GUID 128 // GETID3_ASF_Audio_Spread for audio-only streams, GETID3_ASF_No_Error_Correction for other stream types - // Time Offset QWORD 64 // 100-nanosecond units. typically zero. added to all timestamps of samples in the stream - // Type-Specific Data Length DWORD 32 // number of bytes for Type-Specific Data field - // Error Correction Data Length DWORD 32 // number of bytes for Error Correction Data field - // Flags WORD 16 // - // * Stream Number bits 7 (0x007F) // number of this stream. 1 <= valid <= 127 - // * Reserved bits 8 (0x7F80) // reserved - set to zero - // * Encrypted Content Flag bits 1 (0x8000) // stream contents encrypted if set - // Reserved DWORD 32 // reserved - set to zero - // Type-Specific Data BYTESTREAM variable // type-specific format data, depending on value of Stream Type - // Error Correction Data BYTESTREAM variable // error-correction-specific format data, depending on value of Error Correct Type - - // There is one GETID3_ASF_Stream_Properties_Object for each stream (audio, video) but the - // stream number isn't known until halfway through decoding the structure, hence it - // it is decoded to a temporary variable and then stuck in the appropriate index later - - $StreamPropertiesObjectData['offset'] = $NextObjectOffset + $offset; - $StreamPropertiesObjectData['objectid'] = $NextObjectGUID; - $StreamPropertiesObjectData['objectid_guid'] = $NextObjectGUIDtext; - $StreamPropertiesObjectData['objectsize'] = $NextObjectSize; - $StreamPropertiesObjectData['stream_type'] = substr($ASFHeaderData, $offset, 16); - $offset += 16; - $StreamPropertiesObjectData['stream_type_guid'] = $this->BytestringToGUID($StreamPropertiesObjectData['stream_type']); - $StreamPropertiesObjectData['error_correct_type'] = substr($ASFHeaderData, $offset, 16); - $offset += 16; - $StreamPropertiesObjectData['error_correct_guid'] = $this->BytestringToGUID($StreamPropertiesObjectData['error_correct_type']); - $StreamPropertiesObjectData['time_offset'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 8)); - $offset += 8; - $StreamPropertiesObjectData['type_data_length'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 4)); - $offset += 4; - $StreamPropertiesObjectData['error_data_length'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 4)); - $offset += 4; - $StreamPropertiesObjectData['flags_raw'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $StreamPropertiesObjectStreamNumber = $StreamPropertiesObjectData['flags_raw'] & 0x007F; - $StreamPropertiesObjectData['flags']['encrypted'] = (bool) ($StreamPropertiesObjectData['flags_raw'] & 0x8000); - - $offset += 4; // reserved - DWORD - $StreamPropertiesObjectData['type_specific_data'] = substr($ASFHeaderData, $offset, $StreamPropertiesObjectData['type_data_length']); - $offset += $StreamPropertiesObjectData['type_data_length']; - $StreamPropertiesObjectData['error_correct_data'] = substr($ASFHeaderData, $offset, $StreamPropertiesObjectData['error_data_length']); - $offset += $StreamPropertiesObjectData['error_data_length']; - - switch ($StreamPropertiesObjectData['stream_type']) { - - case GETID3_ASF_Audio_Media: - $thisfile_audio['dataformat'] = (!empty($thisfile_audio['dataformat']) ? $thisfile_audio['dataformat'] : 'asf'); - $thisfile_audio['bitrate_mode'] = (!empty($thisfile_audio['bitrate_mode']) ? $thisfile_audio['bitrate_mode'] : 'cbr'); - - $audiodata = getid3_riff::parseWAVEFORMATex(substr($StreamPropertiesObjectData['type_specific_data'], 0, 16)); - unset($audiodata['raw']); - $thisfile_audio = getid3_lib::array_merge_noclobber($audiodata, $thisfile_audio); - break; - - case GETID3_ASF_Video_Media: - $thisfile_video['dataformat'] = (!empty($thisfile_video['dataformat']) ? $thisfile_video['dataformat'] : 'asf'); - $thisfile_video['bitrate_mode'] = (!empty($thisfile_video['bitrate_mode']) ? $thisfile_video['bitrate_mode'] : 'cbr'); - break; - - case GETID3_ASF_Command_Media: - default: - // do nothing - break; - - } - - $thisfile_asf['stream_properties_object'][$StreamPropertiesObjectStreamNumber] = $StreamPropertiesObjectData; - unset($StreamPropertiesObjectData); // clear for next stream, if any - break; - - case GETID3_ASF_Header_Extension_Object: - // Header Extension Object: (mandatory, one only) - // Field Name Field Type Size (bits) - // Object ID GUID 128 // GUID for Header Extension object - GETID3_ASF_Header_Extension_Object - // Object Size QWORD 64 // size of Header Extension object, including 46 bytes of Header Extension Object header - // Reserved Field 1 GUID 128 // hardcoded: GETID3_ASF_Reserved_1 - // Reserved Field 2 WORD 16 // hardcoded: 0x00000006 - // Header Extension Data Size DWORD 32 // in bytes. valid: 0, or > 24. equals object size minus 46 - // Header Extension Data BYTESTREAM variable // array of zero or more extended header objects - - // shortcut - $thisfile_asf['header_extension_object'] = array(); - $thisfile_asf_headerextensionobject = &$thisfile_asf['header_extension_object']; - - $thisfile_asf_headerextensionobject['offset'] = $NextObjectOffset + $offset; - $thisfile_asf_headerextensionobject['objectid'] = $NextObjectGUID; - $thisfile_asf_headerextensionobject['objectid_guid'] = $NextObjectGUIDtext; - $thisfile_asf_headerextensionobject['objectsize'] = $NextObjectSize; - $thisfile_asf_headerextensionobject['reserved_1'] = substr($ASFHeaderData, $offset, 16); - $offset += 16; - $thisfile_asf_headerextensionobject['reserved_1_guid'] = $this->BytestringToGUID($thisfile_asf_headerextensionobject['reserved_1']); - if ($thisfile_asf_headerextensionobject['reserved_1'] != GETID3_ASF_Reserved_1) { - $info['warning'][] = 'header_extension_object.reserved_1 GUID ('.$this->BytestringToGUID($thisfile_asf_headerextensionobject['reserved_1']).') does not match expected "GETID3_ASF_Reserved_1" GUID ('.$this->BytestringToGUID(GETID3_ASF_Reserved_1).')'; - //return false; - break; - } - $thisfile_asf_headerextensionobject['reserved_2'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - if ($thisfile_asf_headerextensionobject['reserved_2'] != 6) { - $info['warning'][] = 'header_extension_object.reserved_2 ('.getid3_lib::PrintHexBytes($thisfile_asf_headerextensionobject['reserved_2']).') does not match expected value of "6"'; - //return false; - break; - } - $thisfile_asf_headerextensionobject['extension_data_size'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 4)); - $offset += 4; - $thisfile_asf_headerextensionobject['extension_data'] = substr($ASFHeaderData, $offset, $thisfile_asf_headerextensionobject['extension_data_size']); - $unhandled_sections = 0; - $thisfile_asf_headerextensionobject['extension_data_parsed'] = $this->ASF_HeaderExtensionObjectDataParse($thisfile_asf_headerextensionobject['extension_data'], $unhandled_sections); - if ($unhandled_sections === 0) { - unset($thisfile_asf_headerextensionobject['extension_data']); - } - $offset += $thisfile_asf_headerextensionobject['extension_data_size']; - break; - - case GETID3_ASF_Codec_List_Object: - // Codec List Object: (optional, one only) - // Field Name Field Type Size (bits) - // Object ID GUID 128 // GUID for Codec List object - GETID3_ASF_Codec_List_Object - // Object Size QWORD 64 // size of Codec List object, including 44 bytes of Codec List Object header - // Reserved GUID 128 // hardcoded: 86D15241-311D-11D0-A3A4-00A0C90348F6 - // Codec Entries Count DWORD 32 // number of entries in Codec Entries array - // Codec Entries array of: variable // - // * Type WORD 16 // 0x0001 = Video Codec, 0x0002 = Audio Codec, 0xFFFF = Unknown Codec - // * Codec Name Length WORD 16 // number of Unicode characters stored in the Codec Name field - // * Codec Name WCHAR variable // array of Unicode characters - name of codec used to create the content - // * Codec Description Length WORD 16 // number of Unicode characters stored in the Codec Description field - // * Codec Description WCHAR variable // array of Unicode characters - description of format used to create the content - // * Codec Information Length WORD 16 // number of Unicode characters stored in the Codec Information field - // * Codec Information BYTESTREAM variable // opaque array of information bytes about the codec used to create the content - - // shortcut - $thisfile_asf['codec_list_object'] = array(); - $thisfile_asf_codeclistobject = &$thisfile_asf['codec_list_object']; - - $thisfile_asf_codeclistobject['offset'] = $NextObjectOffset + $offset; - $thisfile_asf_codeclistobject['objectid'] = $NextObjectGUID; - $thisfile_asf_codeclistobject['objectid_guid'] = $NextObjectGUIDtext; - $thisfile_asf_codeclistobject['objectsize'] = $NextObjectSize; - $thisfile_asf_codeclistobject['reserved'] = substr($ASFHeaderData, $offset, 16); - $offset += 16; - $thisfile_asf_codeclistobject['reserved_guid'] = $this->BytestringToGUID($thisfile_asf_codeclistobject['reserved']); - if ($thisfile_asf_codeclistobject['reserved'] != $this->GUIDtoBytestring('86D15241-311D-11D0-A3A4-00A0C90348F6')) { - $info['warning'][] = 'codec_list_object.reserved GUID {'.$this->BytestringToGUID($thisfile_asf_codeclistobject['reserved']).'} does not match expected "GETID3_ASF_Reserved_1" GUID {86D15241-311D-11D0-A3A4-00A0C90348F6}'; - //return false; - break; - } - $thisfile_asf_codeclistobject['codec_entries_count'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 4)); - $offset += 4; - for ($CodecEntryCounter = 0; $CodecEntryCounter < $thisfile_asf_codeclistobject['codec_entries_count']; $CodecEntryCounter++) { - // shortcut - $thisfile_asf_codeclistobject['codec_entries'][$CodecEntryCounter] = array(); - $thisfile_asf_codeclistobject_codecentries_current = &$thisfile_asf_codeclistobject['codec_entries'][$CodecEntryCounter]; - - $thisfile_asf_codeclistobject_codecentries_current['type_raw'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $thisfile_asf_codeclistobject_codecentries_current['type'] = $this->ASFCodecListObjectTypeLookup($thisfile_asf_codeclistobject_codecentries_current['type_raw']); - - $CodecNameLength = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)) * 2; // 2 bytes per character - $offset += 2; - $thisfile_asf_codeclistobject_codecentries_current['name'] = substr($ASFHeaderData, $offset, $CodecNameLength); - $offset += $CodecNameLength; - - $CodecDescriptionLength = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)) * 2; // 2 bytes per character - $offset += 2; - $thisfile_asf_codeclistobject_codecentries_current['description'] = substr($ASFHeaderData, $offset, $CodecDescriptionLength); - $offset += $CodecDescriptionLength; - - $CodecInformationLength = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $thisfile_asf_codeclistobject_codecentries_current['information'] = substr($ASFHeaderData, $offset, $CodecInformationLength); - $offset += $CodecInformationLength; - - if ($thisfile_asf_codeclistobject_codecentries_current['type_raw'] == 2) { // audio codec - - if (strpos($thisfile_asf_codeclistobject_codecentries_current['description'], ',') === false) { - $info['warning'][] = '[asf][codec_list_object][codec_entries]['.$CodecEntryCounter.'][description] expected to contain comma-seperated list of parameters: "'.$thisfile_asf_codeclistobject_codecentries_current['description'].'"'; - } else { - - list($AudioCodecBitrate, $AudioCodecFrequency, $AudioCodecChannels) = explode(',', $this->TrimConvert($thisfile_asf_codeclistobject_codecentries_current['description'])); - $thisfile_audio['codec'] = $this->TrimConvert($thisfile_asf_codeclistobject_codecentries_current['name']); - - if (!isset($thisfile_audio['bitrate']) && strstr($AudioCodecBitrate, 'kbps')) { - $thisfile_audio['bitrate'] = (int) (trim(str_replace('kbps', '', $AudioCodecBitrate)) * 1000); - } - //if (!isset($thisfile_video['bitrate']) && isset($thisfile_audio['bitrate']) && isset($thisfile_asf['file_properties_object']['max_bitrate']) && ($thisfile_asf_codeclistobject['codec_entries_count'] > 1)) { - if (empty($thisfile_video['bitrate']) && !empty($thisfile_audio['bitrate']) && !empty($info['bitrate'])) { - //$thisfile_video['bitrate'] = $thisfile_asf['file_properties_object']['max_bitrate'] - $thisfile_audio['bitrate']; - $thisfile_video['bitrate'] = $info['bitrate'] - $thisfile_audio['bitrate']; - } - - $AudioCodecFrequency = (int) trim(str_replace('kHz', '', $AudioCodecFrequency)); - switch ($AudioCodecFrequency) { - case 8: - case 8000: - $thisfile_audio['sample_rate'] = 8000; - break; - - case 11: - case 11025: - $thisfile_audio['sample_rate'] = 11025; - break; - - case 12: - case 12000: - $thisfile_audio['sample_rate'] = 12000; - break; - - case 16: - case 16000: - $thisfile_audio['sample_rate'] = 16000; - break; - - case 22: - case 22050: - $thisfile_audio['sample_rate'] = 22050; - break; - - case 24: - case 24000: - $thisfile_audio['sample_rate'] = 24000; - break; - - case 32: - case 32000: - $thisfile_audio['sample_rate'] = 32000; - break; - - case 44: - case 441000: - $thisfile_audio['sample_rate'] = 44100; - break; - - case 48: - case 48000: - $thisfile_audio['sample_rate'] = 48000; - break; - - default: - $info['warning'][] = 'unknown frequency: "'.$AudioCodecFrequency.'" ('.$this->TrimConvert($thisfile_asf_codeclistobject_codecentries_current['description']).')'; - break; - } - - if (!isset($thisfile_audio['channels'])) { - if (strstr($AudioCodecChannels, 'stereo')) { - $thisfile_audio['channels'] = 2; - } elseif (strstr($AudioCodecChannels, 'mono')) { - $thisfile_audio['channels'] = 1; - } - } - - } - } - } - break; - - case GETID3_ASF_Script_Command_Object: - // Script Command Object: (optional, one only) - // Field Name Field Type Size (bits) - // Object ID GUID 128 // GUID for Script Command object - GETID3_ASF_Script_Command_Object - // Object Size QWORD 64 // size of Script Command object, including 44 bytes of Script Command Object header - // Reserved GUID 128 // hardcoded: 4B1ACBE3-100B-11D0-A39B-00A0C90348F6 - // Commands Count WORD 16 // number of Commands structures in the Script Commands Objects - // Command Types Count WORD 16 // number of Command Types structures in the Script Commands Objects - // Command Types array of: variable // - // * Command Type Name Length WORD 16 // number of Unicode characters for Command Type Name - // * Command Type Name WCHAR variable // array of Unicode characters - name of a type of command - // Commands array of: variable // - // * Presentation Time DWORD 32 // presentation time of that command, in milliseconds - // * Type Index WORD 16 // type of this command, as a zero-based index into the array of Command Types of this object - // * Command Name Length WORD 16 // number of Unicode characters for Command Name - // * Command Name WCHAR variable // array of Unicode characters - name of this command - - // shortcut - $thisfile_asf['script_command_object'] = array(); - $thisfile_asf_scriptcommandobject = &$thisfile_asf['script_command_object']; - - $thisfile_asf_scriptcommandobject['offset'] = $NextObjectOffset + $offset; - $thisfile_asf_scriptcommandobject['objectid'] = $NextObjectGUID; - $thisfile_asf_scriptcommandobject['objectid_guid'] = $NextObjectGUIDtext; - $thisfile_asf_scriptcommandobject['objectsize'] = $NextObjectSize; - $thisfile_asf_scriptcommandobject['reserved'] = substr($ASFHeaderData, $offset, 16); - $offset += 16; - $thisfile_asf_scriptcommandobject['reserved_guid'] = $this->BytestringToGUID($thisfile_asf_scriptcommandobject['reserved']); - if ($thisfile_asf_scriptcommandobject['reserved'] != $this->GUIDtoBytestring('4B1ACBE3-100B-11D0-A39B-00A0C90348F6')) { - $info['warning'][] = 'script_command_object.reserved GUID {'.$this->BytestringToGUID($thisfile_asf_scriptcommandobject['reserved']).'} does not match expected "GETID3_ASF_Reserved_1" GUID {4B1ACBE3-100B-11D0-A39B-00A0C90348F6}'; - //return false; - break; - } - $thisfile_asf_scriptcommandobject['commands_count'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $thisfile_asf_scriptcommandobject['command_types_count'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - for ($CommandTypesCounter = 0; $CommandTypesCounter < $thisfile_asf_scriptcommandobject['command_types_count']; $CommandTypesCounter++) { - $CommandTypeNameLength = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)) * 2; // 2 bytes per character - $offset += 2; - $thisfile_asf_scriptcommandobject['command_types'][$CommandTypesCounter]['name'] = substr($ASFHeaderData, $offset, $CommandTypeNameLength); - $offset += $CommandTypeNameLength; - } - for ($CommandsCounter = 0; $CommandsCounter < $thisfile_asf_scriptcommandobject['commands_count']; $CommandsCounter++) { - $thisfile_asf_scriptcommandobject['commands'][$CommandsCounter]['presentation_time'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 4)); - $offset += 4; - $thisfile_asf_scriptcommandobject['commands'][$CommandsCounter]['type_index'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - - $CommandTypeNameLength = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)) * 2; // 2 bytes per character - $offset += 2; - $thisfile_asf_scriptcommandobject['commands'][$CommandsCounter]['name'] = substr($ASFHeaderData, $offset, $CommandTypeNameLength); - $offset += $CommandTypeNameLength; - } - break; - - case GETID3_ASF_Marker_Object: - // Marker Object: (optional, one only) - // Field Name Field Type Size (bits) - // Object ID GUID 128 // GUID for Marker object - GETID3_ASF_Marker_Object - // Object Size QWORD 64 // size of Marker object, including 48 bytes of Marker Object header - // Reserved GUID 128 // hardcoded: 4CFEDB20-75F6-11CF-9C0F-00A0C90349CB - // Markers Count DWORD 32 // number of Marker structures in Marker Object - // Reserved WORD 16 // hardcoded: 0x0000 - // Name Length WORD 16 // number of bytes in the Name field - // Name WCHAR variable // name of the Marker Object - // Markers array of: variable // - // * Offset QWORD 64 // byte offset into Data Object - // * Presentation Time QWORD 64 // in 100-nanosecond units - // * Entry Length WORD 16 // length in bytes of (Send Time + Flags + Marker Description Length + Marker Description + Padding) - // * Send Time DWORD 32 // in milliseconds - // * Flags DWORD 32 // hardcoded: 0x00000000 - // * Marker Description Length DWORD 32 // number of bytes in Marker Description field - // * Marker Description WCHAR variable // array of Unicode characters - description of marker entry - // * Padding BYTESTREAM variable // optional padding bytes - - // shortcut - $thisfile_asf['marker_object'] = array(); - $thisfile_asf_markerobject = &$thisfile_asf['marker_object']; - - $thisfile_asf_markerobject['offset'] = $NextObjectOffset + $offset; - $thisfile_asf_markerobject['objectid'] = $NextObjectGUID; - $thisfile_asf_markerobject['objectid_guid'] = $NextObjectGUIDtext; - $thisfile_asf_markerobject['objectsize'] = $NextObjectSize; - $thisfile_asf_markerobject['reserved'] = substr($ASFHeaderData, $offset, 16); - $offset += 16; - $thisfile_asf_markerobject['reserved_guid'] = $this->BytestringToGUID($thisfile_asf_markerobject['reserved']); - if ($thisfile_asf_markerobject['reserved'] != $this->GUIDtoBytestring('4CFEDB20-75F6-11CF-9C0F-00A0C90349CB')) { - $info['warning'][] = 'marker_object.reserved GUID {'.$this->BytestringToGUID($thisfile_asf_markerobject['reserved_1']).'} does not match expected "GETID3_ASF_Reserved_1" GUID {4CFEDB20-75F6-11CF-9C0F-00A0C90349CB}'; - break; - } - $thisfile_asf_markerobject['markers_count'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 4)); - $offset += 4; - $thisfile_asf_markerobject['reserved_2'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - if ($thisfile_asf_markerobject['reserved_2'] != 0) { - $info['warning'][] = 'marker_object.reserved_2 ('.getid3_lib::PrintHexBytes($thisfile_asf_markerobject['reserved_2']).') does not match expected value of "0"'; - break; - } - $thisfile_asf_markerobject['name_length'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $thisfile_asf_markerobject['name'] = substr($ASFHeaderData, $offset, $thisfile_asf_markerobject['name_length']); - $offset += $thisfile_asf_markerobject['name_length']; - for ($MarkersCounter = 0; $MarkersCounter < $thisfile_asf_markerobject['markers_count']; $MarkersCounter++) { - $thisfile_asf_markerobject['markers'][$MarkersCounter]['offset'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 8)); - $offset += 8; - $thisfile_asf_markerobject['markers'][$MarkersCounter]['presentation_time'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 8)); - $offset += 8; - $thisfile_asf_markerobject['markers'][$MarkersCounter]['entry_length'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $thisfile_asf_markerobject['markers'][$MarkersCounter]['send_time'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 4)); - $offset += 4; - $thisfile_asf_markerobject['markers'][$MarkersCounter]['flags'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 4)); - $offset += 4; - $thisfile_asf_markerobject['markers'][$MarkersCounter]['marker_description_length'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 4)); - $offset += 4; - $thisfile_asf_markerobject['markers'][$MarkersCounter]['marker_description'] = substr($ASFHeaderData, $offset, $thisfile_asf_markerobject['markers'][$MarkersCounter]['marker_description_length']); - $offset += $thisfile_asf_markerobject['markers'][$MarkersCounter]['marker_description_length']; - $PaddingLength = $thisfile_asf_markerobject['markers'][$MarkersCounter]['entry_length'] - 4 - 4 - 4 - $thisfile_asf_markerobject['markers'][$MarkersCounter]['marker_description_length']; - if ($PaddingLength > 0) { - $thisfile_asf_markerobject['markers'][$MarkersCounter]['padding'] = substr($ASFHeaderData, $offset, $PaddingLength); - $offset += $PaddingLength; - } - } - break; - - case GETID3_ASF_Bitrate_Mutual_Exclusion_Object: - // Bitrate Mutual Exclusion Object: (optional) - // Field Name Field Type Size (bits) - // Object ID GUID 128 // GUID for Bitrate Mutual Exclusion object - GETID3_ASF_Bitrate_Mutual_Exclusion_Object - // Object Size QWORD 64 // size of Bitrate Mutual Exclusion object, including 42 bytes of Bitrate Mutual Exclusion Object header - // Exlusion Type GUID 128 // nature of mutual exclusion relationship. one of: (GETID3_ASF_Mutex_Bitrate, GETID3_ASF_Mutex_Unknown) - // Stream Numbers Count WORD 16 // number of video streams - // Stream Numbers WORD variable // array of mutually exclusive video stream numbers. 1 <= valid <= 127 - - // shortcut - $thisfile_asf['bitrate_mutual_exclusion_object'] = array(); - $thisfile_asf_bitratemutualexclusionobject = &$thisfile_asf['bitrate_mutual_exclusion_object']; - - $thisfile_asf_bitratemutualexclusionobject['offset'] = $NextObjectOffset + $offset; - $thisfile_asf_bitratemutualexclusionobject['objectid'] = $NextObjectGUID; - $thisfile_asf_bitratemutualexclusionobject['objectid_guid'] = $NextObjectGUIDtext; - $thisfile_asf_bitratemutualexclusionobject['objectsize'] = $NextObjectSize; - $thisfile_asf_bitratemutualexclusionobject['reserved'] = substr($ASFHeaderData, $offset, 16); - $thisfile_asf_bitratemutualexclusionobject['reserved_guid'] = $this->BytestringToGUID($thisfile_asf_bitratemutualexclusionobject['reserved']); - $offset += 16; - if (($thisfile_asf_bitratemutualexclusionobject['reserved'] != GETID3_ASF_Mutex_Bitrate) && ($thisfile_asf_bitratemutualexclusionobject['reserved'] != GETID3_ASF_Mutex_Unknown)) { - $info['warning'][] = 'bitrate_mutual_exclusion_object.reserved GUID {'.$this->BytestringToGUID($thisfile_asf_bitratemutualexclusionobject['reserved']).'} does not match expected "GETID3_ASF_Mutex_Bitrate" GUID {'.$this->BytestringToGUID(GETID3_ASF_Mutex_Bitrate).'} or "GETID3_ASF_Mutex_Unknown" GUID {'.$this->BytestringToGUID(GETID3_ASF_Mutex_Unknown).'}'; - //return false; - break; - } - $thisfile_asf_bitratemutualexclusionobject['stream_numbers_count'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - for ($StreamNumberCounter = 0; $StreamNumberCounter < $thisfile_asf_bitratemutualexclusionobject['stream_numbers_count']; $StreamNumberCounter++) { - $thisfile_asf_bitratemutualexclusionobject['stream_numbers'][$StreamNumberCounter] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - } - break; - - case GETID3_ASF_Error_Correction_Object: - // Error Correction Object: (optional, one only) - // Field Name Field Type Size (bits) - // Object ID GUID 128 // GUID for Error Correction object - GETID3_ASF_Error_Correction_Object - // Object Size QWORD 64 // size of Error Correction object, including 44 bytes of Error Correction Object header - // Error Correction Type GUID 128 // type of error correction. one of: (GETID3_ASF_No_Error_Correction, GETID3_ASF_Audio_Spread) - // Error Correction Data Length DWORD 32 // number of bytes in Error Correction Data field - // Error Correction Data BYTESTREAM variable // structure depends on value of Error Correction Type field - - // shortcut - $thisfile_asf['error_correction_object'] = array(); - $thisfile_asf_errorcorrectionobject = &$thisfile_asf['error_correction_object']; - - $thisfile_asf_errorcorrectionobject['offset'] = $NextObjectOffset + $offset; - $thisfile_asf_errorcorrectionobject['objectid'] = $NextObjectGUID; - $thisfile_asf_errorcorrectionobject['objectid_guid'] = $NextObjectGUIDtext; - $thisfile_asf_errorcorrectionobject['objectsize'] = $NextObjectSize; - $thisfile_asf_errorcorrectionobject['error_correction_type'] = substr($ASFHeaderData, $offset, 16); - $offset += 16; - $thisfile_asf_errorcorrectionobject['error_correction_guid'] = $this->BytestringToGUID($thisfile_asf_errorcorrectionobject['error_correction_type']); - $thisfile_asf_errorcorrectionobject['error_correction_data_length'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 4)); - $offset += 4; - switch ($thisfile_asf_errorcorrectionobject['error_correction_type']) { - case GETID3_ASF_No_Error_Correction: - // should be no data, but just in case there is, skip to the end of the field - $offset += $thisfile_asf_errorcorrectionobject['error_correction_data_length']; - break; - - case GETID3_ASF_Audio_Spread: - // Field Name Field Type Size (bits) - // Span BYTE 8 // number of packets over which audio will be spread. - // Virtual Packet Length WORD 16 // size of largest audio payload found in audio stream - // Virtual Chunk Length WORD 16 // size of largest audio payload found in audio stream - // Silence Data Length WORD 16 // number of bytes in Silence Data field - // Silence Data BYTESTREAM variable // hardcoded: 0x00 * (Silence Data Length) bytes - - $thisfile_asf_errorcorrectionobject['span'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 1)); - $offset += 1; - $thisfile_asf_errorcorrectionobject['virtual_packet_length'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $thisfile_asf_errorcorrectionobject['virtual_chunk_length'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $thisfile_asf_errorcorrectionobject['silence_data_length'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $thisfile_asf_errorcorrectionobject['silence_data'] = substr($ASFHeaderData, $offset, $thisfile_asf_errorcorrectionobject['silence_data_length']); - $offset += $thisfile_asf_errorcorrectionobject['silence_data_length']; - break; - - default: - $info['warning'][] = 'error_correction_object.error_correction_type GUID {'.$this->BytestringToGUID($thisfile_asf_errorcorrectionobject['reserved']).'} does not match expected "GETID3_ASF_No_Error_Correction" GUID {'.$this->BytestringToGUID(GETID3_ASF_No_Error_Correction).'} or "GETID3_ASF_Audio_Spread" GUID {'.$this->BytestringToGUID(GETID3_ASF_Audio_Spread).'}'; - //return false; - break; - } - - break; - - case GETID3_ASF_Content_Description_Object: - // Content Description Object: (optional, one only) - // Field Name Field Type Size (bits) - // Object ID GUID 128 // GUID for Content Description object - GETID3_ASF_Content_Description_Object - // Object Size QWORD 64 // size of Content Description object, including 34 bytes of Content Description Object header - // Title Length WORD 16 // number of bytes in Title field - // Author Length WORD 16 // number of bytes in Author field - // Copyright Length WORD 16 // number of bytes in Copyright field - // Description Length WORD 16 // number of bytes in Description field - // Rating Length WORD 16 // number of bytes in Rating field - // Title WCHAR 16 // array of Unicode characters - Title - // Author WCHAR 16 // array of Unicode characters - Author - // Copyright WCHAR 16 // array of Unicode characters - Copyright - // Description WCHAR 16 // array of Unicode characters - Description - // Rating WCHAR 16 // array of Unicode characters - Rating - - // shortcut - $thisfile_asf['content_description_object'] = array(); - $thisfile_asf_contentdescriptionobject = &$thisfile_asf['content_description_object']; - - $thisfile_asf_contentdescriptionobject['offset'] = $NextObjectOffset + $offset; - $thisfile_asf_contentdescriptionobject['objectid'] = $NextObjectGUID; - $thisfile_asf_contentdescriptionobject['objectid_guid'] = $NextObjectGUIDtext; - $thisfile_asf_contentdescriptionobject['objectsize'] = $NextObjectSize; - $thisfile_asf_contentdescriptionobject['title_length'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $thisfile_asf_contentdescriptionobject['author_length'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $thisfile_asf_contentdescriptionobject['copyright_length'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $thisfile_asf_contentdescriptionobject['description_length'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $thisfile_asf_contentdescriptionobject['rating_length'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $thisfile_asf_contentdescriptionobject['title'] = substr($ASFHeaderData, $offset, $thisfile_asf_contentdescriptionobject['title_length']); - $offset += $thisfile_asf_contentdescriptionobject['title_length']; - $thisfile_asf_contentdescriptionobject['author'] = substr($ASFHeaderData, $offset, $thisfile_asf_contentdescriptionobject['author_length']); - $offset += $thisfile_asf_contentdescriptionobject['author_length']; - $thisfile_asf_contentdescriptionobject['copyright'] = substr($ASFHeaderData, $offset, $thisfile_asf_contentdescriptionobject['copyright_length']); - $offset += $thisfile_asf_contentdescriptionobject['copyright_length']; - $thisfile_asf_contentdescriptionobject['description'] = substr($ASFHeaderData, $offset, $thisfile_asf_contentdescriptionobject['description_length']); - $offset += $thisfile_asf_contentdescriptionobject['description_length']; - $thisfile_asf_contentdescriptionobject['rating'] = substr($ASFHeaderData, $offset, $thisfile_asf_contentdescriptionobject['rating_length']); - $offset += $thisfile_asf_contentdescriptionobject['rating_length']; - - $ASFcommentKeysToCopy = array('title'=>'title', 'author'=>'artist', 'copyright'=>'copyright', 'description'=>'comment', 'rating'=>'rating'); - foreach ($ASFcommentKeysToCopy as $keytocopyfrom => $keytocopyto) { - if (!empty($thisfile_asf_contentdescriptionobject[$keytocopyfrom])) { - $thisfile_asf_comments[$keytocopyto][] = $this->TrimTerm($thisfile_asf_contentdescriptionobject[$keytocopyfrom]); - } - } - break; - - case GETID3_ASF_Extended_Content_Description_Object: - // Extended Content Description Object: (optional, one only) - // Field Name Field Type Size (bits) - // Object ID GUID 128 // GUID for Extended Content Description object - GETID3_ASF_Extended_Content_Description_Object - // Object Size QWORD 64 // size of ExtendedContent Description object, including 26 bytes of Extended Content Description Object header - // Content Descriptors Count WORD 16 // number of entries in Content Descriptors list - // Content Descriptors array of: variable // - // * Descriptor Name Length WORD 16 // size in bytes of Descriptor Name field - // * Descriptor Name WCHAR variable // array of Unicode characters - Descriptor Name - // * Descriptor Value Data Type WORD 16 // Lookup array: - // 0x0000 = Unicode String (variable length) - // 0x0001 = BYTE array (variable length) - // 0x0002 = BOOL (DWORD, 32 bits) - // 0x0003 = DWORD (DWORD, 32 bits) - // 0x0004 = QWORD (QWORD, 64 bits) - // 0x0005 = WORD (WORD, 16 bits) - // * Descriptor Value Length WORD 16 // number of bytes stored in Descriptor Value field - // * Descriptor Value variable variable // value for Content Descriptor - - // shortcut - $thisfile_asf['extended_content_description_object'] = array(); - $thisfile_asf_extendedcontentdescriptionobject = &$thisfile_asf['extended_content_description_object']; - - $thisfile_asf_extendedcontentdescriptionobject['offset'] = $NextObjectOffset + $offset; - $thisfile_asf_extendedcontentdescriptionobject['objectid'] = $NextObjectGUID; - $thisfile_asf_extendedcontentdescriptionobject['objectid_guid'] = $NextObjectGUIDtext; - $thisfile_asf_extendedcontentdescriptionobject['objectsize'] = $NextObjectSize; - $thisfile_asf_extendedcontentdescriptionobject['content_descriptors_count'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - for ($ExtendedContentDescriptorsCounter = 0; $ExtendedContentDescriptorsCounter < $thisfile_asf_extendedcontentdescriptionobject['content_descriptors_count']; $ExtendedContentDescriptorsCounter++) { - // shortcut - $thisfile_asf_extendedcontentdescriptionobject['content_descriptors'][$ExtendedContentDescriptorsCounter] = array(); - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current = &$thisfile_asf_extendedcontentdescriptionobject['content_descriptors'][$ExtendedContentDescriptorsCounter]; - - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['base_offset'] = $offset + 30; - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['name_length'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['name'] = substr($ASFHeaderData, $offset, $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['name_length']); - $offset += $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['name_length']; - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value_type'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value_length'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'] = substr($ASFHeaderData, $offset, $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value_length']); - $offset += $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value_length']; - switch ($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value_type']) { - case 0x0000: // Unicode string - break; - - case 0x0001: // BYTE array - // do nothing - break; - - case 0x0002: // BOOL - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'] = (bool) getid3_lib::LittleEndian2Int($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value']); - break; - - case 0x0003: // DWORD - case 0x0004: // QWORD - case 0x0005: // WORD - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'] = getid3_lib::LittleEndian2Int($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value']); - break; - - default: - $info['warning'][] = 'extended_content_description.content_descriptors.'.$ExtendedContentDescriptorsCounter.'.value_type is invalid ('.$thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value_type'].')'; - //return false; - break; - } - switch ($this->TrimConvert(strtolower($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['name']))) { - - case 'wm/albumartist': - case 'artist': - // Note: not 'artist', that comes from 'author' tag - $thisfile_asf_comments['albumartist'] = array($this->TrimTerm($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'])); - break; - - case 'wm/albumtitle': - case 'album': - $thisfile_asf_comments['album'] = array($this->TrimTerm($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'])); - break; - - case 'wm/genre': - case 'genre': - $thisfile_asf_comments['genre'] = array($this->TrimTerm($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'])); - break; - - case 'wm/partofset': - $thisfile_asf_comments['partofset'] = array($this->TrimTerm($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'])); - break; - - case 'wm/tracknumber': - case 'tracknumber': - // be careful casting to int: casting unicode strings to int gives unexpected results (stops parsing at first non-numeric character) - $thisfile_asf_comments['track'] = array($this->TrimTerm($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'])); - foreach ($thisfile_asf_comments['track'] as $key => $value) { - if (preg_match('/^[0-9\x00]+$/', $value)) { - $thisfile_asf_comments['track'][$key] = intval(str_replace("\x00", '', $value)); - } - } - break; - - case 'wm/track': - if (empty($thisfile_asf_comments['track'])) { - $thisfile_asf_comments['track'] = array(1 + $this->TrimConvert($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'])); - } - break; - - case 'wm/year': - case 'year': - case 'date': - $thisfile_asf_comments['year'] = array( $this->TrimTerm($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'])); - break; - - case 'wm/lyrics': - case 'lyrics': - $thisfile_asf_comments['lyrics'] = array($this->TrimTerm($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'])); - break; - - case 'isvbr': - if ($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value']) { - $thisfile_audio['bitrate_mode'] = 'vbr'; - $thisfile_video['bitrate_mode'] = 'vbr'; - } - break; - - case 'id3': - // id3v2 module might not be loaded - if (class_exists('getid3_id3v2')) { - $tempfile = tempnam(GETID3_TEMP_DIR, 'getID3'); - $tempfilehandle = fopen($tempfile, 'wb'); - $tempThisfileInfo = array('encoding'=>$info['encoding']); - fwrite($tempfilehandle, $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value']); - fclose($tempfilehandle); - - $getid3_temp = new getID3(); - $getid3_temp->openfile($tempfile); - $getid3_id3v2 = new getid3_id3v2($getid3_temp); - $getid3_id3v2->Analyze(); - $info['id3v2'] = $getid3_temp->info['id3v2']; - unset($getid3_temp, $getid3_id3v2); - - unlink($tempfile); - } - break; - - case 'wm/encodingtime': - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['encoding_time_unix'] = $this->FILETIMEtoUNIXtime($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value']); - $thisfile_asf_comments['encoding_time_unix'] = array($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['encoding_time_unix']); - break; - - case 'wm/picture': - $WMpicture = $this->ASF_WMpicture($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value']); - foreach ($WMpicture as $key => $value) { - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current[$key] = $value; - } - unset($WMpicture); -/* - $wm_picture_offset = 0; - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['image_type_id'] = getid3_lib::LittleEndian2Int(substr($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'], $wm_picture_offset, 1)); - $wm_picture_offset += 1; - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['image_type'] = $this->WMpictureTypeLookup($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['image_type_id']); - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['image_size'] = getid3_lib::LittleEndian2Int(substr($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'], $wm_picture_offset, 4)); - $wm_picture_offset += 4; - - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['image_mime'] = ''; - do { - $next_byte_pair = substr($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'], $wm_picture_offset, 2); - $wm_picture_offset += 2; - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['image_mime'] .= $next_byte_pair; - } while ($next_byte_pair !== "\x00\x00"); - - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['image_description'] = ''; - do { - $next_byte_pair = substr($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'], $wm_picture_offset, 2); - $wm_picture_offset += 2; - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['image_description'] .= $next_byte_pair; - } while ($next_byte_pair !== "\x00\x00"); - - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['dataoffset'] = $wm_picture_offset; - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['data'] = substr($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'], $wm_picture_offset); - unset($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value']); - - $imageinfo = array(); - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['image_mime'] = ''; - $imagechunkcheck = getid3_lib::GetDataImageSize($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['data'], $imageinfo); - unset($imageinfo); - if (!empty($imagechunkcheck)) { - $thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['image_mime'] = image_type_to_mime_type($imagechunkcheck[2]); - } - if (!isset($thisfile_asf_comments['picture'])) { - $thisfile_asf_comments['picture'] = array(); - } - $thisfile_asf_comments['picture'][] = array('data'=>$thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['data'], 'image_mime'=>$thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['image_mime']); -*/ - break; - - default: - switch ($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value_type']) { - case 0: // Unicode string - if (substr($this->TrimConvert($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['name']), 0, 3) == 'WM/') { - $thisfile_asf_comments[str_replace('wm/', '', strtolower($this->TrimConvert($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['name'])))] = array($this->TrimTerm($thisfile_asf_extendedcontentdescriptionobject_contentdescriptor_current['value'])); - } - break; - - case 1: - break; - } - break; - } - - } - break; - - case GETID3_ASF_Stream_Bitrate_Properties_Object: - // Stream Bitrate Properties Object: (optional, one only) - // Field Name Field Type Size (bits) - // Object ID GUID 128 // GUID for Stream Bitrate Properties object - GETID3_ASF_Stream_Bitrate_Properties_Object - // Object Size QWORD 64 // size of Extended Content Description object, including 26 bytes of Stream Bitrate Properties Object header - // Bitrate Records Count WORD 16 // number of records in Bitrate Records - // Bitrate Records array of: variable // - // * Flags WORD 16 // - // * * Stream Number bits 7 (0x007F) // number of this stream - // * * Reserved bits 9 (0xFF80) // hardcoded: 0 - // * Average Bitrate DWORD 32 // in bits per second - - // shortcut - $thisfile_asf['stream_bitrate_properties_object'] = array(); - $thisfile_asf_streambitratepropertiesobject = &$thisfile_asf['stream_bitrate_properties_object']; - - $thisfile_asf_streambitratepropertiesobject['offset'] = $NextObjectOffset + $offset; - $thisfile_asf_streambitratepropertiesobject['objectid'] = $NextObjectGUID; - $thisfile_asf_streambitratepropertiesobject['objectid_guid'] = $NextObjectGUIDtext; - $thisfile_asf_streambitratepropertiesobject['objectsize'] = $NextObjectSize; - $thisfile_asf_streambitratepropertiesobject['bitrate_records_count'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - for ($BitrateRecordsCounter = 0; $BitrateRecordsCounter < $thisfile_asf_streambitratepropertiesobject['bitrate_records_count']; $BitrateRecordsCounter++) { - $thisfile_asf_streambitratepropertiesobject['bitrate_records'][$BitrateRecordsCounter]['flags_raw'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 2)); - $offset += 2; - $thisfile_asf_streambitratepropertiesobject['bitrate_records'][$BitrateRecordsCounter]['flags']['stream_number'] = $thisfile_asf_streambitratepropertiesobject['bitrate_records'][$BitrateRecordsCounter]['flags_raw'] & 0x007F; - $thisfile_asf_streambitratepropertiesobject['bitrate_records'][$BitrateRecordsCounter]['bitrate'] = getid3_lib::LittleEndian2Int(substr($ASFHeaderData, $offset, 4)); - $offset += 4; - } - break; - - case GETID3_ASF_Padding_Object: - // Padding Object: (optional) - // Field Name Field Type Size (bits) - // Object ID GUID 128 // GUID for Padding object - GETID3_ASF_Padding_Object - // Object Size QWORD 64 // size of Padding object, including 24 bytes of ASF Padding Object header - // Padding Data BYTESTREAM variable // ignore - - // shortcut - $thisfile_asf['padding_object'] = array(); - $thisfile_asf_paddingobject = &$thisfile_asf['padding_object']; - - $thisfile_asf_paddingobject['offset'] = $NextObjectOffset + $offset; - $thisfile_asf_paddingobject['objectid'] = $NextObjectGUID; - $thisfile_asf_paddingobject['objectid_guid'] = $NextObjectGUIDtext; - $thisfile_asf_paddingobject['objectsize'] = $NextObjectSize; - $thisfile_asf_paddingobject['padding_length'] = $thisfile_asf_paddingobject['objectsize'] - 16 - 8; - $thisfile_asf_paddingobject['padding'] = substr($ASFHeaderData, $offset, $thisfile_asf_paddingobject['padding_length']); - $offset += ($NextObjectSize - 16 - 8); - break; - - case GETID3_ASF_Extended_Content_Encryption_Object: - case GETID3_ASF_Content_Encryption_Object: - // WMA DRM - just ignore - $offset += ($NextObjectSize - 16 - 8); - break; - - default: - // Implementations shall ignore any standard or non-standard object that they do not know how to handle. - if ($this->GUIDname($NextObjectGUIDtext)) { - $info['warning'][] = 'unhandled GUID "'.$this->GUIDname($NextObjectGUIDtext).'" {'.$NextObjectGUIDtext.'} in ASF header at offset '.($offset - 16 - 8); - } else { - $info['warning'][] = 'unknown GUID {'.$NextObjectGUIDtext.'} in ASF header at offset '.($offset - 16 - 8); - } - $offset += ($NextObjectSize - 16 - 8); - break; - } - } - if (isset($thisfile_asf_streambitrateproperties['bitrate_records_count'])) { - $ASFbitrateAudio = 0; - $ASFbitrateVideo = 0; - for ($BitrateRecordsCounter = 0; $BitrateRecordsCounter < $thisfile_asf_streambitrateproperties['bitrate_records_count']; $BitrateRecordsCounter++) { - if (isset($thisfile_asf_codeclistobject['codec_entries'][$BitrateRecordsCounter])) { - switch ($thisfile_asf_codeclistobject['codec_entries'][$BitrateRecordsCounter]['type_raw']) { - case 1: - $ASFbitrateVideo += $thisfile_asf_streambitrateproperties['bitrate_records'][$BitrateRecordsCounter]['bitrate']; - break; - - case 2: - $ASFbitrateAudio += $thisfile_asf_streambitrateproperties['bitrate_records'][$BitrateRecordsCounter]['bitrate']; - break; - - default: - // do nothing - break; - } - } - } - if ($ASFbitrateAudio > 0) { - $thisfile_audio['bitrate'] = $ASFbitrateAudio; - } - if ($ASFbitrateVideo > 0) { - $thisfile_video['bitrate'] = $ASFbitrateVideo; - } - } - if (isset($thisfile_asf['stream_properties_object']) && is_array($thisfile_asf['stream_properties_object'])) { - - $thisfile_audio['bitrate'] = 0; - $thisfile_video['bitrate'] = 0; - - foreach ($thisfile_asf['stream_properties_object'] as $streamnumber => $streamdata) { - - switch ($streamdata['stream_type']) { - case GETID3_ASF_Audio_Media: - // Field Name Field Type Size (bits) - // Codec ID / Format Tag WORD 16 // unique ID of audio codec - defined as wFormatTag field of WAVEFORMATEX structure - // Number of Channels WORD 16 // number of channels of audio - defined as nChannels field of WAVEFORMATEX structure - // Samples Per Second DWORD 32 // in Hertz - defined as nSamplesPerSec field of WAVEFORMATEX structure - // Average number of Bytes/sec DWORD 32 // bytes/sec of audio stream - defined as nAvgBytesPerSec field of WAVEFORMATEX structure - // Block Alignment WORD 16 // block size in bytes of audio codec - defined as nBlockAlign field of WAVEFORMATEX structure - // Bits per sample WORD 16 // bits per sample of mono data. set to zero for variable bitrate codecs. defined as wBitsPerSample field of WAVEFORMATEX structure - // Codec Specific Data Size WORD 16 // size in bytes of Codec Specific Data buffer - defined as cbSize field of WAVEFORMATEX structure - // Codec Specific Data BYTESTREAM variable // array of codec-specific data bytes - - // shortcut - $thisfile_asf['audio_media'][$streamnumber] = array(); - $thisfile_asf_audiomedia_currentstream = &$thisfile_asf['audio_media'][$streamnumber]; - - $audiomediaoffset = 0; - - $thisfile_asf_audiomedia_currentstream = getid3_riff::parseWAVEFORMATex(substr($streamdata['type_specific_data'], $audiomediaoffset, 16)); - $audiomediaoffset += 16; - - $thisfile_audio['lossless'] = false; - switch ($thisfile_asf_audiomedia_currentstream['raw']['wFormatTag']) { - case 0x0001: // PCM - case 0x0163: // WMA9 Lossless - $thisfile_audio['lossless'] = true; - break; - } - - if (!empty($thisfile_asf['stream_bitrate_properties_object']['bitrate_records'])) { - foreach ($thisfile_asf['stream_bitrate_properties_object']['bitrate_records'] as $dummy => $dataarray) { - if (isset($dataarray['flags']['stream_number']) && ($dataarray['flags']['stream_number'] == $streamnumber)) { - $thisfile_asf_audiomedia_currentstream['bitrate'] = $dataarray['bitrate']; - $thisfile_audio['bitrate'] += $dataarray['bitrate']; - break; - } - } - } else { - if (!empty($thisfile_asf_audiomedia_currentstream['bytes_sec'])) { - $thisfile_audio['bitrate'] += $thisfile_asf_audiomedia_currentstream['bytes_sec'] * 8; - } elseif (!empty($thisfile_asf_audiomedia_currentstream['bitrate'])) { - $thisfile_audio['bitrate'] += $thisfile_asf_audiomedia_currentstream['bitrate']; - } - } - $thisfile_audio['streams'][$streamnumber] = $thisfile_asf_audiomedia_currentstream; - $thisfile_audio['streams'][$streamnumber]['wformattag'] = $thisfile_asf_audiomedia_currentstream['raw']['wFormatTag']; - $thisfile_audio['streams'][$streamnumber]['lossless'] = $thisfile_audio['lossless']; - $thisfile_audio['streams'][$streamnumber]['bitrate'] = $thisfile_audio['bitrate']; - $thisfile_audio['streams'][$streamnumber]['dataformat'] = 'wma'; - unset($thisfile_audio['streams'][$streamnumber]['raw']); - - $thisfile_asf_audiomedia_currentstream['codec_data_size'] = getid3_lib::LittleEndian2Int(substr($streamdata['type_specific_data'], $audiomediaoffset, 2)); - $audiomediaoffset += 2; - $thisfile_asf_audiomedia_currentstream['codec_data'] = substr($streamdata['type_specific_data'], $audiomediaoffset, $thisfile_asf_audiomedia_currentstream['codec_data_size']); - $audiomediaoffset += $thisfile_asf_audiomedia_currentstream['codec_data_size']; - - break; - - case GETID3_ASF_Video_Media: - // Field Name Field Type Size (bits) - // Encoded Image Width DWORD 32 // width of image in pixels - // Encoded Image Height DWORD 32 // height of image in pixels - // Reserved Flags BYTE 8 // hardcoded: 0x02 - // Format Data Size WORD 16 // size of Format Data field in bytes - // Format Data array of: variable // - // * Format Data Size DWORD 32 // number of bytes in Format Data field, in bytes - defined as biSize field of BITMAPINFOHEADER structure - // * Image Width LONG 32 // width of encoded image in pixels - defined as biWidth field of BITMAPINFOHEADER structure - // * Image Height LONG 32 // height of encoded image in pixels - defined as biHeight field of BITMAPINFOHEADER structure - // * Reserved WORD 16 // hardcoded: 0x0001 - defined as biPlanes field of BITMAPINFOHEADER structure - // * Bits Per Pixel Count WORD 16 // bits per pixel - defined as biBitCount field of BITMAPINFOHEADER structure - // * Compression ID FOURCC 32 // fourcc of video codec - defined as biCompression field of BITMAPINFOHEADER structure - // * Image Size DWORD 32 // image size in bytes - defined as biSizeImage field of BITMAPINFOHEADER structure - // * Horizontal Pixels / Meter DWORD 32 // horizontal resolution of target device in pixels per meter - defined as biXPelsPerMeter field of BITMAPINFOHEADER structure - // * Vertical Pixels / Meter DWORD 32 // vertical resolution of target device in pixels per meter - defined as biYPelsPerMeter field of BITMAPINFOHEADER structure - // * Colors Used Count DWORD 32 // number of color indexes in the color table that are actually used - defined as biClrUsed field of BITMAPINFOHEADER structure - // * Important Colors Count DWORD 32 // number of color index required for displaying bitmap. if zero, all colors are required. defined as biClrImportant field of BITMAPINFOHEADER structure - // * Codec Specific Data BYTESTREAM variable // array of codec-specific data bytes - - // shortcut - $thisfile_asf['video_media'][$streamnumber] = array(); - $thisfile_asf_videomedia_currentstream = &$thisfile_asf['video_media'][$streamnumber]; - - $videomediaoffset = 0; - $thisfile_asf_videomedia_currentstream['image_width'] = getid3_lib::LittleEndian2Int(substr($streamdata['type_specific_data'], $videomediaoffset, 4)); - $videomediaoffset += 4; - $thisfile_asf_videomedia_currentstream['image_height'] = getid3_lib::LittleEndian2Int(substr($streamdata['type_specific_data'], $videomediaoffset, 4)); - $videomediaoffset += 4; - $thisfile_asf_videomedia_currentstream['flags'] = getid3_lib::LittleEndian2Int(substr($streamdata['type_specific_data'], $videomediaoffset, 1)); - $videomediaoffset += 1; - $thisfile_asf_videomedia_currentstream['format_data_size'] = getid3_lib::LittleEndian2Int(substr($streamdata['type_specific_data'], $videomediaoffset, 2)); - $videomediaoffset += 2; - $thisfile_asf_videomedia_currentstream['format_data']['format_data_size'] = getid3_lib::LittleEndian2Int(substr($streamdata['type_specific_data'], $videomediaoffset, 4)); - $videomediaoffset += 4; - $thisfile_asf_videomedia_currentstream['format_data']['image_width'] = getid3_lib::LittleEndian2Int(substr($streamdata['type_specific_data'], $videomediaoffset, 4)); - $videomediaoffset += 4; - $thisfile_asf_videomedia_currentstream['format_data']['image_height'] = getid3_lib::LittleEndian2Int(substr($streamdata['type_specific_data'], $videomediaoffset, 4)); - $videomediaoffset += 4; - $thisfile_asf_videomedia_currentstream['format_data']['reserved'] = getid3_lib::LittleEndian2Int(substr($streamdata['type_specific_data'], $videomediaoffset, 2)); - $videomediaoffset += 2; - $thisfile_asf_videomedia_currentstream['format_data']['bits_per_pixel'] = getid3_lib::LittleEndian2Int(substr($streamdata['type_specific_data'], $videomediaoffset, 2)); - $videomediaoffset += 2; - $thisfile_asf_videomedia_currentstream['format_data']['codec_fourcc'] = substr($streamdata['type_specific_data'], $videomediaoffset, 4); - $videomediaoffset += 4; - $thisfile_asf_videomedia_currentstream['format_data']['image_size'] = getid3_lib::LittleEndian2Int(substr($streamdata['type_specific_data'], $videomediaoffset, 4)); - $videomediaoffset += 4; - $thisfile_asf_videomedia_currentstream['format_data']['horizontal_pels'] = getid3_lib::LittleEndian2Int(substr($streamdata['type_specific_data'], $videomediaoffset, 4)); - $videomediaoffset += 4; - $thisfile_asf_videomedia_currentstream['format_data']['vertical_pels'] = getid3_lib::LittleEndian2Int(substr($streamdata['type_specific_data'], $videomediaoffset, 4)); - $videomediaoffset += 4; - $thisfile_asf_videomedia_currentstream['format_data']['colors_used'] = getid3_lib::LittleEndian2Int(substr($streamdata['type_specific_data'], $videomediaoffset, 4)); - $videomediaoffset += 4; - $thisfile_asf_videomedia_currentstream['format_data']['colors_important'] = getid3_lib::LittleEndian2Int(substr($streamdata['type_specific_data'], $videomediaoffset, 4)); - $videomediaoffset += 4; - $thisfile_asf_videomedia_currentstream['format_data']['codec_data'] = substr($streamdata['type_specific_data'], $videomediaoffset); - - if (!empty($thisfile_asf['stream_bitrate_properties_object']['bitrate_records'])) { - foreach ($thisfile_asf['stream_bitrate_properties_object']['bitrate_records'] as $dummy => $dataarray) { - if (isset($dataarray['flags']['stream_number']) && ($dataarray['flags']['stream_number'] == $streamnumber)) { - $thisfile_asf_videomedia_currentstream['bitrate'] = $dataarray['bitrate']; - $thisfile_video['streams'][$streamnumber]['bitrate'] = $dataarray['bitrate']; - $thisfile_video['bitrate'] += $dataarray['bitrate']; - break; - } - } - } - - $thisfile_asf_videomedia_currentstream['format_data']['codec'] = getid3_riff::fourccLookup($thisfile_asf_videomedia_currentstream['format_data']['codec_fourcc']); - - $thisfile_video['streams'][$streamnumber]['fourcc'] = $thisfile_asf_videomedia_currentstream['format_data']['codec_fourcc']; - $thisfile_video['streams'][$streamnumber]['codec'] = $thisfile_asf_videomedia_currentstream['format_data']['codec']; - $thisfile_video['streams'][$streamnumber]['resolution_x'] = $thisfile_asf_videomedia_currentstream['image_width']; - $thisfile_video['streams'][$streamnumber]['resolution_y'] = $thisfile_asf_videomedia_currentstream['image_height']; - $thisfile_video['streams'][$streamnumber]['bits_per_sample'] = $thisfile_asf_videomedia_currentstream['format_data']['bits_per_pixel']; - break; - - default: - break; - } - } - } - - while (ftell($this->getid3->fp) < $info['avdataend']) { - $NextObjectDataHeader = fread($this->getid3->fp, 24); - $offset = 0; - $NextObjectGUID = substr($NextObjectDataHeader, 0, 16); - $offset += 16; - $NextObjectGUIDtext = $this->BytestringToGUID($NextObjectGUID); - $NextObjectSize = getid3_lib::LittleEndian2Int(substr($NextObjectDataHeader, $offset, 8)); - $offset += 8; - - switch ($NextObjectGUID) { - case GETID3_ASF_Data_Object: - // Data Object: (mandatory, one only) - // Field Name Field Type Size (bits) - // Object ID GUID 128 // GUID for Data object - GETID3_ASF_Data_Object - // Object Size QWORD 64 // size of Data object, including 50 bytes of Data Object header. may be 0 if FilePropertiesObject.BroadcastFlag == 1 - // File ID GUID 128 // unique identifier. identical to File ID field in Header Object - // Total Data Packets QWORD 64 // number of Data Packet entries in Data Object. invalid if FilePropertiesObject.BroadcastFlag == 1 - // Reserved WORD 16 // hardcoded: 0x0101 - - // shortcut - $thisfile_asf['data_object'] = array(); - $thisfile_asf_dataobject = &$thisfile_asf['data_object']; - - $DataObjectData = $NextObjectDataHeader.fread($this->getid3->fp, 50 - 24); - $offset = 24; - - $thisfile_asf_dataobject['objectid'] = $NextObjectGUID; - $thisfile_asf_dataobject['objectid_guid'] = $NextObjectGUIDtext; - $thisfile_asf_dataobject['objectsize'] = $NextObjectSize; - - $thisfile_asf_dataobject['fileid'] = substr($DataObjectData, $offset, 16); - $offset += 16; - $thisfile_asf_dataobject['fileid_guid'] = $this->BytestringToGUID($thisfile_asf_dataobject['fileid']); - $thisfile_asf_dataobject['total_data_packets'] = getid3_lib::LittleEndian2Int(substr($DataObjectData, $offset, 8)); - $offset += 8; - $thisfile_asf_dataobject['reserved'] = getid3_lib::LittleEndian2Int(substr($DataObjectData, $offset, 2)); - $offset += 2; - if ($thisfile_asf_dataobject['reserved'] != 0x0101) { - $info['warning'][] = 'data_object.reserved ('.getid3_lib::PrintHexBytes($thisfile_asf_dataobject['reserved']).') does not match expected value of "0x0101"'; - //return false; - break; - } - - // Data Packets array of: variable // - // * Error Correction Flags BYTE 8 // - // * * Error Correction Data Length bits 4 // if Error Correction Length Type == 00, size of Error Correction Data in bytes, else hardcoded: 0000 - // * * Opaque Data Present bits 1 // - // * * Error Correction Length Type bits 2 // number of bits for size of the error correction data. hardcoded: 00 - // * * Error Correction Present bits 1 // If set, use Opaque Data Packet structure, else use Payload structure - // * Error Correction Data - - $info['avdataoffset'] = ftell($this->getid3->fp); - fseek($this->getid3->fp, ($thisfile_asf_dataobject['objectsize'] - 50), SEEK_CUR); // skip actual audio/video data - $info['avdataend'] = ftell($this->getid3->fp); - break; - - case GETID3_ASF_Simple_Index_Object: - // Simple Index Object: (optional, recommended, one per video stream) - // Field Name Field Type Size (bits) - // Object ID GUID 128 // GUID for Simple Index object - GETID3_ASF_Data_Object - // Object Size QWORD 64 // size of Simple Index object, including 56 bytes of Simple Index Object header - // File ID GUID 128 // unique identifier. may be zero or identical to File ID field in Data Object and Header Object - // Index Entry Time Interval QWORD 64 // interval between index entries in 100-nanosecond units - // Maximum Packet Count DWORD 32 // maximum packet count for all index entries - // Index Entries Count DWORD 32 // number of Index Entries structures - // Index Entries array of: variable // - // * Packet Number DWORD 32 // number of the Data Packet associated with this index entry - // * Packet Count WORD 16 // number of Data Packets to sent at this index entry - - // shortcut - $thisfile_asf['simple_index_object'] = array(); - $thisfile_asf_simpleindexobject = &$thisfile_asf['simple_index_object']; - - $SimpleIndexObjectData = $NextObjectDataHeader.fread($this->getid3->fp, 56 - 24); - $offset = 24; - - $thisfile_asf_simpleindexobject['objectid'] = $NextObjectGUID; - $thisfile_asf_simpleindexobject['objectid_guid'] = $NextObjectGUIDtext; - $thisfile_asf_simpleindexobject['objectsize'] = $NextObjectSize; - - $thisfile_asf_simpleindexobject['fileid'] = substr($SimpleIndexObjectData, $offset, 16); - $offset += 16; - $thisfile_asf_simpleindexobject['fileid_guid'] = $this->BytestringToGUID($thisfile_asf_simpleindexobject['fileid']); - $thisfile_asf_simpleindexobject['index_entry_time_interval'] = getid3_lib::LittleEndian2Int(substr($SimpleIndexObjectData, $offset, 8)); - $offset += 8; - $thisfile_asf_simpleindexobject['maximum_packet_count'] = getid3_lib::LittleEndian2Int(substr($SimpleIndexObjectData, $offset, 4)); - $offset += 4; - $thisfile_asf_simpleindexobject['index_entries_count'] = getid3_lib::LittleEndian2Int(substr($SimpleIndexObjectData, $offset, 4)); - $offset += 4; - - $IndexEntriesData = $SimpleIndexObjectData.fread($this->getid3->fp, 6 * $thisfile_asf_simpleindexobject['index_entries_count']); - for ($IndexEntriesCounter = 0; $IndexEntriesCounter < $thisfile_asf_simpleindexobject['index_entries_count']; $IndexEntriesCounter++) { - $thisfile_asf_simpleindexobject['index_entries'][$IndexEntriesCounter]['packet_number'] = getid3_lib::LittleEndian2Int(substr($IndexEntriesData, $offset, 4)); - $offset += 4; - $thisfile_asf_simpleindexobject['index_entries'][$IndexEntriesCounter]['packet_count'] = getid3_lib::LittleEndian2Int(substr($IndexEntriesData, $offset, 4)); - $offset += 2; - } - - break; - - case GETID3_ASF_Index_Object: - // 6.2 ASF top-level Index Object (optional but recommended when appropriate, 0 or 1) - // Field Name Field Type Size (bits) - // Object ID GUID 128 // GUID for the Index Object - GETID3_ASF_Index_Object - // Object Size QWORD 64 // Specifies the size, in bytes, of the Index Object, including at least 34 bytes of Index Object header - // Index Entry Time Interval DWORD 32 // Specifies the time interval between each index entry in ms. - // Index Specifiers Count WORD 16 // Specifies the number of Index Specifiers structures in this Index Object. - // Index Blocks Count DWORD 32 // Specifies the number of Index Blocks structures in this Index Object. - - // Index Entry Time Interval DWORD 32 // Specifies the time interval between index entries in milliseconds. This value cannot be 0. - // Index Specifiers Count WORD 16 // Specifies the number of entries in the Index Specifiers list. Valid values are 1 and greater. - // Index Specifiers array of: varies // - // * Stream Number WORD 16 // Specifies the stream number that the Index Specifiers refer to. Valid values are between 1 and 127. - // * Index Type WORD 16 // Specifies Index Type values as follows: - // 1 = Nearest Past Data Packet - indexes point to the data packet whose presentation time is closest to the index entry time. - // 2 = Nearest Past Media Object - indexes point to the closest data packet containing an entire object or first fragment of an object. - // 3 = Nearest Past Cleanpoint. - indexes point to the closest data packet containing an entire object (or first fragment of an object) that has the Cleanpoint Flag set. - // Nearest Past Cleanpoint is the most common type of index. - // Index Entry Count DWORD 32 // Specifies the number of Index Entries in the block. - // * Block Positions QWORD varies // Specifies a list of byte offsets of the beginnings of the blocks relative to the beginning of the first Data Packet (i.e., the beginning of the Data Object + 50 bytes). The number of entries in this list is specified by the value of the Index Specifiers Count field. The order of those byte offsets is tied to the order in which Index Specifiers are listed. - // * Index Entries array of: varies // - // * * Offsets DWORD varies // An offset value of 0xffffffff indicates an invalid offset value - - // shortcut - $thisfile_asf['asf_index_object'] = array(); - $thisfile_asf_asfindexobject = &$thisfile_asf['asf_index_object']; - - $ASFIndexObjectData = $NextObjectDataHeader.fread($this->getid3->fp, 34 - 24); - $offset = 24; - - $thisfile_asf_asfindexobject['objectid'] = $NextObjectGUID; - $thisfile_asf_asfindexobject['objectid_guid'] = $NextObjectGUIDtext; - $thisfile_asf_asfindexobject['objectsize'] = $NextObjectSize; - - $thisfile_asf_asfindexobject['entry_time_interval'] = getid3_lib::LittleEndian2Int(substr($ASFIndexObjectData, $offset, 4)); - $offset += 4; - $thisfile_asf_asfindexobject['index_specifiers_count'] = getid3_lib::LittleEndian2Int(substr($ASFIndexObjectData, $offset, 2)); - $offset += 2; - $thisfile_asf_asfindexobject['index_blocks_count'] = getid3_lib::LittleEndian2Int(substr($ASFIndexObjectData, $offset, 4)); - $offset += 4; - - $ASFIndexObjectData .= fread($this->getid3->fp, 4 * $thisfile_asf_asfindexobject['index_specifiers_count']); - for ($IndexSpecifiersCounter = 0; $IndexSpecifiersCounter < $thisfile_asf_asfindexobject['index_specifiers_count']; $IndexSpecifiersCounter++) { - $IndexSpecifierStreamNumber = getid3_lib::LittleEndian2Int(substr($ASFIndexObjectData, $offset, 2)); - $offset += 2; - $thisfile_asf_asfindexobject['index_specifiers'][$IndexSpecifiersCounter]['stream_number'] = $IndexSpecifierStreamNumber; - $thisfile_asf_asfindexobject['index_specifiers'][$IndexSpecifiersCounter]['index_type'] = getid3_lib::LittleEndian2Int(substr($ASFIndexObjectData, $offset, 2)); - $offset += 2; - $thisfile_asf_asfindexobject['index_specifiers'][$IndexSpecifiersCounter]['index_type_text'] = $this->ASFIndexObjectIndexTypeLookup($thisfile_asf_asfindexobject['index_specifiers'][$IndexSpecifiersCounter]['index_type']); - } - - $ASFIndexObjectData .= fread($this->getid3->fp, 4); - $thisfile_asf_asfindexobject['index_entry_count'] = getid3_lib::LittleEndian2Int(substr($ASFIndexObjectData, $offset, 4)); - $offset += 4; - - $ASFIndexObjectData .= fread($this->getid3->fp, 8 * $thisfile_asf_asfindexobject['index_specifiers_count']); - for ($IndexSpecifiersCounter = 0; $IndexSpecifiersCounter < $thisfile_asf_asfindexobject['index_specifiers_count']; $IndexSpecifiersCounter++) { - $thisfile_asf_asfindexobject['block_positions'][$IndexSpecifiersCounter] = getid3_lib::LittleEndian2Int(substr($ASFIndexObjectData, $offset, 8)); - $offset += 8; - } - - $ASFIndexObjectData .= fread($this->getid3->fp, 4 * $thisfile_asf_asfindexobject['index_specifiers_count'] * $thisfile_asf_asfindexobject['index_entry_count']); - for ($IndexEntryCounter = 0; $IndexEntryCounter < $thisfile_asf_asfindexobject['index_entry_count']; $IndexEntryCounter++) { - for ($IndexSpecifiersCounter = 0; $IndexSpecifiersCounter < $thisfile_asf_asfindexobject['index_specifiers_count']; $IndexSpecifiersCounter++) { - $thisfile_asf_asfindexobject['offsets'][$IndexSpecifiersCounter][$IndexEntryCounter] = getid3_lib::LittleEndian2Int(substr($ASFIndexObjectData, $offset, 4)); - $offset += 4; - } - } - break; - - - default: - // Implementations shall ignore any standard or non-standard object that they do not know how to handle. - if ($this->GUIDname($NextObjectGUIDtext)) { - $info['warning'][] = 'unhandled GUID "'.$this->GUIDname($NextObjectGUIDtext).'" {'.$NextObjectGUIDtext.'} in ASF body at offset '.($offset - 16 - 8); - } else { - $info['warning'][] = 'unknown GUID {'.$NextObjectGUIDtext.'} in ASF body at offset '.(ftell($this->getid3->fp) - 16 - 8); - } - fseek($this->getid3->fp, ($NextObjectSize - 16 - 8), SEEK_CUR); - break; - } - } - - if (isset($thisfile_asf_codeclistobject['codec_entries']) && is_array($thisfile_asf_codeclistobject['codec_entries'])) { - foreach ($thisfile_asf_codeclistobject['codec_entries'] as $streamnumber => $streamdata) { - switch ($streamdata['information']) { - case 'WMV1': - case 'WMV2': - case 'WMV3': - case 'MSS1': - case 'MSS2': - case 'WMVA': - case 'WVC1': - case 'WMVP': - case 'WVP2': - $thisfile_video['dataformat'] = 'wmv'; - $info['mime_type'] = 'video/x-ms-wmv'; - break; - - case 'MP42': - case 'MP43': - case 'MP4S': - case 'mp4s': - $thisfile_video['dataformat'] = 'asf'; - $info['mime_type'] = 'video/x-ms-asf'; - break; - - default: - switch ($streamdata['type_raw']) { - case 1: - if (strstr($this->TrimConvert($streamdata['name']), 'Windows Media')) { - $thisfile_video['dataformat'] = 'wmv'; - if ($info['mime_type'] == 'video/x-ms-asf') { - $info['mime_type'] = 'video/x-ms-wmv'; - } - } - break; - - case 2: - if (strstr($this->TrimConvert($streamdata['name']), 'Windows Media')) { - $thisfile_audio['dataformat'] = 'wma'; - if ($info['mime_type'] == 'video/x-ms-asf') { - $info['mime_type'] = 'audio/x-ms-wma'; - } - } - break; - - } - break; - } - } - } - - switch (isset($thisfile_audio['codec']) ? $thisfile_audio['codec'] : '') { - case 'MPEG Layer-3': - $thisfile_audio['dataformat'] = 'mp3'; - break; - - default: - break; - } - - if (isset($thisfile_asf_codeclistobject['codec_entries'])) { - foreach ($thisfile_asf_codeclistobject['codec_entries'] as $streamnumber => $streamdata) { - switch ($streamdata['type_raw']) { - - case 1: // video - $thisfile_video['encoder'] = $this->TrimConvert($thisfile_asf_codeclistobject['codec_entries'][$streamnumber]['name']); - break; - - case 2: // audio - $thisfile_audio['encoder'] = $this->TrimConvert($thisfile_asf_codeclistobject['codec_entries'][$streamnumber]['name']); - - // AH 2003-10-01 - $thisfile_audio['encoder_options'] = $this->TrimConvert($thisfile_asf_codeclistobject['codec_entries'][0]['description']); - - $thisfile_audio['codec'] = $thisfile_audio['encoder']; - break; - - default: - $info['warning'][] = 'Unknown streamtype: [codec_list_object][codec_entries]['.$streamnumber.'][type_raw] == '.$streamdata['type_raw']; - break; - - } - } - } - - if (isset($info['audio'])) { - $thisfile_audio['lossless'] = (isset($thisfile_audio['lossless']) ? $thisfile_audio['lossless'] : false); - $thisfile_audio['dataformat'] = (!empty($thisfile_audio['dataformat']) ? $thisfile_audio['dataformat'] : 'asf'); - } - if (!empty($thisfile_video['dataformat'])) { - $thisfile_video['lossless'] = (isset($thisfile_audio['lossless']) ? $thisfile_audio['lossless'] : false); - $thisfile_video['pixel_aspect_ratio'] = (isset($thisfile_audio['pixel_aspect_ratio']) ? $thisfile_audio['pixel_aspect_ratio'] : (float) 1); - $thisfile_video['dataformat'] = (!empty($thisfile_video['dataformat']) ? $thisfile_video['dataformat'] : 'asf'); - } - if (!empty($thisfile_video['streams'])) { - $thisfile_video['streams']['resolution_x'] = 0; - $thisfile_video['streams']['resolution_y'] = 0; - foreach ($thisfile_video['streams'] as $key => $valuearray) { - if (($valuearray['resolution_x'] > $thisfile_video['streams']['resolution_x']) || ($valuearray['resolution_y'] > $thisfile_video['streams']['resolution_y'])) { - $thisfile_video['resolution_x'] = $valuearray['resolution_x']; - $thisfile_video['resolution_y'] = $valuearray['resolution_y']; - } - } - } - $info['bitrate'] = (isset($thisfile_audio['bitrate']) ? $thisfile_audio['bitrate'] : 0) + (isset($thisfile_video['bitrate']) ? $thisfile_video['bitrate'] : 0); - - if ((!isset($info['playtime_seconds']) || ($info['playtime_seconds'] <= 0)) && ($info['bitrate'] > 0)) { - $info['playtime_seconds'] = ($info['filesize'] - $info['avdataoffset']) / ($info['bitrate'] / 8); - } - - return true; - } - - public static function ASFCodecListObjectTypeLookup($CodecListType) { - static $ASFCodecListObjectTypeLookup = array(); - if (empty($ASFCodecListObjectTypeLookup)) { - $ASFCodecListObjectTypeLookup[0x0001] = 'Video Codec'; - $ASFCodecListObjectTypeLookup[0x0002] = 'Audio Codec'; - $ASFCodecListObjectTypeLookup[0xFFFF] = 'Unknown Codec'; - } - - return (isset($ASFCodecListObjectTypeLookup[$CodecListType]) ? $ASFCodecListObjectTypeLookup[$CodecListType] : 'Invalid Codec Type'); - } - - public static function KnownGUIDs() { - static $GUIDarray = array( - 'GETID3_ASF_Extended_Stream_Properties_Object' => '14E6A5CB-C672-4332-8399-A96952065B5A', - 'GETID3_ASF_Padding_Object' => '1806D474-CADF-4509-A4BA-9AABCB96AAE8', - 'GETID3_ASF_Payload_Ext_Syst_Pixel_Aspect_Ratio' => '1B1EE554-F9EA-4BC8-821A-376B74E4C4B8', - 'GETID3_ASF_Script_Command_Object' => '1EFB1A30-0B62-11D0-A39B-00A0C90348F6', - 'GETID3_ASF_No_Error_Correction' => '20FB5700-5B55-11CF-A8FD-00805F5C442B', - 'GETID3_ASF_Content_Branding_Object' => '2211B3FA-BD23-11D2-B4B7-00A0C955FC6E', - 'GETID3_ASF_Content_Encryption_Object' => '2211B3FB-BD23-11D2-B4B7-00A0C955FC6E', - 'GETID3_ASF_Digital_Signature_Object' => '2211B3FC-BD23-11D2-B4B7-00A0C955FC6E', - 'GETID3_ASF_Extended_Content_Encryption_Object' => '298AE614-2622-4C17-B935-DAE07EE9289C', - 'GETID3_ASF_Simple_Index_Object' => '33000890-E5B1-11CF-89F4-00A0C90349CB', - 'GETID3_ASF_Degradable_JPEG_Media' => '35907DE0-E415-11CF-A917-00805F5C442B', - 'GETID3_ASF_Payload_Extension_System_Timecode' => '399595EC-8667-4E2D-8FDB-98814CE76C1E', - 'GETID3_ASF_Binary_Media' => '3AFB65E2-47EF-40F2-AC2C-70A90D71D343', - 'GETID3_ASF_Timecode_Index_Object' => '3CB73FD0-0C4A-4803-953D-EDF7B6228F0C', - 'GETID3_ASF_Metadata_Library_Object' => '44231C94-9498-49D1-A141-1D134E457054', - 'GETID3_ASF_Reserved_3' => '4B1ACBE3-100B-11D0-A39B-00A0C90348F6', - 'GETID3_ASF_Reserved_4' => '4CFEDB20-75F6-11CF-9C0F-00A0C90349CB', - 'GETID3_ASF_Command_Media' => '59DACFC0-59E6-11D0-A3AC-00A0C90348F6', - 'GETID3_ASF_Header_Extension_Object' => '5FBF03B5-A92E-11CF-8EE3-00C00C205365', - 'GETID3_ASF_Media_Object_Index_Parameters_Obj' => '6B203BAD-3F11-4E84-ACA8-D7613DE2CFA7', - 'GETID3_ASF_Header_Object' => '75B22630-668E-11CF-A6D9-00AA0062CE6C', - 'GETID3_ASF_Content_Description_Object' => '75B22633-668E-11CF-A6D9-00AA0062CE6C', - 'GETID3_ASF_Error_Correction_Object' => '75B22635-668E-11CF-A6D9-00AA0062CE6C', - 'GETID3_ASF_Data_Object' => '75B22636-668E-11CF-A6D9-00AA0062CE6C', - 'GETID3_ASF_Web_Stream_Media_Subtype' => '776257D4-C627-41CB-8F81-7AC7FF1C40CC', - 'GETID3_ASF_Stream_Bitrate_Properties_Object' => '7BF875CE-468D-11D1-8D82-006097C9A2B2', - 'GETID3_ASF_Language_List_Object' => '7C4346A9-EFE0-4BFC-B229-393EDE415C85', - 'GETID3_ASF_Codec_List_Object' => '86D15240-311D-11D0-A3A4-00A0C90348F6', - 'GETID3_ASF_Reserved_2' => '86D15241-311D-11D0-A3A4-00A0C90348F6', - 'GETID3_ASF_File_Properties_Object' => '8CABDCA1-A947-11CF-8EE4-00C00C205365', - 'GETID3_ASF_File_Transfer_Media' => '91BD222C-F21C-497A-8B6D-5AA86BFC0185', - 'GETID3_ASF_Old_RTP_Extension_Data' => '96800C63-4C94-11D1-837B-0080C7A37F95', - 'GETID3_ASF_Advanced_Mutual_Exclusion_Object' => 'A08649CF-4775-4670-8A16-6E35357566CD', - 'GETID3_ASF_Bandwidth_Sharing_Object' => 'A69609E6-517B-11D2-B6AF-00C04FD908E9', - 'GETID3_ASF_Reserved_1' => 'ABD3D211-A9BA-11cf-8EE6-00C00C205365', - 'GETID3_ASF_Bandwidth_Sharing_Exclusive' => 'AF6060AA-5197-11D2-B6AF-00C04FD908E9', - 'GETID3_ASF_Bandwidth_Sharing_Partial' => 'AF6060AB-5197-11D2-B6AF-00C04FD908E9', - 'GETID3_ASF_JFIF_Media' => 'B61BE100-5B4E-11CF-A8FD-00805F5C442B', - 'GETID3_ASF_Stream_Properties_Object' => 'B7DC0791-A9B7-11CF-8EE6-00C00C205365', - 'GETID3_ASF_Video_Media' => 'BC19EFC0-5B4D-11CF-A8FD-00805F5C442B', - 'GETID3_ASF_Audio_Spread' => 'BFC3CD50-618F-11CF-8BB2-00AA00B4E220', - 'GETID3_ASF_Metadata_Object' => 'C5F8CBEA-5BAF-4877-8467-AA8C44FA4CCA', - 'GETID3_ASF_Payload_Ext_Syst_Sample_Duration' => 'C6BD9450-867F-4907-83A3-C77921B733AD', - 'GETID3_ASF_Group_Mutual_Exclusion_Object' => 'D1465A40-5A79-4338-B71B-E36B8FD6C249', - 'GETID3_ASF_Extended_Content_Description_Object' => 'D2D0A440-E307-11D2-97F0-00A0C95EA850', - 'GETID3_ASF_Stream_Prioritization_Object' => 'D4FED15B-88D3-454F-81F0-ED5C45999E24', - 'GETID3_ASF_Payload_Ext_System_Content_Type' => 'D590DC20-07BC-436C-9CF7-F3BBFBF1A4DC', - 'GETID3_ASF_Old_File_Properties_Object' => 'D6E229D0-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_ASF_Header_Object' => 'D6E229D1-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_ASF_Data_Object' => 'D6E229D2-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Index_Object' => 'D6E229D3-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Stream_Properties_Object' => 'D6E229D4-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Content_Description_Object' => 'D6E229D5-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Script_Command_Object' => 'D6E229D6-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Marker_Object' => 'D6E229D7-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Component_Download_Object' => 'D6E229D8-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Stream_Group_Object' => 'D6E229D9-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Scalable_Object' => 'D6E229DA-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Prioritization_Object' => 'D6E229DB-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Bitrate_Mutual_Exclusion_Object' => 'D6E229DC-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Inter_Media_Dependency_Object' => 'D6E229DD-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Rating_Object' => 'D6E229DE-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Index_Parameters_Object' => 'D6E229DF-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Color_Table_Object' => 'D6E229E0-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Language_List_Object' => 'D6E229E1-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Audio_Media' => 'D6E229E2-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Video_Media' => 'D6E229E3-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Image_Media' => 'D6E229E4-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Timecode_Media' => 'D6E229E5-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Text_Media' => 'D6E229E6-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_MIDI_Media' => 'D6E229E7-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Command_Media' => 'D6E229E8-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_No_Error_Concealment' => 'D6E229EA-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Scrambled_Audio' => 'D6E229EB-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_No_Color_Table' => 'D6E229EC-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_SMPTE_Time' => 'D6E229ED-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_ASCII_Text' => 'D6E229EE-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Unicode_Text' => 'D6E229EF-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_HTML_Text' => 'D6E229F0-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_URL_Command' => 'D6E229F1-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Filename_Command' => 'D6E229F2-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_ACM_Codec' => 'D6E229F3-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_VCM_Codec' => 'D6E229F4-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_QuickTime_Codec' => 'D6E229F5-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_DirectShow_Transform_Filter' => 'D6E229F6-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_DirectShow_Rendering_Filter' => 'D6E229F7-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_No_Enhancement' => 'D6E229F8-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Unknown_Enhancement_Type' => 'D6E229F9-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Temporal_Enhancement' => 'D6E229FA-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Spatial_Enhancement' => 'D6E229FB-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Quality_Enhancement' => 'D6E229FC-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Number_of_Channels_Enhancement' => 'D6E229FD-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Frequency_Response_Enhancement' => 'D6E229FE-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Media_Object' => 'D6E229FF-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Mutex_Language' => 'D6E22A00-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Mutex_Bitrate' => 'D6E22A01-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Mutex_Unknown' => 'D6E22A02-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_ASF_Placeholder_Object' => 'D6E22A0E-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Old_Data_Unit_Extension_Object' => 'D6E22A0F-35DA-11D1-9034-00A0C90349BE', - 'GETID3_ASF_Web_Stream_Format' => 'DA1E6B13-8359-4050-B398-388E965BF00C', - 'GETID3_ASF_Payload_Ext_System_File_Name' => 'E165EC0E-19ED-45D7-B4A7-25CBD1E28E9B', - 'GETID3_ASF_Marker_Object' => 'F487CD01-A951-11CF-8EE6-00C00C205365', - 'GETID3_ASF_Timecode_Index_Parameters_Object' => 'F55E496D-9797-4B5D-8C8B-604DFE9BFB24', - 'GETID3_ASF_Audio_Media' => 'F8699E40-5B4D-11CF-A8FD-00805F5C442B', - 'GETID3_ASF_Media_Object_Index_Object' => 'FEB103F8-12AD-4C64-840F-2A1D2F7AD48C', - 'GETID3_ASF_Alt_Extended_Content_Encryption_Obj' => 'FF889EF1-ADEE-40DA-9E71-98704BB928CE', - 'GETID3_ASF_Index_Placeholder_Object' => 'D9AADE20-7C17-4F9C-BC28-8555DD98E2A2', // http://cpan.uwinnipeg.ca/htdocs/Audio-WMA/Audio/WMA.pm.html - 'GETID3_ASF_Compatibility_Object' => '26F18B5D-4584-47EC-9F5F-0E651F0452C9', // http://cpan.uwinnipeg.ca/htdocs/Audio-WMA/Audio/WMA.pm.html - ); - return $GUIDarray; - } - - public static function GUIDname($GUIDstring) { - static $GUIDarray = array(); - if (empty($GUIDarray)) { - $GUIDarray = self::KnownGUIDs(); - } - return array_search($GUIDstring, $GUIDarray); - } - - public static function ASFIndexObjectIndexTypeLookup($id) { - static $ASFIndexObjectIndexTypeLookup = array(); - if (empty($ASFIndexObjectIndexTypeLookup)) { - $ASFIndexObjectIndexTypeLookup[1] = 'Nearest Past Data Packet'; - $ASFIndexObjectIndexTypeLookup[2] = 'Nearest Past Media Object'; - $ASFIndexObjectIndexTypeLookup[3] = 'Nearest Past Cleanpoint'; - } - return (isset($ASFIndexObjectIndexTypeLookup[$id]) ? $ASFIndexObjectIndexTypeLookup[$id] : 'invalid'); - } - - public static function GUIDtoBytestring($GUIDstring) { - // Microsoft defines these 16-byte (128-bit) GUIDs in the strangest way: - // first 4 bytes are in little-endian order - // next 2 bytes are appended in little-endian order - // next 2 bytes are appended in little-endian order - // next 2 bytes are appended in big-endian order - // next 6 bytes are appended in big-endian order - - // AaBbCcDd-EeFf-GgHh-IiJj-KkLlMmNnOoPp is stored as this 16-byte string: - // $Dd $Cc $Bb $Aa $Ff $Ee $Hh $Gg $Ii $Jj $Kk $Ll $Mm $Nn $Oo $Pp - - $hexbytecharstring = chr(hexdec(substr($GUIDstring, 6, 2))); - $hexbytecharstring .= chr(hexdec(substr($GUIDstring, 4, 2))); - $hexbytecharstring .= chr(hexdec(substr($GUIDstring, 2, 2))); - $hexbytecharstring .= chr(hexdec(substr($GUIDstring, 0, 2))); - - $hexbytecharstring .= chr(hexdec(substr($GUIDstring, 11, 2))); - $hexbytecharstring .= chr(hexdec(substr($GUIDstring, 9, 2))); - - $hexbytecharstring .= chr(hexdec(substr($GUIDstring, 16, 2))); - $hexbytecharstring .= chr(hexdec(substr($GUIDstring, 14, 2))); - - $hexbytecharstring .= chr(hexdec(substr($GUIDstring, 19, 2))); - $hexbytecharstring .= chr(hexdec(substr($GUIDstring, 21, 2))); - - $hexbytecharstring .= chr(hexdec(substr($GUIDstring, 24, 2))); - $hexbytecharstring .= chr(hexdec(substr($GUIDstring, 26, 2))); - $hexbytecharstring .= chr(hexdec(substr($GUIDstring, 28, 2))); - $hexbytecharstring .= chr(hexdec(substr($GUIDstring, 30, 2))); - $hexbytecharstring .= chr(hexdec(substr($GUIDstring, 32, 2))); - $hexbytecharstring .= chr(hexdec(substr($GUIDstring, 34, 2))); - - return $hexbytecharstring; - } - - public static function BytestringToGUID($Bytestring) { - $GUIDstring = str_pad(dechex(ord($Bytestring{3})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{2})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{1})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{0})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= '-'; - $GUIDstring .= str_pad(dechex(ord($Bytestring{5})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{4})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= '-'; - $GUIDstring .= str_pad(dechex(ord($Bytestring{7})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{6})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= '-'; - $GUIDstring .= str_pad(dechex(ord($Bytestring{8})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{9})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= '-'; - $GUIDstring .= str_pad(dechex(ord($Bytestring{10})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{11})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{12})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{13})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{14})), 2, '0', STR_PAD_LEFT); - $GUIDstring .= str_pad(dechex(ord($Bytestring{15})), 2, '0', STR_PAD_LEFT); - - return strtoupper($GUIDstring); - } - - public static function FILETIMEtoUNIXtime($FILETIME, $round=true) { - // FILETIME is a 64-bit unsigned integer representing - // the number of 100-nanosecond intervals since January 1, 1601 - // UNIX timestamp is number of seconds since January 1, 1970 - // 116444736000000000 = 10000000 * 60 * 60 * 24 * 365 * 369 + 89 leap days - if ($round) { - return intval(round(($FILETIME - 116444736000000000) / 10000000)); - } - return ($FILETIME - 116444736000000000) / 10000000; - } - - public static function WMpictureTypeLookup($WMpictureType) { - static $WMpictureTypeLookup = array(); - if (empty($WMpictureTypeLookup)) { - $WMpictureTypeLookup[0x03] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'Front Cover'); - $WMpictureTypeLookup[0x04] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'Back Cover'); - $WMpictureTypeLookup[0x00] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'User Defined'); - $WMpictureTypeLookup[0x05] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'Leaflet Page'); - $WMpictureTypeLookup[0x06] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'Media Label'); - $WMpictureTypeLookup[0x07] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'Lead Artist'); - $WMpictureTypeLookup[0x08] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'Artist'); - $WMpictureTypeLookup[0x09] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'Conductor'); - $WMpictureTypeLookup[0x0A] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'Band'); - $WMpictureTypeLookup[0x0B] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'Composer'); - $WMpictureTypeLookup[0x0C] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'Lyricist'); - $WMpictureTypeLookup[0x0D] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'Recording Location'); - $WMpictureTypeLookup[0x0E] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'During Recording'); - $WMpictureTypeLookup[0x0F] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'During Performance'); - $WMpictureTypeLookup[0x10] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'Video Screen Capture'); - $WMpictureTypeLookup[0x12] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'Illustration'); - $WMpictureTypeLookup[0x13] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'Band Logotype'); - $WMpictureTypeLookup[0x14] = getid3_lib::iconv_fallback('ISO-8859-1', 'UTF-16LE', 'Publisher Logotype'); - } - return (isset($WMpictureTypeLookup[$WMpictureType]) ? $WMpictureTypeLookup[$WMpictureType] : ''); - } - - public function ASF_HeaderExtensionObjectDataParse(&$asf_header_extension_object_data, &$unhandled_sections) { - // http://msdn.microsoft.com/en-us/library/bb643323.aspx - - $offset = 0; - $objectOffset = 0; - $HeaderExtensionObjectParsed = array(); - while ($objectOffset < strlen($asf_header_extension_object_data)) { - $offset = $objectOffset; - $thisObject = array(); - - $thisObject['guid'] = substr($asf_header_extension_object_data, $offset, 16); - $offset += 16; - $thisObject['guid_text'] = $this->BytestringToGUID($thisObject['guid']); - $thisObject['guid_name'] = $this->GUIDname($thisObject['guid_text']); - - $thisObject['size'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 8)); - $offset += 8; - if ($thisObject['size'] <= 0) { - break; - } - - switch ($thisObject['guid']) { - case GETID3_ASF_Extended_Stream_Properties_Object: - $thisObject['start_time'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 8)); - $offset += 8; - $thisObject['start_time_unix'] = $this->FILETIMEtoUNIXtime($thisObject['start_time']); - - $thisObject['end_time'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 8)); - $offset += 8; - $thisObject['end_time_unix'] = $this->FILETIMEtoUNIXtime($thisObject['end_time']); - - $thisObject['data_bitrate'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); - $offset += 4; - - $thisObject['buffer_size'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); - $offset += 4; - - $thisObject['initial_buffer_fullness'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); - $offset += 4; - - $thisObject['alternate_data_bitrate'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); - $offset += 4; - - $thisObject['alternate_buffer_size'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); - $offset += 4; - - $thisObject['alternate_initial_buffer_fullness'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); - $offset += 4; - - $thisObject['maximum_object_size'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); - $offset += 4; - - $thisObject['flags_raw'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); - $offset += 4; - $thisObject['flags']['reliable'] = (bool) $thisObject['flags_raw'] & 0x00000001; - $thisObject['flags']['seekable'] = (bool) $thisObject['flags_raw'] & 0x00000002; - $thisObject['flags']['no_cleanpoints'] = (bool) $thisObject['flags_raw'] & 0x00000004; - $thisObject['flags']['resend_live_cleanpoints'] = (bool) $thisObject['flags_raw'] & 0x00000008; - - $thisObject['stream_number'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); - $offset += 2; - - $thisObject['stream_language_id_index'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); - $offset += 2; - - $thisObject['average_time_per_frame'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); - $offset += 4; - - $thisObject['stream_name_count'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); - $offset += 2; - - $thisObject['payload_extension_system_count'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); - $offset += 2; - - for ($i = 0; $i < $thisObject['stream_name_count']; $i++) { - $streamName = array(); - - $streamName['language_id_index'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); - $offset += 2; - - $streamName['stream_name_length'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); - $offset += 2; - - $streamName['stream_name'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, $streamName['stream_name_length'])); - $offset += $streamName['stream_name_length']; - - $thisObject['stream_names'][$i] = $streamName; - } - - for ($i = 0; $i < $thisObject['payload_extension_system_count']; $i++) { - $payloadExtensionSystem = array(); - - $payloadExtensionSystem['extension_system_id'] = substr($asf_header_extension_object_data, $offset, 16); - $offset += 16; - $payloadExtensionSystem['extension_system_id_text'] = $this->BytestringToGUID($payloadExtensionSystem['extension_system_id']); - - $payloadExtensionSystem['extension_system_size'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); - $offset += 2; - if ($payloadExtensionSystem['extension_system_size'] <= 0) { - break 2; - } - - $payloadExtensionSystem['extension_system_info_length'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); - $offset += 4; - - $payloadExtensionSystem['extension_system_info_length'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, $payloadExtensionSystem['extension_system_info_length'])); - $offset += $payloadExtensionSystem['extension_system_info_length']; - - $thisObject['payload_extension_systems'][$i] = $payloadExtensionSystem; - } - - break; - - case GETID3_ASF_Padding_Object: - // padding, skip it - break; - - case GETID3_ASF_Metadata_Object: - $thisObject['description_record_counts'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); - $offset += 2; - - for ($i = 0; $i < $thisObject['description_record_counts']; $i++) { - $descriptionRecord = array(); - - $descriptionRecord['reserved_1'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); // must be zero - $offset += 2; - - $descriptionRecord['stream_number'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); - $offset += 2; - - $descriptionRecord['name_length'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); - $offset += 2; - - $descriptionRecord['data_type'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); - $offset += 2; - $descriptionRecord['data_type_text'] = $this->ASFmetadataLibraryObjectDataTypeLookup($descriptionRecord['data_type']); - - $descriptionRecord['data_length'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); - $offset += 4; - - $descriptionRecord['name'] = substr($asf_header_extension_object_data, $offset, $descriptionRecord['name_length']); - $offset += $descriptionRecord['name_length']; - - $descriptionRecord['data'] = substr($asf_header_extension_object_data, $offset, $descriptionRecord['data_length']); - $offset += $descriptionRecord['data_length']; - switch ($descriptionRecord['data_type']) { - case 0x0000: // Unicode string - break; - - case 0x0001: // BYTE array - // do nothing - break; - - case 0x0002: // BOOL - $descriptionRecord['data'] = (bool) getid3_lib::LittleEndian2Int($descriptionRecord['data']); - break; - - case 0x0003: // DWORD - case 0x0004: // QWORD - case 0x0005: // WORD - $descriptionRecord['data'] = getid3_lib::LittleEndian2Int($descriptionRecord['data']); - break; - - case 0x0006: // GUID - $descriptionRecord['data_text'] = $this->BytestringToGUID($descriptionRecord['data']); - break; - } - - $thisObject['description_record'][$i] = $descriptionRecord; - } - break; - - case GETID3_ASF_Language_List_Object: - $thisObject['language_id_record_counts'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); - $offset += 2; - - for ($i = 0; $i < $thisObject['language_id_record_counts']; $i++) { - $languageIDrecord = array(); - - $languageIDrecord['language_id_length'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 1)); - $offset += 1; - - $languageIDrecord['language_id'] = substr($asf_header_extension_object_data, $offset, $languageIDrecord['language_id_length']); - $offset += $languageIDrecord['language_id_length']; - - $thisObject['language_id_record'][$i] = $languageIDrecord; - } - break; - - case GETID3_ASF_Metadata_Library_Object: - $thisObject['description_records_count'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); - $offset += 2; - - for ($i = 0; $i < $thisObject['description_records_count']; $i++) { - $descriptionRecord = array(); - - $descriptionRecord['language_list_index'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); - $offset += 2; - - $descriptionRecord['stream_number'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); - $offset += 2; - - $descriptionRecord['name_length'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); - $offset += 2; - - $descriptionRecord['data_type'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 2)); - $offset += 2; - $descriptionRecord['data_type_text'] = $this->ASFmetadataLibraryObjectDataTypeLookup($descriptionRecord['data_type']); - - $descriptionRecord['data_length'] = getid3_lib::LittleEndian2Int(substr($asf_header_extension_object_data, $offset, 4)); - $offset += 4; - - $descriptionRecord['name'] = substr($asf_header_extension_object_data, $offset, $descriptionRecord['name_length']); - $offset += $descriptionRecord['name_length']; - - $descriptionRecord['data'] = substr($asf_header_extension_object_data, $offset, $descriptionRecord['data_length']); - $offset += $descriptionRecord['data_length']; - - if (preg_match('#^WM/Picture$#', str_replace("\x00", '', trim($descriptionRecord['name'])))) { - $WMpicture = $this->ASF_WMpicture($descriptionRecord['data']); - foreach ($WMpicture as $key => $value) { - $descriptionRecord['data'] = $WMpicture; - } - unset($WMpicture); - } - - $thisObject['description_record'][$i] = $descriptionRecord; - } - break; - - default: - $unhandled_sections++; - if ($this->GUIDname($thisObject['guid_text'])) { - $this->getid3->info['warning'][] = 'unhandled Header Extension Object GUID "'.$this->GUIDname($thisObject['guid_text']).'" {'.$thisObject['guid_text'].'} at offset '.($offset - 16 - 8); - } else { - $this->getid3->info['warning'][] = 'unknown Header Extension Object GUID {'.$thisObject['guid_text'].'} in at offset '.($offset - 16 - 8); - } - break; - } - $HeaderExtensionObjectParsed[] = $thisObject; - - $objectOffset += $thisObject['size']; - } - return $HeaderExtensionObjectParsed; - } - - - public static function ASFmetadataLibraryObjectDataTypeLookup($id) { - static $ASFmetadataLibraryObjectDataTypeLookup = array( - 0x0000 => 'Unicode string', // The data consists of a sequence of Unicode characters - 0x0001 => 'BYTE array', // The type of the data is implementation-specific - 0x0002 => 'BOOL', // The data is 2 bytes long and should be interpreted as a 16-bit unsigned integer. Only 0x0000 or 0x0001 are permitted values - 0x0003 => 'DWORD', // The data is 4 bytes long and should be interpreted as a 32-bit unsigned integer - 0x0004 => 'QWORD', // The data is 8 bytes long and should be interpreted as a 64-bit unsigned integer - 0x0005 => 'WORD', // The data is 2 bytes long and should be interpreted as a 16-bit unsigned integer - 0x0006 => 'GUID', // The data is 16 bytes long and should be interpreted as a 128-bit GUID - ); - return (isset($ASFmetadataLibraryObjectDataTypeLookup[$id]) ? $ASFmetadataLibraryObjectDataTypeLookup[$id] : 'invalid'); - } - - public function ASF_WMpicture(&$data) { - //typedef struct _WMPicture{ - // LPWSTR pwszMIMEType; - // BYTE bPictureType; - // LPWSTR pwszDescription; - // DWORD dwDataLen; - // BYTE* pbData; - //} WM_PICTURE; - - $WMpicture = array(); - - $offset = 0; - $WMpicture['image_type_id'] = getid3_lib::LittleEndian2Int(substr($data, $offset, 1)); - $offset += 1; - $WMpicture['image_type'] = $this->WMpictureTypeLookup($WMpicture['image_type_id']); - $WMpicture['image_size'] = getid3_lib::LittleEndian2Int(substr($data, $offset, 4)); - $offset += 4; - - $WMpicture['image_mime'] = ''; - do { - $next_byte_pair = substr($data, $offset, 2); - $offset += 2; - $WMpicture['image_mime'] .= $next_byte_pair; - } while ($next_byte_pair !== "\x00\x00"); - - $WMpicture['image_description'] = ''; - do { - $next_byte_pair = substr($data, $offset, 2); - $offset += 2; - $WMpicture['image_description'] .= $next_byte_pair; - } while ($next_byte_pair !== "\x00\x00"); - - $WMpicture['dataoffset'] = $offset; - $WMpicture['data'] = substr($data, $offset); - - $imageinfo = array(); - $WMpicture['image_mime'] = ''; - $imagechunkcheck = getid3_lib::GetDataImageSize($WMpicture['data'], $imageinfo); - unset($imageinfo); - if (!empty($imagechunkcheck)) { - $WMpicture['image_mime'] = image_type_to_mime_type($imagechunkcheck[2]); - } - if (!isset($this->getid3->info['asf']['comments']['picture'])) { - $this->getid3->info['asf']['comments']['picture'] = array(); - } - $this->getid3->info['asf']['comments']['picture'][] = array('data'=>$WMpicture['data'], 'image_mime'=>$WMpicture['image_mime']); - - return $WMpicture; - } - - - // Remove terminator 00 00 and convert UTF-16LE to Latin-1 - public static function TrimConvert($string) { - return trim(getid3_lib::iconv_fallback('UTF-16LE', 'ISO-8859-1', self::TrimTerm($string)), ' '); - } - - - // Remove terminator 00 00 - public static function TrimTerm($string) { - // remove terminator, only if present (it should be, but...) - if (substr($string, -2) === "\x00\x00") { - $string = substr($string, 0, -2); - } - return $string; - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio-video.bink.php b/src/Classes/Vendor/getid3/module.audio-video.bink.php deleted file mode 100755 index 192627665..000000000 --- a/src/Classes/Vendor/getid3/module.audio-video.bink.php +++ /dev/null @@ -1,71 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.bink.php // -// module for analyzing Bink or Smacker audio-video files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_bink extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - -$info['error'][] = 'Bink / Smacker files not properly processed by this version of getID3() ['.$this->getid3->version().']'; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $fileTypeID = fread($this->getid3->fp, 3); - switch ($fileTypeID) { - case 'BIK': - return $this->ParseBink(); - break; - - case 'SMK': - return $this->ParseSmacker(); - break; - - default: - $info['error'][] = 'Expecting "BIK" or "SMK" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($fileTypeID).'"'; - return false; - break; - } - - return true; - - } - - public function ParseBink() { - $info = &$this->getid3->info; - $info['fileformat'] = 'bink'; - $info['video']['dataformat'] = 'bink'; - - $fileData = 'BIK'.fread($this->getid3->fp, 13); - - $info['bink']['data_size'] = getid3_lib::LittleEndian2Int(substr($fileData, 4, 4)); - $info['bink']['frame_count'] = getid3_lib::LittleEndian2Int(substr($fileData, 8, 2)); - - if (($info['avdataend'] - $info['avdataoffset']) != ($info['bink']['data_size'] + 8)) { - $info['error'][] = 'Probably truncated file: expecting '.$info['bink']['data_size'].' bytes, found '.($info['avdataend'] - $info['avdataoffset']); - } - - return true; - } - - public function ParseSmacker() { - $info = &$this->getid3->info; - $info['fileformat'] = 'smacker'; - $info['video']['dataformat'] = 'smacker'; - - return true; - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio-video.flv.php b/src/Classes/Vendor/getid3/module.audio-video.flv.php deleted file mode 100755 index aef8a2fba..000000000 --- a/src/Classes/Vendor/getid3/module.audio-video.flv.php +++ /dev/null @@ -1,729 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -// // -// FLV module by Seth Kaufman // -// // -// * version 0.1 (26 June 2005) // -// // -// // -// * version 0.1.1 (15 July 2005) // -// minor modifications by James Heinrich // -// // -// * version 0.2 (22 February 2006) // -// Support for On2 VP6 codec and meta information // -// by Steve Webster // -// // -// * version 0.3 (15 June 2006) // -// Modified to not read entire file into memory // -// by James Heinrich // -// // -// * version 0.4 (07 December 2007) // -// Bugfixes for incorrectly parsed FLV dimensions // -// and incorrect parsing of onMetaTag // -// by Evgeny Moysevich // -// // -// * version 0.5 (21 May 2009) // -// Fixed parsing of audio tags and added additional codec // -// details. The duration is now read from onMetaTag (if // -// exists), rather than parsing whole file // -// by Nigel Barnes // -// // -// * version 0.6 (24 May 2009) // -// Better parsing of files with h264 video // -// by Evgeny Moysevich // -// // -// * version 0.6.1 (30 May 2011) // -// prevent infinite loops in expGolombUe() // -// // -///////////////////////////////////////////////////////////////// -// // -// module.audio-video.flv.php // -// module for analyzing Shockwave Flash Video files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - -define('GETID3_FLV_TAG_AUDIO', 8); -define('GETID3_FLV_TAG_VIDEO', 9); -define('GETID3_FLV_TAG_META', 18); - -define('GETID3_FLV_VIDEO_H263', 2); -define('GETID3_FLV_VIDEO_SCREEN', 3); -define('GETID3_FLV_VIDEO_VP6FLV', 4); -define('GETID3_FLV_VIDEO_VP6FLV_ALPHA', 5); -define('GETID3_FLV_VIDEO_SCREENV2', 6); -define('GETID3_FLV_VIDEO_H264', 7); - -define('H264_AVC_SEQUENCE_HEADER', 0); -define('H264_PROFILE_BASELINE', 66); -define('H264_PROFILE_MAIN', 77); -define('H264_PROFILE_EXTENDED', 88); -define('H264_PROFILE_HIGH', 100); -define('H264_PROFILE_HIGH10', 110); -define('H264_PROFILE_HIGH422', 122); -define('H264_PROFILE_HIGH444', 144); -define('H264_PROFILE_HIGH444_PREDICTIVE', 244); - -class getid3_flv extends getid3_handler -{ - public $max_frames = 100000; // break out of the loop if too many frames have been scanned; only scan this many if meta frame does not contain useful duration - - public function Analyze() { - $info = &$this->getid3->info; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - - $FLVdataLength = $info['avdataend'] - $info['avdataoffset']; - $FLVheader = fread($this->getid3->fp, 5); - - $info['fileformat'] = 'flv'; - $info['flv']['header']['signature'] = substr($FLVheader, 0, 3); - $info['flv']['header']['version'] = getid3_lib::BigEndian2Int(substr($FLVheader, 3, 1)); - $TypeFlags = getid3_lib::BigEndian2Int(substr($FLVheader, 4, 1)); - - $magic = 'FLV'; - if ($info['flv']['header']['signature'] != $magic) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($info['flv']['header']['signature']).'"'; - unset($info['flv']); - unset($info['fileformat']); - return false; - } - - $info['flv']['header']['hasAudio'] = (bool) ($TypeFlags & 0x04); - $info['flv']['header']['hasVideo'] = (bool) ($TypeFlags & 0x01); - - $FrameSizeDataLength = getid3_lib::BigEndian2Int(fread($this->getid3->fp, 4)); - $FLVheaderFrameLength = 9; - if ($FrameSizeDataLength > $FLVheaderFrameLength) { - fseek($this->getid3->fp, $FrameSizeDataLength - $FLVheaderFrameLength, SEEK_CUR); - } - $Duration = 0; - $found_video = false; - $found_audio = false; - $found_meta = false; - $found_valid_meta_playtime = false; - $tagParseCount = 0; - $info['flv']['framecount'] = array('total'=>0, 'audio'=>0, 'video'=>0); - $flv_framecount = &$info['flv']['framecount']; - while (((ftell($this->getid3->fp) + 16) < $info['avdataend']) && (($tagParseCount++ <= $this->max_frames) || !$found_valid_meta_playtime)) { - $ThisTagHeader = fread($this->getid3->fp, 16); - - $PreviousTagLength = getid3_lib::BigEndian2Int(substr($ThisTagHeader, 0, 4)); - $TagType = getid3_lib::BigEndian2Int(substr($ThisTagHeader, 4, 1)); - $DataLength = getid3_lib::BigEndian2Int(substr($ThisTagHeader, 5, 3)); - $Timestamp = getid3_lib::BigEndian2Int(substr($ThisTagHeader, 8, 3)); - $LastHeaderByte = getid3_lib::BigEndian2Int(substr($ThisTagHeader, 15, 1)); - $NextOffset = ftell($this->getid3->fp) - 1 + $DataLength; - if ($Timestamp > $Duration) { - $Duration = $Timestamp; - } - - $flv_framecount['total']++; - switch ($TagType) { - case GETID3_FLV_TAG_AUDIO: - $flv_framecount['audio']++; - if (!$found_audio) { - $found_audio = true; - $info['flv']['audio']['audioFormat'] = ($LastHeaderByte >> 4) & 0x0F; - $info['flv']['audio']['audioRate'] = ($LastHeaderByte >> 2) & 0x03; - $info['flv']['audio']['audioSampleSize'] = ($LastHeaderByte >> 1) & 0x01; - $info['flv']['audio']['audioType'] = $LastHeaderByte & 0x01; - } - break; - - case GETID3_FLV_TAG_VIDEO: - $flv_framecount['video']++; - if (!$found_video) { - $found_video = true; - $info['flv']['video']['videoCodec'] = $LastHeaderByte & 0x07; - - $FLVvideoHeader = fread($this->getid3->fp, 11); - - if ($info['flv']['video']['videoCodec'] == GETID3_FLV_VIDEO_H264) { - // this code block contributed by: moysevichØgmail*com - - $AVCPacketType = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 0, 1)); - if ($AVCPacketType == H264_AVC_SEQUENCE_HEADER) { - // read AVCDecoderConfigurationRecord - $configurationVersion = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 4, 1)); - $AVCProfileIndication = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 5, 1)); - $profile_compatibility = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 6, 1)); - $lengthSizeMinusOne = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 7, 1)); - $numOfSequenceParameterSets = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 8, 1)); - - if (($numOfSequenceParameterSets & 0x1F) != 0) { - // there is at least one SequenceParameterSet - // read size of the first SequenceParameterSet - //$spsSize = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 9, 2)); - $spsSize = getid3_lib::LittleEndian2Int(substr($FLVvideoHeader, 9, 2)); - // read the first SequenceParameterSet - $sps = fread($this->getid3->fp, $spsSize); - if (strlen($sps) == $spsSize) { // make sure that whole SequenceParameterSet was red - $spsReader = new AVCSequenceParameterSetReader($sps); - $spsReader->readData(); - $info['video']['resolution_x'] = $spsReader->getWidth(); - $info['video']['resolution_y'] = $spsReader->getHeight(); - } - } - } - // end: moysevichØgmail*com - - } elseif ($info['flv']['video']['videoCodec'] == GETID3_FLV_VIDEO_H263) { - - $PictureSizeType = (getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 3, 2))) >> 7; - $PictureSizeType = $PictureSizeType & 0x0007; - $info['flv']['header']['videoSizeType'] = $PictureSizeType; - switch ($PictureSizeType) { - case 0: - //$PictureSizeEnc = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 5, 2)); - //$PictureSizeEnc <<= 1; - //$info['video']['resolution_x'] = ($PictureSizeEnc & 0xFF00) >> 8; - //$PictureSizeEnc = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 6, 2)); - //$PictureSizeEnc <<= 1; - //$info['video']['resolution_y'] = ($PictureSizeEnc & 0xFF00) >> 8; - - $PictureSizeEnc['x'] = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 4, 2)); - $PictureSizeEnc['y'] = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 5, 2)); - $PictureSizeEnc['x'] >>= 7; - $PictureSizeEnc['y'] >>= 7; - $info['video']['resolution_x'] = $PictureSizeEnc['x'] & 0xFF; - $info['video']['resolution_y'] = $PictureSizeEnc['y'] & 0xFF; - break; - - case 1: - $PictureSizeEnc['x'] = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 4, 3)); - $PictureSizeEnc['y'] = getid3_lib::BigEndian2Int(substr($FLVvideoHeader, 6, 3)); - $PictureSizeEnc['x'] >>= 7; - $PictureSizeEnc['y'] >>= 7; - $info['video']['resolution_x'] = $PictureSizeEnc['x'] & 0xFFFF; - $info['video']['resolution_y'] = $PictureSizeEnc['y'] & 0xFFFF; - break; - - case 2: - $info['video']['resolution_x'] = 352; - $info['video']['resolution_y'] = 288; - break; - - case 3: - $info['video']['resolution_x'] = 176; - $info['video']['resolution_y'] = 144; - break; - - case 4: - $info['video']['resolution_x'] = 128; - $info['video']['resolution_y'] = 96; - break; - - case 5: - $info['video']['resolution_x'] = 320; - $info['video']['resolution_y'] = 240; - break; - - case 6: - $info['video']['resolution_x'] = 160; - $info['video']['resolution_y'] = 120; - break; - - default: - $info['video']['resolution_x'] = 0; - $info['video']['resolution_y'] = 0; - break; - - } - } - $info['video']['pixel_aspect_ratio'] = $info['video']['resolution_x'] / $info['video']['resolution_y']; - } - break; - - // Meta tag - case GETID3_FLV_TAG_META: - if (!$found_meta) { - $found_meta = true; - fseek($this->getid3->fp, -1, SEEK_CUR); - $datachunk = fread($this->getid3->fp, $DataLength); - $AMFstream = new AMFStream($datachunk); - $reader = new AMFReader($AMFstream); - $eventName = $reader->readData(); - $info['flv']['meta'][$eventName] = $reader->readData(); - unset($reader); - - $copykeys = array('framerate'=>'frame_rate', 'width'=>'resolution_x', 'height'=>'resolution_y', 'audiodatarate'=>'bitrate', 'videodatarate'=>'bitrate'); - foreach ($copykeys as $sourcekey => $destkey) { - if (isset($info['flv']['meta']['onMetaData'][$sourcekey])) { - switch ($sourcekey) { - case 'width': - case 'height': - $info['video'][$destkey] = intval(round($info['flv']['meta']['onMetaData'][$sourcekey])); - break; - case 'audiodatarate': - $info['audio'][$destkey] = getid3_lib::CastAsInt(round($info['flv']['meta']['onMetaData'][$sourcekey] * 1000)); - break; - case 'videodatarate': - case 'frame_rate': - default: - $info['video'][$destkey] = $info['flv']['meta']['onMetaData'][$sourcekey]; - break; - } - } - } - if (!empty($info['flv']['meta']['onMetaData']['duration'])) { - $found_valid_meta_playtime = true; - } - } - break; - - default: - // noop - break; - } - fseek($this->getid3->fp, $NextOffset, SEEK_SET); - } - - $info['playtime_seconds'] = $Duration / 1000; - if ($info['playtime_seconds'] > 0) { - $info['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; - } - - if ($info['flv']['header']['hasAudio']) { - $info['audio']['codec'] = $this->FLVaudioFormat($info['flv']['audio']['audioFormat']); - $info['audio']['sample_rate'] = $this->FLVaudioRate($info['flv']['audio']['audioRate']); - $info['audio']['bits_per_sample'] = $this->FLVaudioBitDepth($info['flv']['audio']['audioSampleSize']); - - $info['audio']['channels'] = $info['flv']['audio']['audioType'] + 1; // 0=mono,1=stereo - $info['audio']['lossless'] = ($info['flv']['audio']['audioFormat'] ? false : true); // 0=uncompressed - $info['audio']['dataformat'] = 'flv'; - } - if (!empty($info['flv']['header']['hasVideo'])) { - $info['video']['codec'] = $this->FLVvideoCodec($info['flv']['video']['videoCodec']); - $info['video']['dataformat'] = 'flv'; - $info['video']['lossless'] = false; - } - - // Set information from meta - if (!empty($info['flv']['meta']['onMetaData']['duration'])) { - $info['playtime_seconds'] = $info['flv']['meta']['onMetaData']['duration']; - $info['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; - } - if (isset($info['flv']['meta']['onMetaData']['audiocodecid'])) { - $info['audio']['codec'] = $this->FLVaudioFormat($info['flv']['meta']['onMetaData']['audiocodecid']); - } - if (isset($info['flv']['meta']['onMetaData']['videocodecid'])) { - $info['video']['codec'] = $this->FLVvideoCodec($info['flv']['meta']['onMetaData']['videocodecid']); - } - return true; - } - - - public function FLVaudioFormat($id) { - $FLVaudioFormat = array( - 0 => 'Linear PCM, platform endian', - 1 => 'ADPCM', - 2 => 'mp3', - 3 => 'Linear PCM, little endian', - 4 => 'Nellymoser 16kHz mono', - 5 => 'Nellymoser 8kHz mono', - 6 => 'Nellymoser', - 7 => 'G.711A-law logarithmic PCM', - 8 => 'G.711 mu-law logarithmic PCM', - 9 => 'reserved', - 10 => 'AAC', - 11 => false, // unknown? - 12 => false, // unknown? - 13 => false, // unknown? - 14 => 'mp3 8kHz', - 15 => 'Device-specific sound', - ); - return (isset($FLVaudioFormat[$id]) ? $FLVaudioFormat[$id] : false); - } - - public function FLVaudioRate($id) { - $FLVaudioRate = array( - 0 => 5500, - 1 => 11025, - 2 => 22050, - 3 => 44100, - ); - return (isset($FLVaudioRate[$id]) ? $FLVaudioRate[$id] : false); - } - - public function FLVaudioBitDepth($id) { - $FLVaudioBitDepth = array( - 0 => 8, - 1 => 16, - ); - return (isset($FLVaudioBitDepth[$id]) ? $FLVaudioBitDepth[$id] : false); - } - - public function FLVvideoCodec($id) { - $FLVvideoCodec = array( - GETID3_FLV_VIDEO_H263 => 'Sorenson H.263', - GETID3_FLV_VIDEO_SCREEN => 'Screen video', - GETID3_FLV_VIDEO_VP6FLV => 'On2 VP6', - GETID3_FLV_VIDEO_VP6FLV_ALPHA => 'On2 VP6 with alpha channel', - GETID3_FLV_VIDEO_SCREENV2 => 'Screen video v2', - GETID3_FLV_VIDEO_H264 => 'Sorenson H.264', - ); - return (isset($FLVvideoCodec[$id]) ? $FLVvideoCodec[$id] : false); - } -} - -class AMFStream { - public $bytes; - public $pos; - - public function AMFStream(&$bytes) { - $this->bytes =& $bytes; - $this->pos = 0; - } - - public function readByte() { - return getid3_lib::BigEndian2Int(substr($this->bytes, $this->pos++, 1)); - } - - public function readInt() { - return ($this->readByte() << 8) + $this->readByte(); - } - - public function readLong() { - return ($this->readByte() << 24) + ($this->readByte() << 16) + ($this->readByte() << 8) + $this->readByte(); - } - - public function readDouble() { - return getid3_lib::BigEndian2Float($this->read(8)); - } - - public function readUTF() { - $length = $this->readInt(); - return $this->read($length); - } - - public function readLongUTF() { - $length = $this->readLong(); - return $this->read($length); - } - - public function read($length) { - $val = substr($this->bytes, $this->pos, $length); - $this->pos += $length; - return $val; - } - - public function peekByte() { - $pos = $this->pos; - $val = $this->readByte(); - $this->pos = $pos; - return $val; - } - - public function peekInt() { - $pos = $this->pos; - $val = $this->readInt(); - $this->pos = $pos; - return $val; - } - - public function peekLong() { - $pos = $this->pos; - $val = $this->readLong(); - $this->pos = $pos; - return $val; - } - - public function peekDouble() { - $pos = $this->pos; - $val = $this->readDouble(); - $this->pos = $pos; - return $val; - } - - public function peekUTF() { - $pos = $this->pos; - $val = $this->readUTF(); - $this->pos = $pos; - return $val; - } - - public function peekLongUTF() { - $pos = $this->pos; - $val = $this->readLongUTF(); - $this->pos = $pos; - return $val; - } -} - -class AMFReader { - public $stream; - - public function AMFReader(&$stream) { - $this->stream =& $stream; - } - - public function readData() { - $value = null; - - $type = $this->stream->readByte(); - switch ($type) { - - // Double - case 0: - $value = $this->readDouble(); - break; - - // Boolean - case 1: - $value = $this->readBoolean(); - break; - - // String - case 2: - $value = $this->readString(); - break; - - // Object - case 3: - $value = $this->readObject(); - break; - - // null - case 6: - return null; - break; - - // Mixed array - case 8: - $value = $this->readMixedArray(); - break; - - // Array - case 10: - $value = $this->readArray(); - break; - - // Date - case 11: - $value = $this->readDate(); - break; - - // Long string - case 13: - $value = $this->readLongString(); - break; - - // XML (handled as string) - case 15: - $value = $this->readXML(); - break; - - // Typed object (handled as object) - case 16: - $value = $this->readTypedObject(); - break; - - // Long string - default: - $value = '(unknown or unsupported data type)'; - break; - } - - return $value; - } - - public function readDouble() { - return $this->stream->readDouble(); - } - - public function readBoolean() { - return $this->stream->readByte() == 1; - } - - public function readString() { - return $this->stream->readUTF(); - } - - public function readObject() { - // Get highest numerical index - ignored -// $highestIndex = $this->stream->readLong(); - - $data = array(); - - while ($key = $this->stream->readUTF()) { - $data[$key] = $this->readData(); - } - // Mixed array record ends with empty string (0x00 0x00) and 0x09 - if (($key == '') && ($this->stream->peekByte() == 0x09)) { - // Consume byte - $this->stream->readByte(); - } - return $data; - } - - public function readMixedArray() { - // Get highest numerical index - ignored - $highestIndex = $this->stream->readLong(); - - $data = array(); - - while ($key = $this->stream->readUTF()) { - if (is_numeric($key)) { - $key = (float) $key; - } - $data[$key] = $this->readData(); - } - // Mixed array record ends with empty string (0x00 0x00) and 0x09 - if (($key == '') && ($this->stream->peekByte() == 0x09)) { - // Consume byte - $this->stream->readByte(); - } - - return $data; - } - - public function readArray() { - $length = $this->stream->readLong(); - $data = array(); - - for ($i = 0; $i < $length; $i++) { - $data[] = $this->readData(); - } - return $data; - } - - public function readDate() { - $timestamp = $this->stream->readDouble(); - $timezone = $this->stream->readInt(); - return $timestamp; - } - - public function readLongString() { - return $this->stream->readLongUTF(); - } - - public function readXML() { - return $this->stream->readLongUTF(); - } - - public function readTypedObject() { - $className = $this->stream->readUTF(); - return $this->readObject(); - } -} - -class AVCSequenceParameterSetReader { - public $sps; - public $start = 0; - public $currentBytes = 0; - public $currentBits = 0; - public $width; - public $height; - - public function AVCSequenceParameterSetReader($sps) { - $this->sps = $sps; - } - - public function readData() { - $this->skipBits(8); - $this->skipBits(8); - $profile = $this->getBits(8); // read profile - $this->skipBits(16); - $this->expGolombUe(); // read sps id - if (in_array($profile, array(H264_PROFILE_HIGH, H264_PROFILE_HIGH10, H264_PROFILE_HIGH422, H264_PROFILE_HIGH444, H264_PROFILE_HIGH444_PREDICTIVE))) { - if ($this->expGolombUe() == 3) { - $this->skipBits(1); - } - $this->expGolombUe(); - $this->expGolombUe(); - $this->skipBits(1); - if ($this->getBit()) { - for ($i = 0; $i < 8; $i++) { - if ($this->getBit()) { - $size = $i < 6 ? 16 : 64; - $lastScale = 8; - $nextScale = 8; - for ($j = 0; $j < $size; $j++) { - if ($nextScale != 0) { - $deltaScale = $this->expGolombUe(); - $nextScale = ($lastScale + $deltaScale + 256) % 256; - } - if ($nextScale != 0) { - $lastScale = $nextScale; - } - } - } - } - } - } - $this->expGolombUe(); - $pocType = $this->expGolombUe(); - if ($pocType == 0) { - $this->expGolombUe(); - } elseif ($pocType == 1) { - $this->skipBits(1); - $this->expGolombSe(); - $this->expGolombSe(); - $pocCycleLength = $this->expGolombUe(); - for ($i = 0; $i < $pocCycleLength; $i++) { - $this->expGolombSe(); - } - } - $this->expGolombUe(); - $this->skipBits(1); - $this->width = ($this->expGolombUe() + 1) * 16; - $heightMap = $this->expGolombUe() + 1; - $this->height = (2 - $this->getBit()) * $heightMap * 16; - } - - public function skipBits($bits) { - $newBits = $this->currentBits + $bits; - $this->currentBytes += (int)floor($newBits / 8); - $this->currentBits = $newBits % 8; - } - - public function getBit() { - $result = (getid3_lib::BigEndian2Int(substr($this->sps, $this->currentBytes, 1)) >> (7 - $this->currentBits)) & 0x01; - $this->skipBits(1); - return $result; - } - - public function getBits($bits) { - $result = 0; - for ($i = 0; $i < $bits; $i++) { - $result = ($result << 1) + $this->getBit(); - } - return $result; - } - - public function expGolombUe() { - $significantBits = 0; - $bit = $this->getBit(); - while ($bit == 0) { - $significantBits++; - $bit = $this->getBit(); - - if ($significantBits > 31) { - // something is broken, this is an emergency escape to prevent infinite loops - return 0; - } - } - return (1 << $significantBits) + $this->getBits($significantBits) - 1; - } - - public function expGolombSe() { - $result = $this->expGolombUe(); - if (($result & 0x01) == 0) { - return -($result >> 1); - } else { - return ($result + 1) >> 1; - } - } - - public function getWidth() { - return $this->width; - } - - public function getHeight() { - return $this->height; - } -} diff --git a/src/Classes/Vendor/getid3/module.audio-video.matroska.php b/src/Classes/Vendor/getid3/module.audio-video.matroska.php deleted file mode 100755 index 3c1921fb3..000000000 --- a/src/Classes/Vendor/getid3/module.audio-video.matroska.php +++ /dev/null @@ -1,1771 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio-video.matriska.php // -// module for analyzing Matroska containers // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -define('EBML_ID_CHAPTERS', 0x0043A770); // [10][43][A7][70] -- A system to define basic menus and partition data. For more detailed information, look at the Chapters Explanation. -define('EBML_ID_SEEKHEAD', 0x014D9B74); // [11][4D][9B][74] -- Contains the position of other level 1 elements. -define('EBML_ID_TAGS', 0x0254C367); // [12][54][C3][67] -- Element containing elements specific to Tracks/Chapters. A list of valid tags can be found . -define('EBML_ID_INFO', 0x0549A966); // [15][49][A9][66] -- Contains miscellaneous general information and statistics on the file. -define('EBML_ID_TRACKS', 0x0654AE6B); // [16][54][AE][6B] -- A top-level block of information with many tracks described. -define('EBML_ID_SEGMENT', 0x08538067); // [18][53][80][67] -- This element contains all other top-level (level 1) elements. Typically a Matroska file is composed of 1 segment. -define('EBML_ID_ATTACHMENTS', 0x0941A469); // [19][41][A4][69] -- Contain attached files. -define('EBML_ID_EBML', 0x0A45DFA3); // [1A][45][DF][A3] -- Set the EBML characteristics of the data to follow. Each EBML document has to start with this. -define('EBML_ID_CUES', 0x0C53BB6B); // [1C][53][BB][6B] -- A top-level element to speed seeking access. All entries are local to the segment. -define('EBML_ID_CLUSTER', 0x0F43B675); // [1F][43][B6][75] -- The lower level element containing the (monolithic) Block structure. -define('EBML_ID_LANGUAGE', 0x02B59C); // [22][B5][9C] -- Specifies the language of the track in the Matroska languages form. -define('EBML_ID_TRACKTIMECODESCALE', 0x03314F); // [23][31][4F] -- The scale to apply on this track to work at normal speed in relation with other tracks (mostly used to adjust video speed when the audio length differs). -define('EBML_ID_DEFAULTDURATION', 0x03E383); // [23][E3][83] -- Number of nanoseconds (i.e. not scaled) per frame. -define('EBML_ID_CODECNAME', 0x058688); // [25][86][88] -- A human-readable string specifying the codec. -define('EBML_ID_CODECDOWNLOADURL', 0x06B240); // [26][B2][40] -- A URL to download about the codec used. -define('EBML_ID_TIMECODESCALE', 0x0AD7B1); // [2A][D7][B1] -- Timecode scale in nanoseconds (1.000.000 means all timecodes in the segment are expressed in milliseconds). -define('EBML_ID_COLOURSPACE', 0x0EB524); // [2E][B5][24] -- Same value as in AVI (32 bits). -define('EBML_ID_GAMMAVALUE', 0x0FB523); // [2F][B5][23] -- Gamma Value. -define('EBML_ID_CODECSETTINGS', 0x1A9697); // [3A][96][97] -- A string describing the encoding setting used. -define('EBML_ID_CODECINFOURL', 0x1B4040); // [3B][40][40] -- A URL to find information about the codec used. -define('EBML_ID_PREVFILENAME', 0x1C83AB); // [3C][83][AB] -- An escaped filename corresponding to the previous segment. -define('EBML_ID_PREVUID', 0x1CB923); // [3C][B9][23] -- A unique ID to identify the previous chained segment (128 bits). -define('EBML_ID_NEXTFILENAME', 0x1E83BB); // [3E][83][BB] -- An escaped filename corresponding to the next segment. -define('EBML_ID_NEXTUID', 0x1EB923); // [3E][B9][23] -- A unique ID to identify the next chained segment (128 bits). -define('EBML_ID_CONTENTCOMPALGO', 0x0254); // [42][54] -- The compression algorithm used. Algorithms that have been specified so far are: -define('EBML_ID_CONTENTCOMPSETTINGS', 0x0255); // [42][55] -- Settings that might be needed by the decompressor. For Header Stripping (ContentCompAlgo=3), the bytes that were removed from the beggining of each frames of the track. -define('EBML_ID_DOCTYPE', 0x0282); // [42][82] -- A string that describes the type of document that follows this EBML header ('matroska' in our case). -define('EBML_ID_DOCTYPEREADVERSION', 0x0285); // [42][85] -- The minimum DocType version an interpreter has to support to read this file. -define('EBML_ID_EBMLVERSION', 0x0286); // [42][86] -- The version of EBML parser used to create the file. -define('EBML_ID_DOCTYPEVERSION', 0x0287); // [42][87] -- The version of DocType interpreter used to create the file. -define('EBML_ID_EBMLMAXIDLENGTH', 0x02F2); // [42][F2] -- The maximum length of the IDs you'll find in this file (4 or less in Matroska). -define('EBML_ID_EBMLMAXSIZELENGTH', 0x02F3); // [42][F3] -- The maximum length of the sizes you'll find in this file (8 or less in Matroska). This does not override the element size indicated at the beginning of an element. Elements that have an indicated size which is larger than what is allowed by EBMLMaxSizeLength shall be considered invalid. -define('EBML_ID_EBMLREADVERSION', 0x02F7); // [42][F7] -- The minimum EBML version a parser has to support to read this file. -define('EBML_ID_CHAPLANGUAGE', 0x037C); // [43][7C] -- The languages corresponding to the string, in the bibliographic ISO-639-2 form. -define('EBML_ID_CHAPCOUNTRY', 0x037E); // [43][7E] -- The countries corresponding to the string, same 2 octets as in Internet domains. -define('EBML_ID_SEGMENTFAMILY', 0x0444); // [44][44] -- A randomly generated unique ID that all segments related to each other must use (128 bits). -define('EBML_ID_DATEUTC', 0x0461); // [44][61] -- Date of the origin of timecode (value 0), i.e. production date. -define('EBML_ID_TAGLANGUAGE', 0x047A); // [44][7A] -- Specifies the language of the tag specified, in the Matroska languages form. -define('EBML_ID_TAGDEFAULT', 0x0484); // [44][84] -- Indication to know if this is the default/original language to use for the given tag. -define('EBML_ID_TAGBINARY', 0x0485); // [44][85] -- The values of the Tag if it is binary. Note that this cannot be used in the same SimpleTag as TagString. -define('EBML_ID_TAGSTRING', 0x0487); // [44][87] -- The value of the Tag. -define('EBML_ID_DURATION', 0x0489); // [44][89] -- Duration of the segment (based on TimecodeScale). -define('EBML_ID_CHAPPROCESSPRIVATE', 0x050D); // [45][0D] -- Some optional data attached to the ChapProcessCodecID information. For ChapProcessCodecID = 1, it is the "DVD level" equivalent. -define('EBML_ID_CHAPTERFLAGENABLED', 0x0598); // [45][98] -- Specify wether the chapter is enabled. It can be enabled/disabled by a Control Track. When disabled, the movie should skip all the content between the TimeStart and TimeEnd of this chapter. -define('EBML_ID_TAGNAME', 0x05A3); // [45][A3] -- The name of the Tag that is going to be stored. -define('EBML_ID_EDITIONENTRY', 0x05B9); // [45][B9] -- Contains all information about a segment edition. -define('EBML_ID_EDITIONUID', 0x05BC); // [45][BC] -- A unique ID to identify the edition. It's useful for tagging an edition. -define('EBML_ID_EDITIONFLAGHIDDEN', 0x05BD); // [45][BD] -- If an edition is hidden (1), it should not be available to the user interface (but still to Control Tracks). -define('EBML_ID_EDITIONFLAGDEFAULT', 0x05DB); // [45][DB] -- If a flag is set (1) the edition should be used as the default one. -define('EBML_ID_EDITIONFLAGORDERED', 0x05DD); // [45][DD] -- Specify if the chapters can be defined multiple times and the order to play them is enforced. -define('EBML_ID_FILEDATA', 0x065C); // [46][5C] -- The data of the file. -define('EBML_ID_FILEMIMETYPE', 0x0660); // [46][60] -- MIME type of the file. -define('EBML_ID_FILENAME', 0x066E); // [46][6E] -- Filename of the attached file. -define('EBML_ID_FILEREFERRAL', 0x0675); // [46][75] -- A binary value that a track/codec can refer to when the attachment is needed. -define('EBML_ID_FILEDESCRIPTION', 0x067E); // [46][7E] -- A human-friendly name for the attached file. -define('EBML_ID_FILEUID', 0x06AE); // [46][AE] -- Unique ID representing the file, as random as possible. -define('EBML_ID_CONTENTENCALGO', 0x07E1); // [47][E1] -- The encryption algorithm used. The value '0' means that the contents have not been encrypted but only signed. Predefined values: -define('EBML_ID_CONTENTENCKEYID', 0x07E2); // [47][E2] -- For public key algorithms this is the ID of the public key the the data was encrypted with. -define('EBML_ID_CONTENTSIGNATURE', 0x07E3); // [47][E3] -- A cryptographic signature of the contents. -define('EBML_ID_CONTENTSIGKEYID', 0x07E4); // [47][E4] -- This is the ID of the private key the data was signed with. -define('EBML_ID_CONTENTSIGALGO', 0x07E5); // [47][E5] -- The algorithm used for the signature. A value of '0' means that the contents have not been signed but only encrypted. Predefined values: -define('EBML_ID_CONTENTSIGHASHALGO', 0x07E6); // [47][E6] -- The hash algorithm used for the signature. A value of '0' means that the contents have not been signed but only encrypted. Predefined values: -define('EBML_ID_MUXINGAPP', 0x0D80); // [4D][80] -- Muxing application or library ("libmatroska-0.4.3"). -define('EBML_ID_SEEK', 0x0DBB); // [4D][BB] -- Contains a single seek entry to an EBML element. -define('EBML_ID_CONTENTENCODINGORDER', 0x1031); // [50][31] -- Tells when this modification was used during encoding/muxing starting with 0 and counting upwards. The decoder/demuxer has to start with the highest order number it finds and work its way down. This value has to be unique over all ContentEncodingOrder elements in the segment. -define('EBML_ID_CONTENTENCODINGSCOPE', 0x1032); // [50][32] -- A bit field that describes which elements have been modified in this way. Values (big endian) can be OR'ed. Possible values: -define('EBML_ID_CONTENTENCODINGTYPE', 0x1033); // [50][33] -- A value describing what kind of transformation has been done. Possible values: -define('EBML_ID_CONTENTCOMPRESSION', 0x1034); // [50][34] -- Settings describing the compression used. Must be present if the value of ContentEncodingType is 0 and absent otherwise. Each block must be decompressable even if no previous block is available in order not to prevent seeking. -define('EBML_ID_CONTENTENCRYPTION', 0x1035); // [50][35] -- Settings describing the encryption used. Must be present if the value of ContentEncodingType is 1 and absent otherwise. -define('EBML_ID_CUEREFNUMBER', 0x135F); // [53][5F] -- Number of the referenced Block of Track X in the specified Cluster. -define('EBML_ID_NAME', 0x136E); // [53][6E] -- A human-readable track name. -define('EBML_ID_CUEBLOCKNUMBER', 0x1378); // [53][78] -- Number of the Block in the specified Cluster. -define('EBML_ID_TRACKOFFSET', 0x137F); // [53][7F] -- A value to add to the Block's Timecode. This can be used to adjust the playback offset of a track. -define('EBML_ID_SEEKID', 0x13AB); // [53][AB] -- The binary ID corresponding to the element name. -define('EBML_ID_SEEKPOSITION', 0x13AC); // [53][AC] -- The position of the element in the segment in octets (0 = first level 1 element). -define('EBML_ID_STEREOMODE', 0x13B8); // [53][B8] -- Stereo-3D video mode. -define('EBML_ID_OLDSTEREOMODE', 0x13B9); // [53][B9] -- Bogus StereoMode value used in old versions of libmatroska. DO NOT USE. (0: mono, 1: right eye, 2: left eye, 3: both eyes). -define('EBML_ID_PIXELCROPBOTTOM', 0x14AA); // [54][AA] -- The number of video pixels to remove at the bottom of the image (for HDTV content). -define('EBML_ID_DISPLAYWIDTH', 0x14B0); // [54][B0] -- Width of the video frames to display. -define('EBML_ID_DISPLAYUNIT', 0x14B2); // [54][B2] -- Type of the unit for DisplayWidth/Height (0: pixels, 1: centimeters, 2: inches). -define('EBML_ID_ASPECTRATIOTYPE', 0x14B3); // [54][B3] -- Specify the possible modifications to the aspect ratio (0: free resizing, 1: keep aspect ratio, 2: fixed). -define('EBML_ID_DISPLAYHEIGHT', 0x14BA); // [54][BA] -- Height of the video frames to display. -define('EBML_ID_PIXELCROPTOP', 0x14BB); // [54][BB] -- The number of video pixels to remove at the top of the image. -define('EBML_ID_PIXELCROPLEFT', 0x14CC); // [54][CC] -- The number of video pixels to remove on the left of the image. -define('EBML_ID_PIXELCROPRIGHT', 0x14DD); // [54][DD] -- The number of video pixels to remove on the right of the image. -define('EBML_ID_FLAGFORCED', 0x15AA); // [55][AA] -- Set if that track MUST be used during playback. There can be many forced track for a kind (audio, video or subs), the player should select the one which language matches the user preference or the default + forced track. Overlay MAY happen between a forced and non-forced track of the same kind. -define('EBML_ID_MAXBLOCKADDITIONID', 0x15EE); // [55][EE] -- The maximum value of BlockAddID. A value 0 means there is no BlockAdditions for this track. -define('EBML_ID_WRITINGAPP', 0x1741); // [57][41] -- Writing application ("mkvmerge-0.3.3"). -define('EBML_ID_CLUSTERSILENTTRACKS', 0x1854); // [58][54] -- The list of tracks that are not used in that part of the stream. It is useful when using overlay tracks on seeking. Then you should decide what track to use. -define('EBML_ID_CLUSTERSILENTTRACKNUMBER', 0x18D7); // [58][D7] -- One of the track number that are not used from now on in the stream. It could change later if not specified as silent in a further Cluster. -define('EBML_ID_ATTACHEDFILE', 0x21A7); // [61][A7] -- An attached file. -define('EBML_ID_CONTENTENCODING', 0x2240); // [62][40] -- Settings for one content encoding like compression or encryption. -define('EBML_ID_BITDEPTH', 0x2264); // [62][64] -- Bits per sample, mostly used for PCM. -define('EBML_ID_CODECPRIVATE', 0x23A2); // [63][A2] -- Private data only known to the codec. -define('EBML_ID_TARGETS', 0x23C0); // [63][C0] -- Contain all UIDs where the specified meta data apply. It is void to describe everything in the segment. -define('EBML_ID_CHAPTERPHYSICALEQUIV', 0x23C3); // [63][C3] -- Specify the physical equivalent of this ChapterAtom like "DVD" (60) or "SIDE" (50), see complete list of values. -define('EBML_ID_TAGCHAPTERUID', 0x23C4); // [63][C4] -- A unique ID to identify the Chapter(s) the tags belong to. If the value is 0 at this level, the tags apply to all chapters in the Segment. -define('EBML_ID_TAGTRACKUID', 0x23C5); // [63][C5] -- A unique ID to identify the Track(s) the tags belong to. If the value is 0 at this level, the tags apply to all tracks in the Segment. -define('EBML_ID_TAGATTACHMENTUID', 0x23C6); // [63][C6] -- A unique ID to identify the Attachment(s) the tags belong to. If the value is 0 at this level, the tags apply to all the attachments in the Segment. -define('EBML_ID_TAGEDITIONUID', 0x23C9); // [63][C9] -- A unique ID to identify the EditionEntry(s) the tags belong to. If the value is 0 at this level, the tags apply to all editions in the Segment. -define('EBML_ID_TARGETTYPE', 0x23CA); // [63][CA] -- An informational string that can be used to display the logical level of the target like "ALBUM", "TRACK", "MOVIE", "CHAPTER", etc (see TargetType). -define('EBML_ID_TRACKTRANSLATE', 0x2624); // [66][24] -- The track identification for the given Chapter Codec. -define('EBML_ID_TRACKTRANSLATETRACKID', 0x26A5); // [66][A5] -- The binary value used to represent this track in the chapter codec data. The format depends on the ChapProcessCodecID used. -define('EBML_ID_TRACKTRANSLATECODEC', 0x26BF); // [66][BF] -- The chapter codec using this ID (0: Matroska Script, 1: DVD-menu). -define('EBML_ID_TRACKTRANSLATEEDITIONUID', 0x26FC); // [66][FC] -- Specify an edition UID on which this translation applies. When not specified, it means for all editions found in the segment. -define('EBML_ID_SIMPLETAG', 0x27C8); // [67][C8] -- Contains general information about the target. -define('EBML_ID_TARGETTYPEVALUE', 0x28CA); // [68][CA] -- A number to indicate the logical level of the target (see TargetType). -define('EBML_ID_CHAPPROCESSCOMMAND', 0x2911); // [69][11] -- Contains all the commands associated to the Atom. -define('EBML_ID_CHAPPROCESSTIME', 0x2922); // [69][22] -- Defines when the process command should be handled (0: during the whole chapter, 1: before starting playback, 2: after playback of the chapter). -define('EBML_ID_CHAPTERTRANSLATE', 0x2924); // [69][24] -- A tuple of corresponding ID used by chapter codecs to represent this segment. -define('EBML_ID_CHAPPROCESSDATA', 0x2933); // [69][33] -- Contains the command information. The data should be interpreted depending on the ChapProcessCodecID value. For ChapProcessCodecID = 1, the data correspond to the binary DVD cell pre/post commands. -define('EBML_ID_CHAPPROCESS', 0x2944); // [69][44] -- Contains all the commands associated to the Atom. -define('EBML_ID_CHAPPROCESSCODECID', 0x2955); // [69][55] -- Contains the type of the codec used for the processing. A value of 0 means native Matroska processing (to be defined), a value of 1 means the DVD command set is used. More codec IDs can be added later. -define('EBML_ID_CHAPTERTRANSLATEID', 0x29A5); // [69][A5] -- The binary value used to represent this segment in the chapter codec data. The format depends on the ChapProcessCodecID used. -define('EBML_ID_CHAPTERTRANSLATECODEC', 0x29BF); // [69][BF] -- The chapter codec using this ID (0: Matroska Script, 1: DVD-menu). -define('EBML_ID_CHAPTERTRANSLATEEDITIONUID', 0x29FC); // [69][FC] -- Specify an edition UID on which this correspondance applies. When not specified, it means for all editions found in the segment. -define('EBML_ID_CONTENTENCODINGS', 0x2D80); // [6D][80] -- Settings for several content encoding mechanisms like compression or encryption. -define('EBML_ID_MINCACHE', 0x2DE7); // [6D][E7] -- The minimum number of frames a player should be able to cache during playback. If set to 0, the reference pseudo-cache system is not used. -define('EBML_ID_MAXCACHE', 0x2DF8); // [6D][F8] -- The maximum cache size required to store referenced frames in and the current frame. 0 means no cache is needed. -define('EBML_ID_CHAPTERSEGMENTUID', 0x2E67); // [6E][67] -- A segment to play in place of this chapter. Edition ChapterSegmentEditionUID should be used for this segment, otherwise no edition is used. -define('EBML_ID_CHAPTERSEGMENTEDITIONUID', 0x2EBC); // [6E][BC] -- The edition to play from the segment linked in ChapterSegmentUID. -define('EBML_ID_TRACKOVERLAY', 0x2FAB); // [6F][AB] -- Specify that this track is an overlay track for the Track specified (in the u-integer). That means when this track has a gap (see SilentTracks) the overlay track should be used instead. The order of multiple TrackOverlay matters, the first one is the one that should be used. If not found it should be the second, etc. -define('EBML_ID_TAG', 0x3373); // [73][73] -- Element containing elements specific to Tracks/Chapters. -define('EBML_ID_SEGMENTFILENAME', 0x3384); // [73][84] -- A filename corresponding to this segment. -define('EBML_ID_SEGMENTUID', 0x33A4); // [73][A4] -- A randomly generated unique ID to identify the current segment between many others (128 bits). -define('EBML_ID_CHAPTERUID', 0x33C4); // [73][C4] -- A unique ID to identify the Chapter. -define('EBML_ID_TRACKUID', 0x33C5); // [73][C5] -- A unique ID to identify the Track. This should be kept the same when making a direct stream copy of the Track to another file. -define('EBML_ID_ATTACHMENTLINK', 0x3446); // [74][46] -- The UID of an attachment that is used by this codec. -define('EBML_ID_CLUSTERBLOCKADDITIONS', 0x35A1); // [75][A1] -- Contain additional blocks to complete the main one. An EBML parser that has no knowledge of the Block structure could still see and use/skip these data. -define('EBML_ID_CHANNELPOSITIONS', 0x347B); // [7D][7B] -- Table of horizontal angles for each successive channel, see appendix. -define('EBML_ID_OUTPUTSAMPLINGFREQUENCY', 0x38B5); // [78][B5] -- Real output sampling frequency in Hz (used for SBR techniques). -define('EBML_ID_TITLE', 0x3BA9); // [7B][A9] -- General name of the segment. -define('EBML_ID_CHAPTERDISPLAY', 0x00); // [80] -- Contains all possible strings to use for the chapter display. -define('EBML_ID_TRACKTYPE', 0x03); // [83] -- A set of track types coded on 8 bits (1: video, 2: audio, 3: complex, 0x10: logo, 0x11: subtitle, 0x12: buttons, 0x20: control). -define('EBML_ID_CHAPSTRING', 0x05); // [85] -- Contains the string to use as the chapter atom. -define('EBML_ID_CODECID', 0x06); // [86] -- An ID corresponding to the codec, see the codec page for more info. -define('EBML_ID_FLAGDEFAULT', 0x08); // [88] -- Set if that track (audio, video or subs) SHOULD be used if no language found matches the user preference. -define('EBML_ID_CHAPTERTRACKNUMBER', 0x09); // [89] -- UID of the Track to apply this chapter too. In the absense of a control track, choosing this chapter will select the listed Tracks and deselect unlisted tracks. Absense of this element indicates that the Chapter should be applied to any currently used Tracks. -define('EBML_ID_CLUSTERSLICES', 0x0E); // [8E] -- Contains slices description. -define('EBML_ID_CHAPTERTRACK', 0x0F); // [8F] -- List of tracks on which the chapter applies. If this element is not present, all tracks apply -define('EBML_ID_CHAPTERTIMESTART', 0x11); // [91] -- Timecode of the start of Chapter (not scaled). -define('EBML_ID_CHAPTERTIMEEND', 0x12); // [92] -- Timecode of the end of Chapter (timecode excluded, not scaled). -define('EBML_ID_CUEREFTIME', 0x16); // [96] -- Timecode of the referenced Block. -define('EBML_ID_CUEREFCLUSTER', 0x17); // [97] -- Position of the Cluster containing the referenced Block. -define('EBML_ID_CHAPTERFLAGHIDDEN', 0x18); // [98] -- If a chapter is hidden (1), it should not be available to the user interface (but still to Control Tracks). -define('EBML_ID_FLAGINTERLACED', 0x1A); // [9A] -- Set if the video is interlaced. -define('EBML_ID_CLUSTERBLOCKDURATION', 0x1B); // [9B] -- The duration of the Block (based on TimecodeScale). This element is mandatory when DefaultDuration is set for the track. When not written and with no DefaultDuration, the value is assumed to be the difference between the timecode of this Block and the timecode of the next Block in "display" order (not coding order). This element can be useful at the end of a Track (as there is not other Block available), or when there is a break in a track like for subtitle tracks. -define('EBML_ID_FLAGLACING', 0x1C); // [9C] -- Set if the track may contain blocks using lacing. -define('EBML_ID_CHANNELS', 0x1F); // [9F] -- Numbers of channels in the track. -define('EBML_ID_CLUSTERBLOCKGROUP', 0x20); // [A0] -- Basic container of information containing a single Block or BlockVirtual, and information specific to that Block/VirtualBlock. -define('EBML_ID_CLUSTERBLOCK', 0x21); // [A1] -- Block containing the actual data to be rendered and a timecode relative to the Cluster Timecode. -define('EBML_ID_CLUSTERBLOCKVIRTUAL', 0x22); // [A2] -- A Block with no data. It must be stored in the stream at the place the real Block should be in display order. -define('EBML_ID_CLUSTERSIMPLEBLOCK', 0x23); // [A3] -- Similar to Block but without all the extra information, mostly used to reduced overhead when no extra feature is needed. -define('EBML_ID_CLUSTERCODECSTATE', 0x24); // [A4] -- The new codec state to use. Data interpretation is private to the codec. This information should always be referenced by a seek entry. -define('EBML_ID_CLUSTERBLOCKADDITIONAL', 0x25); // [A5] -- Interpreted by the codec as it wishes (using the BlockAddID). -define('EBML_ID_CLUSTERBLOCKMORE', 0x26); // [A6] -- Contain the BlockAdditional and some parameters. -define('EBML_ID_CLUSTERPOSITION', 0x27); // [A7] -- Position of the Cluster in the segment (0 in live broadcast streams). It might help to resynchronise offset on damaged streams. -define('EBML_ID_CODECDECODEALL', 0x2A); // [AA] -- The codec can decode potentially damaged data. -define('EBML_ID_CLUSTERPREVSIZE', 0x2B); // [AB] -- Size of the previous Cluster, in octets. Can be useful for backward playing. -define('EBML_ID_TRACKENTRY', 0x2E); // [AE] -- Describes a track with all elements. -define('EBML_ID_CLUSTERENCRYPTEDBLOCK', 0x2F); // [AF] -- Similar to SimpleBlock but the data inside the Block are Transformed (encrypt and/or signed). -define('EBML_ID_PIXELWIDTH', 0x30); // [B0] -- Width of the encoded video frames in pixels. -define('EBML_ID_CUETIME', 0x33); // [B3] -- Absolute timecode according to the segment time base. -define('EBML_ID_SAMPLINGFREQUENCY', 0x35); // [B5] -- Sampling frequency in Hz. -define('EBML_ID_CHAPTERATOM', 0x36); // [B6] -- Contains the atom information to use as the chapter atom (apply to all tracks). -define('EBML_ID_CUETRACKPOSITIONS', 0x37); // [B7] -- Contain positions for different tracks corresponding to the timecode. -define('EBML_ID_FLAGENABLED', 0x39); // [B9] -- Set if the track is used. -define('EBML_ID_PIXELHEIGHT', 0x3A); // [BA] -- Height of the encoded video frames in pixels. -define('EBML_ID_CUEPOINT', 0x3B); // [BB] -- Contains all information relative to a seek point in the segment. -define('EBML_ID_CRC32', 0x3F); // [BF] -- The CRC is computed on all the data of the Master element it's in, regardless of its position. It's recommended to put the CRC value at the beggining of the Master element for easier reading. All level 1 elements should include a CRC-32. -define('EBML_ID_CLUSTERBLOCKADDITIONID', 0x4B); // [CB] -- The ID of the BlockAdditional element (0 is the main Block). -define('EBML_ID_CLUSTERLACENUMBER', 0x4C); // [CC] -- The reverse number of the frame in the lace (0 is the last frame, 1 is the next to last, etc). While there are a few files in the wild with this element, it is no longer in use and has been deprecated. Being able to interpret this element is not required for playback. -define('EBML_ID_CLUSTERFRAMENUMBER', 0x4D); // [CD] -- The number of the frame to generate from this lace with this delay (allow you to generate many frames from the same Block/Frame). -define('EBML_ID_CLUSTERDELAY', 0x4E); // [CE] -- The (scaled) delay to apply to the element. -define('EBML_ID_CLUSTERDURATION', 0x4F); // [CF] -- The (scaled) duration to apply to the element. -define('EBML_ID_TRACKNUMBER', 0x57); // [D7] -- The track number as used in the Block Header (using more than 127 tracks is not encouraged, though the design allows an unlimited number). -define('EBML_ID_CUEREFERENCE', 0x5B); // [DB] -- The Clusters containing the required referenced Blocks. -define('EBML_ID_VIDEO', 0x60); // [E0] -- Video settings. -define('EBML_ID_AUDIO', 0x61); // [E1] -- Audio settings. -define('EBML_ID_CLUSTERTIMESLICE', 0x68); // [E8] -- Contains extra time information about the data contained in the Block. While there are a few files in the wild with this element, it is no longer in use and has been deprecated. Being able to interpret this element is not required for playback. -define('EBML_ID_CUECODECSTATE', 0x6A); // [EA] -- The position of the Codec State corresponding to this Cue element. 0 means that the data is taken from the initial Track Entry. -define('EBML_ID_CUEREFCODECSTATE', 0x6B); // [EB] -- The position of the Codec State corresponding to this referenced element. 0 means that the data is taken from the initial Track Entry. -define('EBML_ID_VOID', 0x6C); // [EC] -- Used to void damaged data, to avoid unexpected behaviors when using damaged data. The content is discarded. Also used to reserve space in a sub-element for later use. -define('EBML_ID_CLUSTERTIMECODE', 0x67); // [E7] -- Absolute timecode of the cluster (based on TimecodeScale). -define('EBML_ID_CLUSTERBLOCKADDID', 0x6E); // [EE] -- An ID to identify the BlockAdditional level. -define('EBML_ID_CUECLUSTERPOSITION', 0x71); // [F1] -- The position of the Cluster containing the required Block. -define('EBML_ID_CUETRACK', 0x77); // [F7] -- The track for which a position is given. -define('EBML_ID_CLUSTERREFERENCEPRIORITY', 0x7A); // [FA] -- This frame is referenced and has the specified cache priority. In cache only a frame of the same or higher priority can replace this frame. A value of 0 means the frame is not referenced. -define('EBML_ID_CLUSTERREFERENCEBLOCK', 0x7B); // [FB] -- Timecode of another frame used as a reference (ie: B or P frame). The timecode is relative to the block it's attached to. -define('EBML_ID_CLUSTERREFERENCEVIRTUAL', 0x7D); // [FD] -- Relative position of the data that should be in position of the virtual block. - - -/** -* @tutorial http://www.matroska.org/technical/specs/index.html -* -* @todo Rewrite EBML parser to reduce it's size and honor default element values -* @todo After rewrite implement stream size calculation, that will provide additional useful info and enable AAC/FLAC audio bitrate detection -*/ -class getid3_matroska extends getid3_handler -{ - // public options - public static $hide_clusters = true; // if true, do not return information about CLUSTER chunks, since there's a lot of them and they're not usually useful [default: TRUE] - public static $parse_whole_file = false; // true to parse the whole file, not only header [default: FALSE] - - // private parser settings/placeholders - private $EBMLbuffer = ''; - private $EBMLbuffer_offset = 0; - private $EBMLbuffer_length = 0; - private $current_offset = 0; - private $unuseful_elements = array(EBML_ID_CRC32, EBML_ID_VOID); - - public function Analyze() - { - $info = &$this->getid3->info; - - // parse container - try { - $this->parseEBML($info); - } catch (Exception $e) { - $info['error'][] = 'EBML parser: '.$e->getMessage(); - } - - // calculate playtime - if (isset($info['matroska']['info']) && is_array($info['matroska']['info'])) { - foreach ($info['matroska']['info'] as $key => $infoarray) { - if (isset($infoarray['Duration'])) { - // TimecodeScale is how many nanoseconds each Duration unit is - $info['playtime_seconds'] = $infoarray['Duration'] * ((isset($infoarray['TimecodeScale']) ? $infoarray['TimecodeScale'] : 1000000) / 1000000000); - break; - } - } - } - - // extract tags - if (isset($info['matroska']['tags']) && is_array($info['matroska']['tags'])) { - foreach ($info['matroska']['tags'] as $key => $infoarray) { - $this->ExtractCommentsSimpleTag($infoarray); - } - } - - // process tracks - if (isset($info['matroska']['tracks']['tracks']) && is_array($info['matroska']['tracks']['tracks'])) { - foreach ($info['matroska']['tracks']['tracks'] as $key => $trackarray) { - - $track_info = array(); - $track_info['dataformat'] = self::CodecIDtoCommonName($trackarray['CodecID']); - $track_info['default'] = (isset($trackarray['FlagDefault']) ? $trackarray['FlagDefault'] : true); - if (isset($trackarray['Name'])) { $track_info['name'] = $trackarray['Name']; } - - switch ($trackarray['TrackType']) { - - case 1: // Video - $track_info['resolution_x'] = $trackarray['PixelWidth']; - $track_info['resolution_y'] = $trackarray['PixelHeight']; - $track_info['display_unit'] = self::displayUnit(isset($trackarray['DisplayUnit']) ? $trackarray['DisplayUnit'] : 0); - $track_info['display_x'] = (isset($trackarray['DisplayWidth']) ? $trackarray['DisplayWidth'] : $trackarray['PixelWidth']); - $track_info['display_y'] = (isset($trackarray['DisplayHeight']) ? $trackarray['DisplayHeight'] : $trackarray['PixelHeight']); - - if (isset($trackarray['PixelCropBottom'])) { $track_info['crop_bottom'] = $trackarray['PixelCropBottom']; } - if (isset($trackarray['PixelCropTop'])) { $track_info['crop_top'] = $trackarray['PixelCropTop']; } - if (isset($trackarray['PixelCropLeft'])) { $track_info['crop_left'] = $trackarray['PixelCropLeft']; } - if (isset($trackarray['PixelCropRight'])) { $track_info['crop_right'] = $trackarray['PixelCropRight']; } - if (isset($trackarray['DefaultDuration'])) { $track_info['frame_rate'] = round(1000000000 / $trackarray['DefaultDuration'], 3); } - if (isset($trackarray['CodecName'])) { $track_info['codec'] = $trackarray['CodecName']; } - - switch ($trackarray['CodecID']) { - case 'V_MS/VFW/FOURCC': - if (!getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, false)) { - $this->warning('Unable to parse codec private data ['.basename(__FILE__).':'.__LINE__.'] because cannot include "module.audio-video.riff.php"'); - break; - } - $parsed = getid3_riff::ParseBITMAPINFOHEADER($trackarray['CodecPrivate']); - $track_info['codec'] = getid3_riff::fourccLookup($parsed['fourcc']); - $info['matroska']['track_codec_parsed'][$trackarray['TrackNumber']] = $parsed; - break; - - /*case 'V_MPEG4/ISO/AVC': - $h264['profile'] = getid3_lib::BigEndian2Int(substr($trackarray['CodecPrivate'], 1, 1)); - $h264['level'] = getid3_lib::BigEndian2Int(substr($trackarray['CodecPrivate'], 3, 1)); - $rn = getid3_lib::BigEndian2Int(substr($trackarray['CodecPrivate'], 4, 1)); - $h264['NALUlength'] = ($rn & 3) + 1; - $rn = getid3_lib::BigEndian2Int(substr($trackarray['CodecPrivate'], 5, 1)); - $nsps = ($rn & 31); - $offset = 6; - for ($i = 0; $i < $nsps; $i ++) { - $length = getid3_lib::BigEndian2Int(substr($trackarray['CodecPrivate'], $offset, 2)); - $h264['SPS'][] = substr($trackarray['CodecPrivate'], $offset + 2, $length); - $offset += 2 + $length; - } - $npps = getid3_lib::BigEndian2Int(substr($trackarray['CodecPrivate'], $offset, 1)); - $offset += 1; - for ($i = 0; $i < $npps; $i ++) { - $length = getid3_lib::BigEndian2Int(substr($trackarray['CodecPrivate'], $offset, 2)); - $h264['PPS'][] = substr($trackarray['CodecPrivate'], $offset + 2, $length); - $offset += 2 + $length; - } - $info['matroska']['track_codec_parsed'][$trackarray['TrackNumber']] = $h264; - break;*/ - } - - $info['video']['streams'][] = $track_info; - break; - - case 2: // Audio - $track_info['sample_rate'] = (isset($trackarray['SamplingFrequency']) ? $trackarray['SamplingFrequency'] : 8000.0); - $track_info['channels'] = (isset($trackarray['Channels']) ? $trackarray['Channels'] : 1); - $track_info['language'] = (isset($trackarray['Language']) ? $trackarray['Language'] : 'eng'); - if (isset($trackarray['BitDepth'])) { $track_info['bits_per_sample'] = $trackarray['BitDepth']; } - if (isset($trackarray['CodecName'])) { $track_info['codec'] = $trackarray['CodecName']; } - - switch ($trackarray['CodecID']) { - case 'A_PCM/INT/LIT': - case 'A_PCM/INT/BIG': - $track_info['bitrate'] = $trackarray['SamplingFrequency'] * $trackarray['Channels'] * $trackarray['BitDepth']; - break; - - case 'A_AC3': - case 'A_DTS': - case 'A_MPEG/L3': - case 'A_MPEG/L2': - case 'A_FLAC': - if (!getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.'.($track_info['dataformat'] == 'mp2' ? 'mp3' : $track_info['dataformat']).'.php', __FILE__, false)) { - $this->warning('Unable to parse audio data ['.basename(__FILE__).':'.__LINE__.'] because cannot include "module.audio.'.$track_info['dataformat'].'.php"'); - break; - } - - if (!isset($info['matroska']['track_data_offsets'][$trackarray['TrackNumber']])) { - $this->warning('Unable to parse audio data ['.basename(__FILE__).':'.__LINE__.'] because $info[matroska][track_data_offsets]['.$trackarray['TrackNumber'].'] not set'); - break; - } - - // create temp instance - $getid3_temp = new getID3(); - if ($track_info['dataformat'] != 'flac') { - $getid3_temp->openfile($this->getid3->filename); - } - $getid3_temp->info['avdataoffset'] = $info['matroska']['track_data_offsets'][$trackarray['TrackNumber']]['offset']; - if ($track_info['dataformat'][0] == 'm' || $track_info['dataformat'] == 'flac') { - $getid3_temp->info['avdataend'] = $info['matroska']['track_data_offsets'][$trackarray['TrackNumber']]['offset'] + $info['matroska']['track_data_offsets'][$trackarray['TrackNumber']]['length']; - } - - // analyze - $class = 'getid3_'.($track_info['dataformat'] == 'mp2' ? 'mp3' : $track_info['dataformat']); - $header_data_key = $track_info['dataformat'][0] == 'm' ? 'mpeg' : $track_info['dataformat']; - $getid3_audio = new $class($getid3_temp, __CLASS__); - if ($track_info['dataformat'] == 'flac') { - $getid3_audio->AnalyzeString($trackarray['CodecPrivate']); - } - else { - $getid3_audio->Analyze(); - } - if (!empty($getid3_temp->info[$header_data_key])) { - $info['matroska']['track_codec_parsed'][$trackarray['TrackNumber']] = $getid3_temp->info[$header_data_key]; - if (isset($getid3_temp->info['audio']) && is_array($getid3_temp->info['audio'])) { - foreach ($getid3_temp->info['audio'] as $key => $value) { - $track_info[$key] = $value; - } - } - } - else { - $this->warning('Unable to parse audio data ['.basename(__FILE__).':'.__LINE__.'] because '.$class.'::Analyze() failed at offset '.$getid3_temp->info['avdataoffset']); - } - - // copy errors and warnings - if (!empty($getid3_temp->info['error'])) { - foreach ($getid3_temp->info['error'] as $newerror) { - $this->warning($class.'() says: ['.$newerror.']'); - } - } - if (!empty($getid3_temp->info['warning'])) { - foreach ($getid3_temp->info['warning'] as $newerror) { - if ($track_info['dataformat'] == 'mp3' && preg_match('/^Probable truncated file: expecting \d+ bytes of audio data, only found \d+ \(short by \d+ bytes\)$/', $newerror)) { - // LAME/Xing header is probably set, but audio data is chunked into Matroska file and near-impossible to verify if audio stream is complete, so ignore useless warning - continue; - } - $this->warning($class.'() says: ['.$newerror.']'); - } - } - unset($getid3_temp, $getid3_audio); - break; - - case 'A_AAC': - case 'A_AAC/MPEG2/LC': - case 'A_AAC/MPEG2/LC/SBR': - case 'A_AAC/MPEG4/LC': - case 'A_AAC/MPEG4/LC/SBR': - $this->warning($trackarray['CodecID'].' audio data contains no header, audio/video bitrates can\'t be calculated'); - break; - - case 'A_VORBIS': - if (!isset($trackarray['CodecPrivate'])) { - $this->warning('Unable to parse audio data ['.basename(__FILE__).':'.__LINE__.'] because CodecPrivate data not set'); - break; - } - $vorbis_offset = strpos($trackarray['CodecPrivate'], 'vorbis', 1); - if ($vorbis_offset === false) { - $this->warning('Unable to parse audio data ['.basename(__FILE__).':'.__LINE__.'] because CodecPrivate data does not contain "vorbis" keyword'); - break; - } - $vorbis_offset -= 1; - - if (!getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.ogg.php', __FILE__, false)) { - $this->warning('Unable to parse audio data ['.basename(__FILE__).':'.__LINE__.'] because cannot include "module.audio.ogg.php"'); - break; - } - - // create temp instance - $getid3_temp = new getID3(); - - // analyze - $getid3_ogg = new getid3_ogg($getid3_temp); - $oggpageinfo['page_seqno'] = 0; - $getid3_ogg->ParseVorbisPageHeader($trackarray['CodecPrivate'], $vorbis_offset, $oggpageinfo); - if (!empty($getid3_temp->info['ogg'])) { - $info['matroska']['track_codec_parsed'][$trackarray['TrackNumber']] = $getid3_temp->info['ogg']; - if (isset($getid3_temp->info['audio']) && is_array($getid3_temp->info['audio'])) { - foreach ($getid3_temp->info['audio'] as $key => $value) { - $track_info[$key] = $value; - } - } - } - - // copy errors and warnings - if (!empty($getid3_temp->info['error'])) { - foreach ($getid3_temp->info['error'] as $newerror) { - $this->warning('getid3_ogg() says: ['.$newerror.']'); - } - } - if (!empty($getid3_temp->info['warning'])) { - foreach ($getid3_temp->info['warning'] as $newerror) { - $this->warning('getid3_ogg() says: ['.$newerror.']'); - } - } - - if (!empty($getid3_temp->info['ogg']['bitrate_nominal'])) { - $track_info['bitrate'] = $getid3_temp->info['ogg']['bitrate_nominal']; - } - unset($getid3_temp, $getid3_ogg, $oggpageinfo, $vorbis_offset); - break; - - case 'A_MS/ACM': - if (!getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, false)) { - $this->warning('Unable to parse audio data ['.basename(__FILE__).':'.__LINE__.'] because cannot include "module.audio-video.riff.php"'); - break; - } - - $parsed = getid3_riff::parseWAVEFORMATex($trackarray['CodecPrivate']); - foreach ($parsed as $key => $value) { - if ($key != 'raw') { - $track_info[$key] = $value; - } - } - $info['matroska']['track_codec_parsed'][$trackarray['TrackNumber']] = $parsed; - break; - - default: - $this->warning('Unhandled audio type "'.(isset($trackarray['CodecID']) ? $trackarray['CodecID'] : '').'"'); - } - - $info['audio']['streams'][] = $track_info; - break; - } - } - - if (!empty($info['video']['streams'])) { - $info['video'] = self::getDefaultStreamInfo($info['video']['streams']); - } - if (!empty($info['audio']['streams'])) { - $info['audio'] = self::getDefaultStreamInfo($info['audio']['streams']); - } - } - - // process attachments - if (isset($info['matroska']['attachments']) && $this->getid3->option_save_attachments !== getID3::ATTACHMENTS_NONE) { - foreach ($info['matroska']['attachments'] as $i => $entry) { - if (strpos($entry['FileMimeType'], 'image/') === 0 && !empty($entry['FileData'])) { - $info['matroska']['comments']['picture'][] = array('data' => $entry['FileData'], 'image_mime' => $entry['FileMimeType'], 'filename' => $entry['FileName']); - } - } - } - - // determine mime type - if (!empty($info['video']['streams'])) { - $info['mime_type'] = ($info['matroska']['doctype'] == 'webm' ? 'video/webm' : 'video/x-matroska'); - } elseif (!empty($info['audio']['streams'])) { - $info['mime_type'] = ($info['matroska']['doctype'] == 'webm' ? 'audio/webm' : 'audio/x-matroska'); - } elseif (isset($info['mime_type'])) { - unset($info['mime_type']); - } - - return true; - } - - private function parseEBML(&$info) { - // http://www.matroska.org/technical/specs/index.html#EBMLBasics - $this->current_offset = $info['avdataoffset']; - - while ($this->getEBMLelement($top_element, $info['avdataend'])) { - switch ($top_element['id']) { - - case EBML_ID_EBML: - $info['fileformat'] = 'matroska'; - $info['matroska']['header']['offset'] = $top_element['offset']; - $info['matroska']['header']['length'] = $top_element['length']; - - while ($this->getEBMLelement($element_data, $top_element['end'], true)) { - switch ($element_data['id']) { - - case EBML_ID_EBMLVERSION: - case EBML_ID_EBMLREADVERSION: - case EBML_ID_EBMLMAXIDLENGTH: - case EBML_ID_EBMLMAXSIZELENGTH: - case EBML_ID_DOCTYPEVERSION: - case EBML_ID_DOCTYPEREADVERSION: - $element_data['data'] = getid3_lib::BigEndian2Int($element_data['data']); - break; - - case EBML_ID_DOCTYPE: - $element_data['data'] = getid3_lib::trimNullByte($element_data['data']); - $info['matroska']['doctype'] = $element_data['data']; - break; - - case EBML_ID_CRC32: // not useful, ignore - $this->current_offset = $element_data['end']; - unset($element_data); - break; - - default: - $this->unhandledElement('header', __LINE__, $element_data); - } - if (!empty($element_data)) { - unset($element_data['offset'], $element_data['end']); - $info['matroska']['header']['elements'][] = $element_data; - } - } - break; - - case EBML_ID_SEGMENT: - $info['matroska']['segment'][0]['offset'] = $top_element['offset']; - $info['matroska']['segment'][0]['length'] = $top_element['length']; - - while ($this->getEBMLelement($element_data, $top_element['end'])) { - if ($element_data['id'] != EBML_ID_CLUSTER || !self::$hide_clusters) { // collect clusters only if required - $info['matroska']['segments'][] = $element_data; - } - switch ($element_data['id']) { - - case EBML_ID_SEEKHEAD: // Contains the position of other level 1 elements. - - while ($this->getEBMLelement($seek_entry, $element_data['end'])) { - switch ($seek_entry['id']) { - - case EBML_ID_SEEK: // Contains a single seek entry to an EBML element - while ($this->getEBMLelement($sub_seek_entry, $seek_entry['end'], true)) { - - switch ($sub_seek_entry['id']) { - - case EBML_ID_SEEKID: - $seek_entry['target_id'] = self::EBML2Int($sub_seek_entry['data']); - $seek_entry['target_name'] = self::EBMLidName($seek_entry['target_id']); - break; - - case EBML_ID_SEEKPOSITION: - $seek_entry['target_offset'] = $element_data['offset'] + getid3_lib::BigEndian2Int($sub_seek_entry['data']); - break; - - default: - $this->unhandledElement('seekhead.seek', __LINE__, $sub_seek_entry); } - } - - if ($seek_entry['target_id'] != EBML_ID_CLUSTER || !self::$hide_clusters) { // collect clusters only if required - $info['matroska']['seek'][] = $seek_entry; - } - break; - - default: - $this->unhandledElement('seekhead', __LINE__, $seek_entry); - } - } - break; - - case EBML_ID_TRACKS: // A top-level block of information with many tracks described. - $info['matroska']['tracks'] = $element_data; - - while ($this->getEBMLelement($track_entry, $element_data['end'])) { - switch ($track_entry['id']) { - - case EBML_ID_TRACKENTRY: //subelements: Describes a track with all elements. - - while ($this->getEBMLelement($subelement, $track_entry['end'], array(EBML_ID_VIDEO, EBML_ID_AUDIO, EBML_ID_CONTENTENCODINGS, EBML_ID_CODECPRIVATE))) { - switch ($subelement['id']) { - - case EBML_ID_TRACKNUMBER: - case EBML_ID_TRACKUID: - case EBML_ID_TRACKTYPE: - case EBML_ID_MINCACHE: - case EBML_ID_MAXCACHE: - case EBML_ID_MAXBLOCKADDITIONID: - case EBML_ID_DEFAULTDURATION: // nanoseconds per frame - $track_entry[$subelement['id_name']] = getid3_lib::BigEndian2Int($subelement['data']); - break; - - case EBML_ID_TRACKTIMECODESCALE: - $track_entry[$subelement['id_name']] = getid3_lib::BigEndian2Float($subelement['data']); - break; - - case EBML_ID_CODECID: - case EBML_ID_LANGUAGE: - case EBML_ID_NAME: - case EBML_ID_CODECNAME: - $track_entry[$subelement['id_name']] = getid3_lib::trimNullByte($subelement['data']); - break; - - case EBML_ID_CODECPRIVATE: - $track_entry[$subelement['id_name']] = $this->readEBMLelementData($subelement['length'], true); - break; - - case EBML_ID_FLAGENABLED: - case EBML_ID_FLAGDEFAULT: - case EBML_ID_FLAGFORCED: - case EBML_ID_FLAGLACING: - case EBML_ID_CODECDECODEALL: - $track_entry[$subelement['id_name']] = (bool) getid3_lib::BigEndian2Int($subelement['data']); - break; - - case EBML_ID_VIDEO: - - while ($this->getEBMLelement($sub_subelement, $subelement['end'], true)) { - switch ($sub_subelement['id']) { - - case EBML_ID_PIXELWIDTH: - case EBML_ID_PIXELHEIGHT: - case EBML_ID_PIXELCROPBOTTOM: - case EBML_ID_PIXELCROPTOP: - case EBML_ID_PIXELCROPLEFT: - case EBML_ID_PIXELCROPRIGHT: - case EBML_ID_DISPLAYWIDTH: - case EBML_ID_DISPLAYHEIGHT: - case EBML_ID_DISPLAYUNIT: - case EBML_ID_ASPECTRATIOTYPE: - case EBML_ID_STEREOMODE: - case EBML_ID_OLDSTEREOMODE: - $track_entry[$sub_subelement['id_name']] = getid3_lib::BigEndian2Int($sub_subelement['data']); - break; - - case EBML_ID_FLAGINTERLACED: - $track_entry[$sub_subelement['id_name']] = (bool)getid3_lib::BigEndian2Int($sub_subelement['data']); - break; - - case EBML_ID_GAMMAVALUE: - $track_entry[$sub_subelement['id_name']] = getid3_lib::BigEndian2Float($sub_subelement['data']); - break; - - case EBML_ID_COLOURSPACE: - $track_entry[$sub_subelement['id_name']] = getid3_lib::trimNullByte($sub_subelement['data']); - break; - - default: - $this->unhandledElement('track.video', __LINE__, $sub_subelement); - } - } - break; - - case EBML_ID_AUDIO: - - while ($this->getEBMLelement($sub_subelement, $subelement['end'], true)) { - switch ($sub_subelement['id']) { - - case EBML_ID_CHANNELS: - case EBML_ID_BITDEPTH: - $track_entry[$sub_subelement['id_name']] = getid3_lib::BigEndian2Int($sub_subelement['data']); - break; - - case EBML_ID_SAMPLINGFREQUENCY: - case EBML_ID_OUTPUTSAMPLINGFREQUENCY: - $track_entry[$sub_subelement['id_name']] = getid3_lib::BigEndian2Float($sub_subelement['data']); - break; - - case EBML_ID_CHANNELPOSITIONS: - $track_entry[$sub_subelement['id_name']] = getid3_lib::trimNullByte($sub_subelement['data']); - break; - - default: - $this->unhandledElement('track.audio', __LINE__, $sub_subelement); - } - } - break; - - case EBML_ID_CONTENTENCODINGS: - - while ($this->getEBMLelement($sub_subelement, $subelement['end'])) { - switch ($sub_subelement['id']) { - - case EBML_ID_CONTENTENCODING: - - while ($this->getEBMLelement($sub_sub_subelement, $sub_subelement['end'], array(EBML_ID_CONTENTCOMPRESSION, EBML_ID_CONTENTENCRYPTION))) { - switch ($sub_sub_subelement['id']) { - - case EBML_ID_CONTENTENCODINGORDER: - case EBML_ID_CONTENTENCODINGSCOPE: - case EBML_ID_CONTENTENCODINGTYPE: - $track_entry[$sub_subelement['id_name']][$sub_sub_subelement['id_name']] = getid3_lib::BigEndian2Int($sub_sub_subelement['data']); - break; - - case EBML_ID_CONTENTCOMPRESSION: - - while ($this->getEBMLelement($sub_sub_sub_subelement, $sub_sub_subelement['end'], true)) { - switch ($sub_sub_sub_subelement['id']) { - - case EBML_ID_CONTENTCOMPALGO: - $track_entry[$sub_subelement['id_name']][$sub_sub_subelement['id_name']][$sub_sub_sub_subelement['id_name']] = getid3_lib::BigEndian2Int($sub_sub_sub_subelement['data']); - break; - - case EBML_ID_CONTENTCOMPSETTINGS: - $track_entry[$sub_subelement['id_name']][$sub_sub_subelement['id_name']][$sub_sub_sub_subelement['id_name']] = $sub_sub_sub_subelement['data']; - break; - - default: - $this->unhandledElement('track.contentencodings.contentencoding.contentcompression', __LINE__, $sub_sub_sub_subelement); - } - } - break; - - case EBML_ID_CONTENTENCRYPTION: - - while ($this->getEBMLelement($sub_sub_sub_subelement, $sub_sub_subelement['end'], true)) { - switch ($sub_sub_sub_subelement['id']) { - - case EBML_ID_CONTENTENCALGO: - case EBML_ID_CONTENTSIGALGO: - case EBML_ID_CONTENTSIGHASHALGO: - $track_entry[$sub_subelement['id_name']][$sub_sub_subelement['id_name']][$sub_sub_sub_subelement['id_name']] = getid3_lib::BigEndian2Int($sub_sub_sub_subelement['data']); - break; - - case EBML_ID_CONTENTENCKEYID: - case EBML_ID_CONTENTSIGNATURE: - case EBML_ID_CONTENTSIGKEYID: - $track_entry[$sub_subelement['id_name']][$sub_sub_subelement['id_name']][$sub_sub_sub_subelement['id_name']] = $sub_sub_sub_subelement['data']; - break; - - default: - $this->unhandledElement('track.contentencodings.contentencoding.contentcompression', __LINE__, $sub_sub_sub_subelement); - } - } - break; - - default: - $this->unhandledElement('track.contentencodings.contentencoding', __LINE__, $sub_sub_subelement); - } - } - break; - - default: - $this->unhandledElement('track.contentencodings', __LINE__, $sub_subelement); - } - } - break; - - default: - $this->unhandledElement('track', __LINE__, $subelement); - } - } - - $info['matroska']['tracks']['tracks'][] = $track_entry; - break; - - default: - $this->unhandledElement('tracks', __LINE__, $track_entry); - } - } - break; - - case EBML_ID_INFO: // Contains miscellaneous general information and statistics on the file. - $info_entry = array(); - - while ($this->getEBMLelement($subelement, $element_data['end'], true)) { - switch ($subelement['id']) { - - case EBML_ID_TIMECODESCALE: - $info_entry[$subelement['id_name']] = getid3_lib::BigEndian2Int($subelement['data']); - break; - - case EBML_ID_DURATION: - $info_entry[$subelement['id_name']] = getid3_lib::BigEndian2Float($subelement['data']); - break; - - case EBML_ID_DATEUTC: - $info_entry[$subelement['id_name']] = getid3_lib::BigEndian2Int($subelement['data']); - $info_entry[$subelement['id_name'].'_unix'] = self::EBMLdate2unix($info_entry[$subelement['id_name']]); - break; - - case EBML_ID_SEGMENTUID: - case EBML_ID_PREVUID: - case EBML_ID_NEXTUID: - $info_entry[$subelement['id_name']] = getid3_lib::trimNullByte($subelement['data']); - break; - - case EBML_ID_SEGMENTFAMILY: - $info_entry[$subelement['id_name']][] = getid3_lib::trimNullByte($subelement['data']); - break; - - case EBML_ID_SEGMENTFILENAME: - case EBML_ID_PREVFILENAME: - case EBML_ID_NEXTFILENAME: - case EBML_ID_TITLE: - case EBML_ID_MUXINGAPP: - case EBML_ID_WRITINGAPP: - $info_entry[$subelement['id_name']] = getid3_lib::trimNullByte($subelement['data']); - $info['matroska']['comments'][strtolower($subelement['id_name'])][] = $info_entry[$subelement['id_name']]; - break; - - case EBML_ID_CHAPTERTRANSLATE: - $chaptertranslate_entry = array(); - - while ($this->getEBMLelement($sub_subelement, $subelement['end'], true)) { - switch ($sub_subelement['id']) { - - case EBML_ID_CHAPTERTRANSLATEEDITIONUID: - $chaptertranslate_entry[$sub_subelement['id_name']][] = getid3_lib::BigEndian2Int($sub_subelement['data']); - break; - - case EBML_ID_CHAPTERTRANSLATECODEC: - $chaptertranslate_entry[$sub_subelement['id_name']] = getid3_lib::BigEndian2Int($sub_subelement['data']); - break; - - case EBML_ID_CHAPTERTRANSLATEID: - $chaptertranslate_entry[$sub_subelement['id_name']] = getid3_lib::trimNullByte($sub_subelement['data']); - break; - - default: - $this->unhandledElement('info.chaptertranslate', __LINE__, $sub_subelement); - } - } - $info_entry[$subelement['id_name']] = $chaptertranslate_entry; - break; - - default: - $this->unhandledElement('info', __LINE__, $subelement); - } - } - $info['matroska']['info'][] = $info_entry; - break; - - case EBML_ID_CUES: // A top-level element to speed seeking access. All entries are local to the segment. Should be mandatory for non "live" streams. - if (self::$hide_clusters) { // do not parse cues if hide clusters is "ON" till they point to clusters anyway - $this->current_offset = $element_data['end']; - break; - } - $cues_entry = array(); - - while ($this->getEBMLelement($subelement, $element_data['end'])) { - switch ($subelement['id']) { - - case EBML_ID_CUEPOINT: - $cuepoint_entry = array(); - - while ($this->getEBMLelement($sub_subelement, $subelement['end'], array(EBML_ID_CUETRACKPOSITIONS))) { - switch ($sub_subelement['id']) { - - case EBML_ID_CUETRACKPOSITIONS: - $cuetrackpositions_entry = array(); - - while ($this->getEBMLelement($sub_sub_subelement, $sub_subelement['end'], true)) { - switch ($sub_sub_subelement['id']) { - - case EBML_ID_CUETRACK: - case EBML_ID_CUECLUSTERPOSITION: - case EBML_ID_CUEBLOCKNUMBER: - case EBML_ID_CUECODECSTATE: - $cuetrackpositions_entry[$sub_sub_subelement['id_name']] = getid3_lib::BigEndian2Int($sub_sub_subelement['data']); - break; - - default: - $this->unhandledElement('cues.cuepoint.cuetrackpositions', __LINE__, $sub_sub_subelement); - } - } - $cuepoint_entry[$sub_subelement['id_name']][] = $cuetrackpositions_entry; - break; - - case EBML_ID_CUETIME: - $cuepoint_entry[$sub_subelement['id_name']] = getid3_lib::BigEndian2Int($sub_subelement['data']); - break; - - default: - $this->unhandledElement('cues.cuepoint', __LINE__, $sub_subelement); - } - } - $cues_entry[] = $cuepoint_entry; - break; - - default: - $this->unhandledElement('cues', __LINE__, $subelement); - } - } - $info['matroska']['cues'] = $cues_entry; - break; - - case EBML_ID_TAGS: // Element containing elements specific to Tracks/Chapters. - $tags_entry = array(); - - while ($this->getEBMLelement($subelement, $element_data['end'], false)) { - switch ($subelement['id']) { - - case EBML_ID_TAG: - $tag_entry = array(); - - while ($this->getEBMLelement($sub_subelement, $subelement['end'], false)) { - switch ($sub_subelement['id']) { - - case EBML_ID_TARGETS: - $targets_entry = array(); - - while ($this->getEBMLelement($sub_sub_subelement, $sub_subelement['end'], true)) { - switch ($sub_sub_subelement['id']) { - - case EBML_ID_TARGETTYPEVALUE: - $targets_entry[$sub_sub_subelement['id_name']] = getid3_lib::BigEndian2Int($sub_sub_subelement['data']); - $targets_entry[strtolower($sub_sub_subelement['id_name']).'_long'] = self::TargetTypeValue($targets_entry[$sub_sub_subelement['id_name']]); - break; - - case EBML_ID_TARGETTYPE: - $targets_entry[$sub_sub_subelement['id_name']] = $sub_sub_subelement['data']; - break; - - case EBML_ID_TAGTRACKUID: - case EBML_ID_TAGEDITIONUID: - case EBML_ID_TAGCHAPTERUID: - case EBML_ID_TAGATTACHMENTUID: - $targets_entry[$sub_sub_subelement['id_name']][] = getid3_lib::BigEndian2Int($sub_sub_subelement['data']); - break; - - default: - $this->unhandledElement('tags.tag.targets', __LINE__, $sub_sub_subelement); - } - } - $tag_entry[$sub_subelement['id_name']] = $targets_entry; - break; - - case EBML_ID_SIMPLETAG: - $tag_entry[$sub_subelement['id_name']][] = $this->HandleEMBLSimpleTag($sub_subelement['end']); - break; - - default: - $this->unhandledElement('tags.tag', __LINE__, $sub_subelement); - } - } - $tags_entry[] = $tag_entry; - break; - - default: - $this->unhandledElement('tags', __LINE__, $subelement); - } - } - $info['matroska']['tags'] = $tags_entry; - break; - - case EBML_ID_ATTACHMENTS: // Contain attached files. - - while ($this->getEBMLelement($subelement, $element_data['end'])) { - switch ($subelement['id']) { - - case EBML_ID_ATTACHEDFILE: - $attachedfile_entry = array(); - - while ($this->getEBMLelement($sub_subelement, $subelement['end'], array(EBML_ID_FILEDATA))) { - switch ($sub_subelement['id']) { - - case EBML_ID_FILEDESCRIPTION: - case EBML_ID_FILENAME: - case EBML_ID_FILEMIMETYPE: - $attachedfile_entry[$sub_subelement['id_name']] = $sub_subelement['data']; - break; - - case EBML_ID_FILEDATA: - $attachedfile_entry['data_offset'] = $this->current_offset; - $attachedfile_entry['data_length'] = $sub_subelement['length']; - - $attachedfile_entry[$sub_subelement['id_name']] = $this->saveAttachment( - $attachedfile_entry['FileName'], - $attachedfile_entry['data_offset'], - $attachedfile_entry['data_length']); - - $this->current_offset = $sub_subelement['end']; - break; - - case EBML_ID_FILEUID: - $attachedfile_entry[$sub_subelement['id_name']] = getid3_lib::BigEndian2Int($sub_subelement['data']); - break; - - default: - $this->unhandledElement('attachments.attachedfile', __LINE__, $sub_subelement); - } - } - $info['matroska']['attachments'][] = $attachedfile_entry; - break; - - default: - $this->unhandledElement('attachments', __LINE__, $subelement); - } - } - break; - - case EBML_ID_CHAPTERS: - - while ($this->getEBMLelement($subelement, $element_data['end'])) { - switch ($subelement['id']) { - - case EBML_ID_EDITIONENTRY: - $editionentry_entry = array(); - - while ($this->getEBMLelement($sub_subelement, $subelement['end'], array(EBML_ID_CHAPTERATOM))) { - switch ($sub_subelement['id']) { - - case EBML_ID_EDITIONUID: - $editionentry_entry[$sub_subelement['id_name']] = getid3_lib::BigEndian2Int($sub_subelement['data']); - break; - - case EBML_ID_EDITIONFLAGHIDDEN: - case EBML_ID_EDITIONFLAGDEFAULT: - case EBML_ID_EDITIONFLAGORDERED: - $editionentry_entry[$sub_subelement['id_name']] = (bool)getid3_lib::BigEndian2Int($sub_subelement['data']); - break; - - case EBML_ID_CHAPTERATOM: - $chapteratom_entry = array(); - - while ($this->getEBMLelement($sub_sub_subelement, $sub_subelement['end'], array(EBML_ID_CHAPTERTRACK, EBML_ID_CHAPTERDISPLAY))) { - switch ($sub_sub_subelement['id']) { - - case EBML_ID_CHAPTERSEGMENTUID: - case EBML_ID_CHAPTERSEGMENTEDITIONUID: - $chapteratom_entry[$sub_sub_subelement['id_name']] = $sub_sub_subelement['data']; - break; - - case EBML_ID_CHAPTERFLAGENABLED: - case EBML_ID_CHAPTERFLAGHIDDEN: - $chapteratom_entry[$sub_sub_subelement['id_name']] = (bool)getid3_lib::BigEndian2Int($sub_sub_subelement['data']); - break; - - case EBML_ID_CHAPTERUID: - case EBML_ID_CHAPTERTIMESTART: - case EBML_ID_CHAPTERTIMEEND: - $chapteratom_entry[$sub_sub_subelement['id_name']] = getid3_lib::BigEndian2Int($sub_sub_subelement['data']); - break; - - case EBML_ID_CHAPTERTRACK: - $chaptertrack_entry = array(); - - while ($this->getEBMLelement($sub_sub_sub_subelement, $sub_sub_subelement['end'], true)) { - switch ($sub_sub_sub_subelement['id']) { - - case EBML_ID_CHAPTERTRACKNUMBER: - $chaptertrack_entry[$sub_sub_sub_subelement['id_name']] = getid3_lib::BigEndian2Int($sub_sub_sub_subelement['data']); - break; - - default: - $this->unhandledElement('chapters.editionentry.chapteratom.chaptertrack', __LINE__, $sub_sub_sub_subelement); - } - } - $chapteratom_entry[$sub_sub_subelement['id_name']][] = $chaptertrack_entry; - break; - - case EBML_ID_CHAPTERDISPLAY: - $chapterdisplay_entry = array(); - - while ($this->getEBMLelement($sub_sub_sub_subelement, $sub_sub_subelement['end'], true)) { - switch ($sub_sub_sub_subelement['id']) { - - case EBML_ID_CHAPSTRING: - case EBML_ID_CHAPLANGUAGE: - case EBML_ID_CHAPCOUNTRY: - $chapterdisplay_entry[$sub_sub_sub_subelement['id_name']] = $sub_sub_sub_subelement['data']; - break; - - default: - $this->unhandledElement('chapters.editionentry.chapteratom.chapterdisplay', __LINE__, $sub_sub_sub_subelement); - } - } - $chapteratom_entry[$sub_sub_subelement['id_name']][] = $chapterdisplay_entry; - break; - - default: - $this->unhandledElement('chapters.editionentry.chapteratom', __LINE__, $sub_sub_subelement); - } - } - $editionentry_entry[$sub_subelement['id_name']][] = $chapteratom_entry; - break; - - default: - $this->unhandledElement('chapters.editionentry', __LINE__, $sub_subelement); - } - } - $info['matroska']['chapters'][] = $editionentry_entry; - break; - - default: - $this->unhandledElement('chapters', __LINE__, $subelement); - } - } - break; - - case EBML_ID_CLUSTER: // The lower level element containing the (monolithic) Block structure. - $cluster_entry = array(); - - while ($this->getEBMLelement($subelement, $element_data['end'], array(EBML_ID_CLUSTERSILENTTRACKS, EBML_ID_CLUSTERBLOCKGROUP, EBML_ID_CLUSTERSIMPLEBLOCK))) { - switch ($subelement['id']) { - - case EBML_ID_CLUSTERTIMECODE: - case EBML_ID_CLUSTERPOSITION: - case EBML_ID_CLUSTERPREVSIZE: - $cluster_entry[$subelement['id_name']] = getid3_lib::BigEndian2Int($subelement['data']); - break; - - case EBML_ID_CLUSTERSILENTTRACKS: - $cluster_silent_tracks = array(); - - while ($this->getEBMLelement($sub_subelement, $subelement['end'], true)) { - switch ($sub_subelement['id']) { - - case EBML_ID_CLUSTERSILENTTRACKNUMBER: - $cluster_silent_tracks[] = getid3_lib::BigEndian2Int($sub_subelement['data']); - break; - - default: - $this->unhandledElement('cluster.silenttracks', __LINE__, $sub_subelement); - } - } - $cluster_entry[$subelement['id_name']][] = $cluster_silent_tracks; - break; - - case EBML_ID_CLUSTERBLOCKGROUP: - $cluster_block_group = array('offset' => $this->current_offset); - - while ($this->getEBMLelement($sub_subelement, $subelement['end'], array(EBML_ID_CLUSTERBLOCK))) { - switch ($sub_subelement['id']) { - - case EBML_ID_CLUSTERBLOCK: - $cluster_block_group[$sub_subelement['id_name']] = $this->HandleEMBLClusterBlock($sub_subelement, EBML_ID_CLUSTERBLOCK, $info); - break; - - case EBML_ID_CLUSTERREFERENCEPRIORITY: // unsigned-int - case EBML_ID_CLUSTERBLOCKDURATION: // unsigned-int - $cluster_block_group[$sub_subelement['id_name']] = getid3_lib::BigEndian2Int($sub_subelement['data']); - break; - - case EBML_ID_CLUSTERREFERENCEBLOCK: // signed-int - $cluster_block_group[$sub_subelement['id_name']][] = getid3_lib::BigEndian2Int($sub_subelement['data'], false, true); - break; - - case EBML_ID_CLUSTERCODECSTATE: - $cluster_block_group[$sub_subelement['id_name']] = getid3_lib::trimNullByte($sub_subelement['data']); - break; - - default: - $this->unhandledElement('clusters.blockgroup', __LINE__, $sub_subelement); - } - } - $cluster_entry[$subelement['id_name']][] = $cluster_block_group; - break; - - case EBML_ID_CLUSTERSIMPLEBLOCK: - $cluster_entry[$subelement['id_name']][] = $this->HandleEMBLClusterBlock($subelement, EBML_ID_CLUSTERSIMPLEBLOCK, $info); - break; - - default: - $this->unhandledElement('cluster', __LINE__, $subelement); - } - $this->current_offset = $subelement['end']; - } - if (!self::$hide_clusters) { - $info['matroska']['cluster'][] = $cluster_entry; - } - - // check to see if all the data we need exists already, if so, break out of the loop - if (!self::$parse_whole_file) { - if (isset($info['matroska']['info']) && is_array($info['matroska']['info'])) { - if (isset($info['matroska']['tracks']['tracks']) && is_array($info['matroska']['tracks']['tracks'])) { - if (count($info['matroska']['track_data_offsets']) == count($info['matroska']['tracks']['tracks'])) { - return; - } - } - } - } - break; - - default: - $this->unhandledElement('segment', __LINE__, $element_data); - } - } - break; - - default: - $this->unhandledElement('root', __LINE__, $top_element); - } - } - } - - private function EnsureBufferHasEnoughData($min_data=1024) { - if (($this->current_offset - $this->EBMLbuffer_offset) >= ($this->EBMLbuffer_length - $min_data)) { - $read_bytes = max($min_data, $this->getid3->fread_buffer_size()); - - try { - $this->fseek($this->current_offset); - $this->EBMLbuffer_offset = $this->current_offset; - $this->EBMLbuffer = $this->fread($read_bytes); - $this->EBMLbuffer_length = strlen($this->EBMLbuffer); - } catch (getid3_exception $e) { - $this->warning('EBML parser: '.$e->getMessage()); - return false; - } - - if ($this->EBMLbuffer_length == 0 && $this->feof()) { - return $this->error('EBML parser: ran out of file at offset '.$this->current_offset); - } - } - return true; - } - - private function readEBMLint() { - $actual_offset = $this->current_offset - $this->EBMLbuffer_offset; - - // get length of integer - $first_byte_int = ord($this->EBMLbuffer[$actual_offset]); - if (0x80 & $first_byte_int) { - $length = 1; - } elseif (0x40 & $first_byte_int) { - $length = 2; - } elseif (0x20 & $first_byte_int) { - $length = 3; - } elseif (0x10 & $first_byte_int) { - $length = 4; - } elseif (0x08 & $first_byte_int) { - $length = 5; - } elseif (0x04 & $first_byte_int) { - $length = 6; - } elseif (0x02 & $first_byte_int) { - $length = 7; - } elseif (0x01 & $first_byte_int) { - $length = 8; - } else { - throw new Exception('invalid EBML integer (leading 0x00) at '.$this->current_offset); - } - - // read - $int_value = self::EBML2Int(substr($this->EBMLbuffer, $actual_offset, $length)); - $this->current_offset += $length; - - return $int_value; - } - - private function readEBMLelementData($length, $check_buffer=false) { - if ($check_buffer && !$this->EnsureBufferHasEnoughData($length)) { - return false; - } - $data = substr($this->EBMLbuffer, $this->current_offset - $this->EBMLbuffer_offset, $length); - $this->current_offset += $length; - return $data; - } - - private function getEBMLelement(&$element, $parent_end, $get_data=false) { - if ($this->current_offset >= $parent_end) { - return false; - } - - if (!$this->EnsureBufferHasEnoughData()) { - $this->current_offset = PHP_INT_MAX; // do not exit parser right now, allow to finish current loop to gather maximum information - return false; - } - - $element = array(); - - // set offset - $element['offset'] = $this->current_offset; - - // get ID - $element['id'] = $this->readEBMLint(); - - // get name - $element['id_name'] = self::EBMLidName($element['id']); - - // get length - $element['length'] = $this->readEBMLint(); - - // get end offset - $element['end'] = $this->current_offset + $element['length']; - - // get raw data - $dont_parse = (in_array($element['id'], $this->unuseful_elements) || $element['id_name'] == dechex($element['id'])); - if (($get_data === true || (is_array($get_data) && !in_array($element['id'], $get_data))) && !$dont_parse) { - $element['data'] = $this->readEBMLelementData($element['length'], $element); - } - - return true; - } - - private function unhandledElement($type, $line, $element) { - // warn only about unknown and missed elements, not about unuseful - if (!in_array($element['id'], $this->unuseful_elements)) { - $this->warning('Unhandled '.$type.' element ['.basename(__FILE__).':'.$line.'] ('.$element['id'].'::'.$element['id_name'].' ['.$element['length'].' bytes]) at '.$element['offset']); - } - - // increase offset for unparsed elements - if (!isset($element['data'])) { - $this->current_offset = $element['end']; - } - } - - private function ExtractCommentsSimpleTag($SimpleTagArray) { - if (!empty($SimpleTagArray['SimpleTag'])) { - foreach ($SimpleTagArray['SimpleTag'] as $SimpleTagKey => $SimpleTagData) { - if (!empty($SimpleTagData['TagName']) && !empty($SimpleTagData['TagString'])) { - $this->getid3->info['matroska']['comments'][strtolower($SimpleTagData['TagName'])][] = $SimpleTagData['TagString']; - } - if (!empty($SimpleTagData['SimpleTag'])) { - $this->ExtractCommentsSimpleTag($SimpleTagData); - } - } - } - - return true; - } - - private function HandleEMBLSimpleTag($parent_end) { - $simpletag_entry = array(); - - while ($this->getEBMLelement($element, $parent_end, array(EBML_ID_SIMPLETAG))) { - switch ($element['id']) { - - case EBML_ID_TAGNAME: - case EBML_ID_TAGLANGUAGE: - case EBML_ID_TAGSTRING: - case EBML_ID_TAGBINARY: - $simpletag_entry[$element['id_name']] = $element['data']; - break; - - case EBML_ID_SIMPLETAG: - $simpletag_entry[$element['id_name']][] = $this->HandleEMBLSimpleTag($element['end']); - break; - - case EBML_ID_TAGDEFAULT: - $simpletag_entry[$element['id_name']] = (bool)getid3_lib::BigEndian2Int($element['data']); - break; - - default: - $this->unhandledElement('tag.simpletag', __LINE__, $element); - } - } - - return $simpletag_entry; - } - - private function HandleEMBLClusterBlock($element, $block_type, &$info) { - // http://www.matroska.org/technical/specs/index.html#block_structure - // http://www.matroska.org/technical/specs/index.html#simpleblock_structure - - $block_data = array(); - $block_data['tracknumber'] = $this->readEBMLint(); - $block_data['timecode'] = getid3_lib::BigEndian2Int($this->readEBMLelementData(2), false, true); - $block_data['flags_raw'] = getid3_lib::BigEndian2Int($this->readEBMLelementData(1)); - - if ($block_type == EBML_ID_CLUSTERSIMPLEBLOCK) { - $block_data['flags']['keyframe'] = (($block_data['flags_raw'] & 0x80) >> 7); - //$block_data['flags']['reserved1'] = (($block_data['flags_raw'] & 0x70) >> 4); - } - else { - //$block_data['flags']['reserved1'] = (($block_data['flags_raw'] & 0xF0) >> 4); - } - $block_data['flags']['invisible'] = (bool)(($block_data['flags_raw'] & 0x08) >> 3); - $block_data['flags']['lacing'] = (($block_data['flags_raw'] & 0x06) >> 1); // 00=no lacing; 01=Xiph lacing; 11=EBML lacing; 10=fixed-size lacing - if ($block_type == EBML_ID_CLUSTERSIMPLEBLOCK) { - $block_data['flags']['discardable'] = (($block_data['flags_raw'] & 0x01)); - } - else { - //$block_data['flags']['reserved2'] = (($block_data['flags_raw'] & 0x01) >> 0); - } - $block_data['flags']['lacing_type'] = self::BlockLacingType($block_data['flags']['lacing']); - - // Lace (when lacing bit is set) - if ($block_data['flags']['lacing'] > 0) { - $block_data['lace_frames'] = getid3_lib::BigEndian2Int($this->readEBMLelementData(1)) + 1; // Number of frames in the lace-1 (uint8) - if ($block_data['flags']['lacing'] != 0x02) { - for ($i = 1; $i < $block_data['lace_frames']; $i ++) { // Lace-coded size of each frame of the lace, except for the last one (multiple uint8). *This is not used with Fixed-size lacing as it is calculated automatically from (total size of lace) / (number of frames in lace). - if ($block_data['flags']['lacing'] == 0x03) { // EBML lacing - $block_data['lace_frames_size'][$i] = $this->readEBMLint(); // TODO: read size correctly, calc size for the last frame. For now offsets are deteminded OK with readEBMLint() and that's the most important thing. - } - else { // Xiph lacing - $block_data['lace_frames_size'][$i] = 0; - do { - $size = getid3_lib::BigEndian2Int($this->readEBMLelementData(1)); - $block_data['lace_frames_size'][$i] += $size; - } - while ($size == 255); - } - } - if ($block_data['flags']['lacing'] == 0x01) { // calc size of the last frame only for Xiph lacing, till EBML sizes are now anyway determined incorrectly - $block_data['lace_frames_size'][] = $element['end'] - $this->current_offset - array_sum($block_data['lace_frames_size']); - } - } - } - - if (!isset($info['matroska']['track_data_offsets'][$block_data['tracknumber']])) { - $info['matroska']['track_data_offsets'][$block_data['tracknumber']]['offset'] = $this->current_offset; - $info['matroska']['track_data_offsets'][$block_data['tracknumber']]['length'] = $element['end'] - $this->current_offset; - //$info['matroska']['track_data_offsets'][$block_data['tracknumber']]['total_length'] = 0; - } - //$info['matroska']['track_data_offsets'][$block_data['tracknumber']]['total_length'] += $info['matroska']['track_data_offsets'][$block_data['tracknumber']]['length']; - //$info['matroska']['track_data_offsets'][$block_data['tracknumber']]['duration'] = $block_data['timecode'] * ((isset($info['matroska']['info'][0]['TimecodeScale']) ? $info['matroska']['info'][0]['TimecodeScale'] : 1000000) / 1000000000); - - // set offset manually - $this->current_offset = $element['end']; - - return $block_data; - } - - private static function EBML2Int($EBMLstring) { - // http://matroska.org/specs/ - - // Element ID coded with an UTF-8 like system: - // 1xxx xxxx - Class A IDs (2^7 -2 possible values) (base 0x8X) - // 01xx xxxx xxxx xxxx - Class B IDs (2^14-2 possible values) (base 0x4X 0xXX) - // 001x xxxx xxxx xxxx xxxx xxxx - Class C IDs (2^21-2 possible values) (base 0x2X 0xXX 0xXX) - // 0001 xxxx xxxx xxxx xxxx xxxx xxxx xxxx - Class D IDs (2^28-2 possible values) (base 0x1X 0xXX 0xXX 0xXX) - // Values with all x at 0 and 1 are reserved (hence the -2). - - // Data size, in octets, is also coded with an UTF-8 like system : - // 1xxx xxxx - value 0 to 2^7-2 - // 01xx xxxx xxxx xxxx - value 0 to 2^14-2 - // 001x xxxx xxxx xxxx xxxx xxxx - value 0 to 2^21-2 - // 0001 xxxx xxxx xxxx xxxx xxxx xxxx xxxx - value 0 to 2^28-2 - // 0000 1xxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx - value 0 to 2^35-2 - // 0000 01xx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx - value 0 to 2^42-2 - // 0000 001x xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx - value 0 to 2^49-2 - // 0000 0001 xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx - value 0 to 2^56-2 - - $first_byte_int = ord($EBMLstring[0]); - if (0x80 & $first_byte_int) { - $EBMLstring[0] = chr($first_byte_int & 0x7F); - } elseif (0x40 & $first_byte_int) { - $EBMLstring[0] = chr($first_byte_int & 0x3F); - } elseif (0x20 & $first_byte_int) { - $EBMLstring[0] = chr($first_byte_int & 0x1F); - } elseif (0x10 & $first_byte_int) { - $EBMLstring[0] = chr($first_byte_int & 0x0F); - } elseif (0x08 & $first_byte_int) { - $EBMLstring[0] = chr($first_byte_int & 0x07); - } elseif (0x04 & $first_byte_int) { - $EBMLstring[0] = chr($first_byte_int & 0x03); - } elseif (0x02 & $first_byte_int) { - $EBMLstring[0] = chr($first_byte_int & 0x01); - } elseif (0x01 & $first_byte_int) { - $EBMLstring[0] = chr($first_byte_int & 0x00); - } - - return getid3_lib::BigEndian2Int($EBMLstring); - } - - private static function EBMLdate2unix($EBMLdatestamp) { - // Date - signed 8 octets integer in nanoseconds with 0 indicating the precise beginning of the millennium (at 2001-01-01T00:00:00,000000000 UTC) - // 978307200 == mktime(0, 0, 0, 1, 1, 2001) == January 1, 2001 12:00:00am UTC - return round(($EBMLdatestamp / 1000000000) + 978307200); - } - - public static function TargetTypeValue($target_type) { - // http://www.matroska.org/technical/specs/tagging/index.html - static $TargetTypeValue = array(); - if (empty($TargetTypeValue)) { - $TargetTypeValue[10] = 'A: ~ V:shot'; // the lowest hierarchy found in music or movies - $TargetTypeValue[20] = 'A:subtrack/part/movement ~ V:scene'; // corresponds to parts of a track for audio (like a movement) - $TargetTypeValue[30] = 'A:track/song ~ V:chapter'; // the common parts of an album or a movie - $TargetTypeValue[40] = 'A:part/session ~ V:part/session'; // when an album or episode has different logical parts - $TargetTypeValue[50] = 'A:album/opera/concert ~ V:movie/episode/concert'; // the most common grouping level of music and video (equals to an episode for TV series) - $TargetTypeValue[60] = 'A:edition/issue/volume/opus ~ V:season/sequel/volume'; // a list of lower levels grouped together - $TargetTypeValue[70] = 'A:collection ~ V:collection'; // the high hierarchy consisting of many different lower items - } - return (isset($TargetTypeValue[$target_type]) ? $TargetTypeValue[$target_type] : $target_type); - } - - public static function BlockLacingType($lacingtype) { - // http://matroska.org/technical/specs/index.html#block_structure - static $BlockLacingType = array(); - if (empty($BlockLacingType)) { - $BlockLacingType[0x00] = 'no lacing'; - $BlockLacingType[0x01] = 'Xiph lacing'; - $BlockLacingType[0x02] = 'fixed-size lacing'; - $BlockLacingType[0x03] = 'EBML lacing'; - } - return (isset($BlockLacingType[$lacingtype]) ? $BlockLacingType[$lacingtype] : $lacingtype); - } - - public static function CodecIDtoCommonName($codecid) { - // http://www.matroska.org/technical/specs/codecid/index.html - static $CodecIDlist = array(); - if (empty($CodecIDlist)) { - $CodecIDlist['A_AAC'] = 'aac'; - $CodecIDlist['A_AAC/MPEG2/LC'] = 'aac'; - $CodecIDlist['A_AC3'] = 'ac3'; - $CodecIDlist['A_DTS'] = 'dts'; - $CodecIDlist['A_FLAC'] = 'flac'; - $CodecIDlist['A_MPEG/L1'] = 'mp1'; - $CodecIDlist['A_MPEG/L2'] = 'mp2'; - $CodecIDlist['A_MPEG/L3'] = 'mp3'; - $CodecIDlist['A_PCM/INT/LIT'] = 'pcm'; // PCM Integer Little Endian - $CodecIDlist['A_PCM/INT/BIG'] = 'pcm'; // PCM Integer Big Endian - $CodecIDlist['A_QUICKTIME/QDMC'] = 'quicktime'; // Quicktime: QDesign Music - $CodecIDlist['A_QUICKTIME/QDM2'] = 'quicktime'; // Quicktime: QDesign Music v2 - $CodecIDlist['A_VORBIS'] = 'vorbis'; - $CodecIDlist['V_MPEG1'] = 'mpeg'; - $CodecIDlist['V_THEORA'] = 'theora'; - $CodecIDlist['V_REAL/RV40'] = 'real'; - $CodecIDlist['V_REAL/RV10'] = 'real'; - $CodecIDlist['V_REAL/RV20'] = 'real'; - $CodecIDlist['V_REAL/RV30'] = 'real'; - $CodecIDlist['V_QUICKTIME'] = 'quicktime'; // Quicktime - $CodecIDlist['V_MPEG4/ISO/AP'] = 'mpeg4'; - $CodecIDlist['V_MPEG4/ISO/ASP'] = 'mpeg4'; - $CodecIDlist['V_MPEG4/ISO/AVC'] = 'h264'; - $CodecIDlist['V_MPEG4/ISO/SP'] = 'mpeg4'; - $CodecIDlist['V_VP8'] = 'vp8'; - $CodecIDlist['V_MS/VFW/FOURCC'] = 'riff'; - $CodecIDlist['A_MS/ACM'] = 'riff'; - } - return (isset($CodecIDlist[$codecid]) ? $CodecIDlist[$codecid] : $codecid); - } - - private static function EBMLidName($value) { - static $EBMLidList = array(); - if (empty($EBMLidList)) { - $EBMLidList[EBML_ID_ASPECTRATIOTYPE] = 'AspectRatioType'; - $EBMLidList[EBML_ID_ATTACHEDFILE] = 'AttachedFile'; - $EBMLidList[EBML_ID_ATTACHMENTLINK] = 'AttachmentLink'; - $EBMLidList[EBML_ID_ATTACHMENTS] = 'Attachments'; - $EBMLidList[EBML_ID_AUDIO] = 'Audio'; - $EBMLidList[EBML_ID_BITDEPTH] = 'BitDepth'; - $EBMLidList[EBML_ID_CHANNELPOSITIONS] = 'ChannelPositions'; - $EBMLidList[EBML_ID_CHANNELS] = 'Channels'; - $EBMLidList[EBML_ID_CHAPCOUNTRY] = 'ChapCountry'; - $EBMLidList[EBML_ID_CHAPLANGUAGE] = 'ChapLanguage'; - $EBMLidList[EBML_ID_CHAPPROCESS] = 'ChapProcess'; - $EBMLidList[EBML_ID_CHAPPROCESSCODECID] = 'ChapProcessCodecID'; - $EBMLidList[EBML_ID_CHAPPROCESSCOMMAND] = 'ChapProcessCommand'; - $EBMLidList[EBML_ID_CHAPPROCESSDATA] = 'ChapProcessData'; - $EBMLidList[EBML_ID_CHAPPROCESSPRIVATE] = 'ChapProcessPrivate'; - $EBMLidList[EBML_ID_CHAPPROCESSTIME] = 'ChapProcessTime'; - $EBMLidList[EBML_ID_CHAPSTRING] = 'ChapString'; - $EBMLidList[EBML_ID_CHAPTERATOM] = 'ChapterAtom'; - $EBMLidList[EBML_ID_CHAPTERDISPLAY] = 'ChapterDisplay'; - $EBMLidList[EBML_ID_CHAPTERFLAGENABLED] = 'ChapterFlagEnabled'; - $EBMLidList[EBML_ID_CHAPTERFLAGHIDDEN] = 'ChapterFlagHidden'; - $EBMLidList[EBML_ID_CHAPTERPHYSICALEQUIV] = 'ChapterPhysicalEquiv'; - $EBMLidList[EBML_ID_CHAPTERS] = 'Chapters'; - $EBMLidList[EBML_ID_CHAPTERSEGMENTEDITIONUID] = 'ChapterSegmentEditionUID'; - $EBMLidList[EBML_ID_CHAPTERSEGMENTUID] = 'ChapterSegmentUID'; - $EBMLidList[EBML_ID_CHAPTERTIMEEND] = 'ChapterTimeEnd'; - $EBMLidList[EBML_ID_CHAPTERTIMESTART] = 'ChapterTimeStart'; - $EBMLidList[EBML_ID_CHAPTERTRACK] = 'ChapterTrack'; - $EBMLidList[EBML_ID_CHAPTERTRACKNUMBER] = 'ChapterTrackNumber'; - $EBMLidList[EBML_ID_CHAPTERTRANSLATE] = 'ChapterTranslate'; - $EBMLidList[EBML_ID_CHAPTERTRANSLATECODEC] = 'ChapterTranslateCodec'; - $EBMLidList[EBML_ID_CHAPTERTRANSLATEEDITIONUID] = 'ChapterTranslateEditionUID'; - $EBMLidList[EBML_ID_CHAPTERTRANSLATEID] = 'ChapterTranslateID'; - $EBMLidList[EBML_ID_CHAPTERUID] = 'ChapterUID'; - $EBMLidList[EBML_ID_CLUSTER] = 'Cluster'; - $EBMLidList[EBML_ID_CLUSTERBLOCK] = 'ClusterBlock'; - $EBMLidList[EBML_ID_CLUSTERBLOCKADDID] = 'ClusterBlockAddID'; - $EBMLidList[EBML_ID_CLUSTERBLOCKADDITIONAL] = 'ClusterBlockAdditional'; - $EBMLidList[EBML_ID_CLUSTERBLOCKADDITIONID] = 'ClusterBlockAdditionID'; - $EBMLidList[EBML_ID_CLUSTERBLOCKADDITIONS] = 'ClusterBlockAdditions'; - $EBMLidList[EBML_ID_CLUSTERBLOCKDURATION] = 'ClusterBlockDuration'; - $EBMLidList[EBML_ID_CLUSTERBLOCKGROUP] = 'ClusterBlockGroup'; - $EBMLidList[EBML_ID_CLUSTERBLOCKMORE] = 'ClusterBlockMore'; - $EBMLidList[EBML_ID_CLUSTERBLOCKVIRTUAL] = 'ClusterBlockVirtual'; - $EBMLidList[EBML_ID_CLUSTERCODECSTATE] = 'ClusterCodecState'; - $EBMLidList[EBML_ID_CLUSTERDELAY] = 'ClusterDelay'; - $EBMLidList[EBML_ID_CLUSTERDURATION] = 'ClusterDuration'; - $EBMLidList[EBML_ID_CLUSTERENCRYPTEDBLOCK] = 'ClusterEncryptedBlock'; - $EBMLidList[EBML_ID_CLUSTERFRAMENUMBER] = 'ClusterFrameNumber'; - $EBMLidList[EBML_ID_CLUSTERLACENUMBER] = 'ClusterLaceNumber'; - $EBMLidList[EBML_ID_CLUSTERPOSITION] = 'ClusterPosition'; - $EBMLidList[EBML_ID_CLUSTERPREVSIZE] = 'ClusterPrevSize'; - $EBMLidList[EBML_ID_CLUSTERREFERENCEBLOCK] = 'ClusterReferenceBlock'; - $EBMLidList[EBML_ID_CLUSTERREFERENCEPRIORITY] = 'ClusterReferencePriority'; - $EBMLidList[EBML_ID_CLUSTERREFERENCEVIRTUAL] = 'ClusterReferenceVirtual'; - $EBMLidList[EBML_ID_CLUSTERSILENTTRACKNUMBER] = 'ClusterSilentTrackNumber'; - $EBMLidList[EBML_ID_CLUSTERSILENTTRACKS] = 'ClusterSilentTracks'; - $EBMLidList[EBML_ID_CLUSTERSIMPLEBLOCK] = 'ClusterSimpleBlock'; - $EBMLidList[EBML_ID_CLUSTERTIMECODE] = 'ClusterTimecode'; - $EBMLidList[EBML_ID_CLUSTERTIMESLICE] = 'ClusterTimeSlice'; - $EBMLidList[EBML_ID_CODECDECODEALL] = 'CodecDecodeAll'; - $EBMLidList[EBML_ID_CODECDOWNLOADURL] = 'CodecDownloadURL'; - $EBMLidList[EBML_ID_CODECID] = 'CodecID'; - $EBMLidList[EBML_ID_CODECINFOURL] = 'CodecInfoURL'; - $EBMLidList[EBML_ID_CODECNAME] = 'CodecName'; - $EBMLidList[EBML_ID_CODECPRIVATE] = 'CodecPrivate'; - $EBMLidList[EBML_ID_CODECSETTINGS] = 'CodecSettings'; - $EBMLidList[EBML_ID_COLOURSPACE] = 'ColourSpace'; - $EBMLidList[EBML_ID_CONTENTCOMPALGO] = 'ContentCompAlgo'; - $EBMLidList[EBML_ID_CONTENTCOMPRESSION] = 'ContentCompression'; - $EBMLidList[EBML_ID_CONTENTCOMPSETTINGS] = 'ContentCompSettings'; - $EBMLidList[EBML_ID_CONTENTENCALGO] = 'ContentEncAlgo'; - $EBMLidList[EBML_ID_CONTENTENCKEYID] = 'ContentEncKeyID'; - $EBMLidList[EBML_ID_CONTENTENCODING] = 'ContentEncoding'; - $EBMLidList[EBML_ID_CONTENTENCODINGORDER] = 'ContentEncodingOrder'; - $EBMLidList[EBML_ID_CONTENTENCODINGS] = 'ContentEncodings'; - $EBMLidList[EBML_ID_CONTENTENCODINGSCOPE] = 'ContentEncodingScope'; - $EBMLidList[EBML_ID_CONTENTENCODINGTYPE] = 'ContentEncodingType'; - $EBMLidList[EBML_ID_CONTENTENCRYPTION] = 'ContentEncryption'; - $EBMLidList[EBML_ID_CONTENTSIGALGO] = 'ContentSigAlgo'; - $EBMLidList[EBML_ID_CONTENTSIGHASHALGO] = 'ContentSigHashAlgo'; - $EBMLidList[EBML_ID_CONTENTSIGKEYID] = 'ContentSigKeyID'; - $EBMLidList[EBML_ID_CONTENTSIGNATURE] = 'ContentSignature'; - $EBMLidList[EBML_ID_CRC32] = 'CRC32'; - $EBMLidList[EBML_ID_CUEBLOCKNUMBER] = 'CueBlockNumber'; - $EBMLidList[EBML_ID_CUECLUSTERPOSITION] = 'CueClusterPosition'; - $EBMLidList[EBML_ID_CUECODECSTATE] = 'CueCodecState'; - $EBMLidList[EBML_ID_CUEPOINT] = 'CuePoint'; - $EBMLidList[EBML_ID_CUEREFCLUSTER] = 'CueRefCluster'; - $EBMLidList[EBML_ID_CUEREFCODECSTATE] = 'CueRefCodecState'; - $EBMLidList[EBML_ID_CUEREFERENCE] = 'CueReference'; - $EBMLidList[EBML_ID_CUEREFNUMBER] = 'CueRefNumber'; - $EBMLidList[EBML_ID_CUEREFTIME] = 'CueRefTime'; - $EBMLidList[EBML_ID_CUES] = 'Cues'; - $EBMLidList[EBML_ID_CUETIME] = 'CueTime'; - $EBMLidList[EBML_ID_CUETRACK] = 'CueTrack'; - $EBMLidList[EBML_ID_CUETRACKPOSITIONS] = 'CueTrackPositions'; - $EBMLidList[EBML_ID_DATEUTC] = 'DateUTC'; - $EBMLidList[EBML_ID_DEFAULTDURATION] = 'DefaultDuration'; - $EBMLidList[EBML_ID_DISPLAYHEIGHT] = 'DisplayHeight'; - $EBMLidList[EBML_ID_DISPLAYUNIT] = 'DisplayUnit'; - $EBMLidList[EBML_ID_DISPLAYWIDTH] = 'DisplayWidth'; - $EBMLidList[EBML_ID_DOCTYPE] = 'DocType'; - $EBMLidList[EBML_ID_DOCTYPEREADVERSION] = 'DocTypeReadVersion'; - $EBMLidList[EBML_ID_DOCTYPEVERSION] = 'DocTypeVersion'; - $EBMLidList[EBML_ID_DURATION] = 'Duration'; - $EBMLidList[EBML_ID_EBML] = 'EBML'; - $EBMLidList[EBML_ID_EBMLMAXIDLENGTH] = 'EBMLMaxIDLength'; - $EBMLidList[EBML_ID_EBMLMAXSIZELENGTH] = 'EBMLMaxSizeLength'; - $EBMLidList[EBML_ID_EBMLREADVERSION] = 'EBMLReadVersion'; - $EBMLidList[EBML_ID_EBMLVERSION] = 'EBMLVersion'; - $EBMLidList[EBML_ID_EDITIONENTRY] = 'EditionEntry'; - $EBMLidList[EBML_ID_EDITIONFLAGDEFAULT] = 'EditionFlagDefault'; - $EBMLidList[EBML_ID_EDITIONFLAGHIDDEN] = 'EditionFlagHidden'; - $EBMLidList[EBML_ID_EDITIONFLAGORDERED] = 'EditionFlagOrdered'; - $EBMLidList[EBML_ID_EDITIONUID] = 'EditionUID'; - $EBMLidList[EBML_ID_FILEDATA] = 'FileData'; - $EBMLidList[EBML_ID_FILEDESCRIPTION] = 'FileDescription'; - $EBMLidList[EBML_ID_FILEMIMETYPE] = 'FileMimeType'; - $EBMLidList[EBML_ID_FILENAME] = 'FileName'; - $EBMLidList[EBML_ID_FILEREFERRAL] = 'FileReferral'; - $EBMLidList[EBML_ID_FILEUID] = 'FileUID'; - $EBMLidList[EBML_ID_FLAGDEFAULT] = 'FlagDefault'; - $EBMLidList[EBML_ID_FLAGENABLED] = 'FlagEnabled'; - $EBMLidList[EBML_ID_FLAGFORCED] = 'FlagForced'; - $EBMLidList[EBML_ID_FLAGINTERLACED] = 'FlagInterlaced'; - $EBMLidList[EBML_ID_FLAGLACING] = 'FlagLacing'; - $EBMLidList[EBML_ID_GAMMAVALUE] = 'GammaValue'; - $EBMLidList[EBML_ID_INFO] = 'Info'; - $EBMLidList[EBML_ID_LANGUAGE] = 'Language'; - $EBMLidList[EBML_ID_MAXBLOCKADDITIONID] = 'MaxBlockAdditionID'; - $EBMLidList[EBML_ID_MAXCACHE] = 'MaxCache'; - $EBMLidList[EBML_ID_MINCACHE] = 'MinCache'; - $EBMLidList[EBML_ID_MUXINGAPP] = 'MuxingApp'; - $EBMLidList[EBML_ID_NAME] = 'Name'; - $EBMLidList[EBML_ID_NEXTFILENAME] = 'NextFilename'; - $EBMLidList[EBML_ID_NEXTUID] = 'NextUID'; - $EBMLidList[EBML_ID_OUTPUTSAMPLINGFREQUENCY] = 'OutputSamplingFrequency'; - $EBMLidList[EBML_ID_PIXELCROPBOTTOM] = 'PixelCropBottom'; - $EBMLidList[EBML_ID_PIXELCROPLEFT] = 'PixelCropLeft'; - $EBMLidList[EBML_ID_PIXELCROPRIGHT] = 'PixelCropRight'; - $EBMLidList[EBML_ID_PIXELCROPTOP] = 'PixelCropTop'; - $EBMLidList[EBML_ID_PIXELHEIGHT] = 'PixelHeight'; - $EBMLidList[EBML_ID_PIXELWIDTH] = 'PixelWidth'; - $EBMLidList[EBML_ID_PREVFILENAME] = 'PrevFilename'; - $EBMLidList[EBML_ID_PREVUID] = 'PrevUID'; - $EBMLidList[EBML_ID_SAMPLINGFREQUENCY] = 'SamplingFrequency'; - $EBMLidList[EBML_ID_SEEK] = 'Seek'; - $EBMLidList[EBML_ID_SEEKHEAD] = 'SeekHead'; - $EBMLidList[EBML_ID_SEEKID] = 'SeekID'; - $EBMLidList[EBML_ID_SEEKPOSITION] = 'SeekPosition'; - $EBMLidList[EBML_ID_SEGMENT] = 'Segment'; - $EBMLidList[EBML_ID_SEGMENTFAMILY] = 'SegmentFamily'; - $EBMLidList[EBML_ID_SEGMENTFILENAME] = 'SegmentFilename'; - $EBMLidList[EBML_ID_SEGMENTUID] = 'SegmentUID'; - $EBMLidList[EBML_ID_SIMPLETAG] = 'SimpleTag'; - $EBMLidList[EBML_ID_CLUSTERSLICES] = 'ClusterSlices'; - $EBMLidList[EBML_ID_STEREOMODE] = 'StereoMode'; - $EBMLidList[EBML_ID_OLDSTEREOMODE] = 'OldStereoMode'; - $EBMLidList[EBML_ID_TAG] = 'Tag'; - $EBMLidList[EBML_ID_TAGATTACHMENTUID] = 'TagAttachmentUID'; - $EBMLidList[EBML_ID_TAGBINARY] = 'TagBinary'; - $EBMLidList[EBML_ID_TAGCHAPTERUID] = 'TagChapterUID'; - $EBMLidList[EBML_ID_TAGDEFAULT] = 'TagDefault'; - $EBMLidList[EBML_ID_TAGEDITIONUID] = 'TagEditionUID'; - $EBMLidList[EBML_ID_TAGLANGUAGE] = 'TagLanguage'; - $EBMLidList[EBML_ID_TAGNAME] = 'TagName'; - $EBMLidList[EBML_ID_TAGTRACKUID] = 'TagTrackUID'; - $EBMLidList[EBML_ID_TAGS] = 'Tags'; - $EBMLidList[EBML_ID_TAGSTRING] = 'TagString'; - $EBMLidList[EBML_ID_TARGETS] = 'Targets'; - $EBMLidList[EBML_ID_TARGETTYPE] = 'TargetType'; - $EBMLidList[EBML_ID_TARGETTYPEVALUE] = 'TargetTypeValue'; - $EBMLidList[EBML_ID_TIMECODESCALE] = 'TimecodeScale'; - $EBMLidList[EBML_ID_TITLE] = 'Title'; - $EBMLidList[EBML_ID_TRACKENTRY] = 'TrackEntry'; - $EBMLidList[EBML_ID_TRACKNUMBER] = 'TrackNumber'; - $EBMLidList[EBML_ID_TRACKOFFSET] = 'TrackOffset'; - $EBMLidList[EBML_ID_TRACKOVERLAY] = 'TrackOverlay'; - $EBMLidList[EBML_ID_TRACKS] = 'Tracks'; - $EBMLidList[EBML_ID_TRACKTIMECODESCALE] = 'TrackTimecodeScale'; - $EBMLidList[EBML_ID_TRACKTRANSLATE] = 'TrackTranslate'; - $EBMLidList[EBML_ID_TRACKTRANSLATECODEC] = 'TrackTranslateCodec'; - $EBMLidList[EBML_ID_TRACKTRANSLATEEDITIONUID] = 'TrackTranslateEditionUID'; - $EBMLidList[EBML_ID_TRACKTRANSLATETRACKID] = 'TrackTranslateTrackID'; - $EBMLidList[EBML_ID_TRACKTYPE] = 'TrackType'; - $EBMLidList[EBML_ID_TRACKUID] = 'TrackUID'; - $EBMLidList[EBML_ID_VIDEO] = 'Video'; - $EBMLidList[EBML_ID_VOID] = 'Void'; - $EBMLidList[EBML_ID_WRITINGAPP] = 'WritingApp'; - } - - return (isset($EBMLidList[$value]) ? $EBMLidList[$value] : dechex($value)); - } - - public static function displayUnit($value) { - // http://www.matroska.org/technical/specs/index.html#DisplayUnit - static $units = array( - 0 => 'pixels', - 1 => 'centimeters', - 2 => 'inches', - 3 => 'Display Aspect Ratio'); - - return (isset($units[$value]) ? $units[$value] : 'unknown'); - } - - private static function getDefaultStreamInfo($streams) - { - foreach (array_reverse($streams) as $stream) { - if ($stream['default']) { - break; - } - } - - $unset = array('default', 'name'); - foreach ($unset as $u) { - if (isset($stream[$u])) { - unset($stream[$u]); - } - } - - $info = $stream; - $info['streams'] = $streams; - - return $info; - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio-video.mpeg.php b/src/Classes/Vendor/getid3/module.audio-video.mpeg.php deleted file mode 100755 index 999c5d9a5..000000000 --- a/src/Classes/Vendor/getid3/module.audio-video.mpeg.php +++ /dev/null @@ -1,296 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio-video.mpeg.php // -// module for analyzing MPEG files // -// dependencies: module.audio.mp3.php // -// /// -///////////////////////////////////////////////////////////////// - -getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.mp3.php', __FILE__, true); - -define('GETID3_MPEG_VIDEO_PICTURE_START', "\x00\x00\x01\x00"); -define('GETID3_MPEG_VIDEO_USER_DATA_START', "\x00\x00\x01\xB2"); -define('GETID3_MPEG_VIDEO_SEQUENCE_HEADER', "\x00\x00\x01\xB3"); -define('GETID3_MPEG_VIDEO_SEQUENCE_ERROR', "\x00\x00\x01\xB4"); -define('GETID3_MPEG_VIDEO_EXTENSION_START', "\x00\x00\x01\xB5"); -define('GETID3_MPEG_VIDEO_SEQUENCE_END', "\x00\x00\x01\xB7"); -define('GETID3_MPEG_VIDEO_GROUP_START', "\x00\x00\x01\xB8"); -define('GETID3_MPEG_AUDIO_START', "\x00\x00\x01\xC0"); - - -class getid3_mpeg extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - if ($info['avdataend'] <= $info['avdataoffset']) { - $info['error'][] = '"avdataend" ('.$info['avdataend'].') is unexpectedly less-than-or-equal-to "avdataoffset" ('.$info['avdataoffset'].')'; - return false; - } - $info['fileformat'] = 'mpeg'; - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $MPEGstreamData = fread($this->getid3->fp, min(100000, $info['avdataend'] - $info['avdataoffset'])); - $MPEGstreamDataLength = strlen($MPEGstreamData); - - $foundVideo = true; - $VideoChunkOffset = 0; - while (substr($MPEGstreamData, $VideoChunkOffset++, 4) !== GETID3_MPEG_VIDEO_SEQUENCE_HEADER) { - if ($VideoChunkOffset >= $MPEGstreamDataLength) { - $foundVideo = false; - break; - } - } - if ($foundVideo) { - - // Start code 32 bits - // horizontal frame size 12 bits - // vertical frame size 12 bits - // pixel aspect ratio 4 bits - // frame rate 4 bits - // bitrate 18 bits - // marker bit 1 bit - // VBV buffer size 10 bits - // constrained parameter flag 1 bit - // intra quant. matrix flag 1 bit - // intra quant. matrix values 512 bits (present if matrix flag == 1) - // non-intra quant. matrix flag 1 bit - // non-intra quant. matrix values 512 bits (present if matrix flag == 1) - - $info['video']['dataformat'] = 'mpeg'; - - $VideoChunkOffset += (strlen(GETID3_MPEG_VIDEO_SEQUENCE_HEADER) - 1); - - $FrameSizeDWORD = getid3_lib::BigEndian2Int(substr($MPEGstreamData, $VideoChunkOffset, 3)); - $VideoChunkOffset += 3; - - $AspectRatioFrameRateDWORD = getid3_lib::BigEndian2Int(substr($MPEGstreamData, $VideoChunkOffset, 1)); - $VideoChunkOffset += 1; - - $assortedinformation = getid3_lib::BigEndian2Bin(substr($MPEGstreamData, $VideoChunkOffset, 4)); - $VideoChunkOffset += 4; - - $info['mpeg']['video']['raw']['framesize_horizontal'] = ($FrameSizeDWORD & 0xFFF000) >> 12; // 12 bits for horizontal frame size - $info['mpeg']['video']['raw']['framesize_vertical'] = ($FrameSizeDWORD & 0x000FFF); // 12 bits for vertical frame size - $info['mpeg']['video']['raw']['pixel_aspect_ratio'] = ($AspectRatioFrameRateDWORD & 0xF0) >> 4; - $info['mpeg']['video']['raw']['frame_rate'] = ($AspectRatioFrameRateDWORD & 0x0F); - - $info['mpeg']['video']['framesize_horizontal'] = $info['mpeg']['video']['raw']['framesize_horizontal']; - $info['mpeg']['video']['framesize_vertical'] = $info['mpeg']['video']['raw']['framesize_vertical']; - - $info['mpeg']['video']['pixel_aspect_ratio'] = $this->MPEGvideoAspectRatioLookup($info['mpeg']['video']['raw']['pixel_aspect_ratio']); - $info['mpeg']['video']['pixel_aspect_ratio_text'] = $this->MPEGvideoAspectRatioTextLookup($info['mpeg']['video']['raw']['pixel_aspect_ratio']); - $info['mpeg']['video']['frame_rate'] = $this->MPEGvideoFramerateLookup($info['mpeg']['video']['raw']['frame_rate']); - - $info['mpeg']['video']['raw']['bitrate'] = getid3_lib::Bin2Dec(substr($assortedinformation, 0, 18)); - $info['mpeg']['video']['raw']['marker_bit'] = (bool) getid3_lib::Bin2Dec(substr($assortedinformation, 18, 1)); - $info['mpeg']['video']['raw']['vbv_buffer_size'] = getid3_lib::Bin2Dec(substr($assortedinformation, 19, 10)); - $info['mpeg']['video']['raw']['constrained_param_flag'] = (bool) getid3_lib::Bin2Dec(substr($assortedinformation, 29, 1)); - $info['mpeg']['video']['raw']['intra_quant_flag'] = (bool) getid3_lib::Bin2Dec(substr($assortedinformation, 30, 1)); - if ($info['mpeg']['video']['raw']['intra_quant_flag']) { - - // read 512 bits - $info['mpeg']['video']['raw']['intra_quant'] = getid3_lib::BigEndian2Bin(substr($MPEGstreamData, $VideoChunkOffset, 64)); - $VideoChunkOffset += 64; - - $info['mpeg']['video']['raw']['non_intra_quant_flag'] = (bool) getid3_lib::Bin2Dec(substr($info['mpeg']['video']['raw']['intra_quant'], 511, 1)); - $info['mpeg']['video']['raw']['intra_quant'] = getid3_lib::Bin2Dec(substr($assortedinformation, 31, 1)).substr(getid3_lib::BigEndian2Bin(substr($MPEGstreamData, $VideoChunkOffset, 64)), 0, 511); - - if ($info['mpeg']['video']['raw']['non_intra_quant_flag']) { - $info['mpeg']['video']['raw']['non_intra_quant'] = substr($MPEGstreamData, $VideoChunkOffset, 64); - $VideoChunkOffset += 64; - } - - } else { - - $info['mpeg']['video']['raw']['non_intra_quant_flag'] = (bool) getid3_lib::Bin2Dec(substr($assortedinformation, 31, 1)); - if ($info['mpeg']['video']['raw']['non_intra_quant_flag']) { - $info['mpeg']['video']['raw']['non_intra_quant'] = substr($MPEGstreamData, $VideoChunkOffset, 64); - $VideoChunkOffset += 64; - } - - } - - if ($info['mpeg']['video']['raw']['bitrate'] == 0x3FFFF) { // 18 set bits - - $info['warning'][] = 'This version of getID3() ['.$this->getid3->version().'] cannot determine average bitrate of VBR MPEG video files'; - $info['mpeg']['video']['bitrate_mode'] = 'vbr'; - - } else { - - $info['mpeg']['video']['bitrate'] = $info['mpeg']['video']['raw']['bitrate'] * 400; - $info['mpeg']['video']['bitrate_mode'] = 'cbr'; - $info['video']['bitrate'] = $info['mpeg']['video']['bitrate']; - - } - - $info['video']['resolution_x'] = $info['mpeg']['video']['framesize_horizontal']; - $info['video']['resolution_y'] = $info['mpeg']['video']['framesize_vertical']; - $info['video']['frame_rate'] = $info['mpeg']['video']['frame_rate']; - $info['video']['bitrate_mode'] = $info['mpeg']['video']['bitrate_mode']; - $info['video']['pixel_aspect_ratio'] = $info['mpeg']['video']['pixel_aspect_ratio']; - $info['video']['lossless'] = false; - $info['video']['bits_per_sample'] = 24; - - } else { - - $info['error'][] = 'Could not find start of video block in the first 100,000 bytes (or before end of file) - this might not be an MPEG-video file?'; - - } - - //0x000001B3 begins the sequence_header of every MPEG video stream. - //But in MPEG-2, this header must immediately be followed by an - //extension_start_code (0x000001B5) with a sequence_extension ID (1). - //(This extension contains all the additional MPEG-2 stuff.) - //MPEG-1 doesn't have this extension, so that's a sure way to tell the - //difference between MPEG-1 and MPEG-2 video streams. - - if (substr($MPEGstreamData, $VideoChunkOffset, 4) == GETID3_MPEG_VIDEO_EXTENSION_START) { - $info['video']['codec'] = 'MPEG-2'; - } else { - $info['video']['codec'] = 'MPEG-1'; - } - - - $AudioChunkOffset = 0; - while (true) { - while (substr($MPEGstreamData, $AudioChunkOffset++, 4) !== GETID3_MPEG_AUDIO_START) { - if ($AudioChunkOffset >= $MPEGstreamDataLength) { - break 2; - } - } - - $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); - $getid3_temp->info = $info; - $getid3_mp3 = new getid3_mp3($getid3_temp); - for ($i = 0; $i <= 7; $i++) { - // some files have the MPEG-audio header 8 bytes after the end of the $00 $00 $01 $C0 signature, some have it up to 13 bytes (or more?) after - // I have no idea why or what the difference is, so this is a stupid hack. - // If anybody has any better idea of what's going on, please let me know - info@getid3.org - fseek($getid3_temp->fp, ftell($this->getid3->fp), SEEK_SET); - $getid3_temp->info = $info; // only overwrite real data if valid header found - if ($getid3_mp3->decodeMPEGaudioHeader(($AudioChunkOffset + 3) + 8 + $i, $getid3_temp->info, false)) { - $info = $getid3_temp->info; - $info['audio']['bitrate_mode'] = 'cbr'; - $info['audio']['lossless'] = false; - unset($getid3_temp, $getid3_mp3); - break 2; - } - } - unset($getid3_temp, $getid3_mp3); - } - - // Temporary hack to account for interleaving overhead: - if (!empty($info['video']['bitrate']) && !empty($info['audio']['bitrate'])) { - $info['playtime_seconds'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($info['video']['bitrate'] + $info['audio']['bitrate']); - - // Interleaved MPEG audio/video files have a certain amount of overhead that varies - // by both video and audio bitrates, and not in any sensible, linear/logarithmic patter - // Use interpolated lookup tables to approximately guess how much is overhead, because - // playtime is calculated as filesize / total-bitrate - $info['playtime_seconds'] *= $this->MPEGsystemNonOverheadPercentage($info['video']['bitrate'], $info['audio']['bitrate']); - - //switch ($info['video']['bitrate']) { - // case('5000000'): - // $multiplier = 0.93292642112380355828048824319889; - // break; - // case('5500000'): - // $multiplier = 0.93582895375200989965359777343219; - // break; - // case('6000000'): - // $multiplier = 0.93796247714820932532911373859139; - // break; - // case('7000000'): - // $multiplier = 0.9413264083635103463010117778776; - // break; - // default: - // $multiplier = 1; - // break; - //} - //$info['playtime_seconds'] *= $multiplier; - //$info['warning'][] = 'Interleaved MPEG audio/video playtime may be inaccurate. With current hack should be within a few seconds of accurate. Report to info@getid3.org if off by more than 10 seconds.'; - if ($info['video']['bitrate'] < 50000) { - $info['warning'][] = 'Interleaved MPEG audio/video playtime may be slightly inaccurate for video bitrates below 100kbps. Except in extreme low-bitrate situations, error should be less than 1%. Report to info@getid3.org if greater than this.'; - } - } - - return true; - } - - - public function MPEGsystemNonOverheadPercentage($VideoBitrate, $AudioBitrate) { - $OverheadPercentage = 0; - - $AudioBitrate = max(min($AudioBitrate / 1000, 384), 32); // limit to range of 32kbps - 384kbps (should be only legal bitrates, but maybe VBR?) - $VideoBitrate = max(min($VideoBitrate / 1000, 10000), 10); // limit to range of 10kbps - 10Mbps (beyond that curves flatten anyways, no big loss) - - - //OMBB[audiobitrate] = array(video-10kbps, video-100kbps, video-1000kbps, video-10000kbps) - $OverheadMultiplierByBitrate[32] = array(0, 0.9676287944368530, 0.9802276264360310, 0.9844916183244460, 0.9852821845179940); - $OverheadMultiplierByBitrate[48] = array(0, 0.9779100089209830, 0.9787770035359320, 0.9846738664076130, 0.9852683013799960); - $OverheadMultiplierByBitrate[56] = array(0, 0.9731249855367600, 0.9776624308938040, 0.9832606361852130, 0.9843922606633340); - $OverheadMultiplierByBitrate[64] = array(0, 0.9755642683275760, 0.9795256705493390, 0.9836573009193170, 0.9851122539404470); - $OverheadMultiplierByBitrate[96] = array(0, 0.9788025247497290, 0.9798553314148700, 0.9822956869792560, 0.9834815119124690); - $OverheadMultiplierByBitrate[128] = array(0, 0.9816940050925480, 0.9821675936072120, 0.9829756927470870, 0.9839763420152050); - $OverheadMultiplierByBitrate[160] = array(0, 0.9825894094561180, 0.9820913399073960, 0.9823907143253970, 0.9832821783651570); - $OverheadMultiplierByBitrate[192] = array(0, 0.9832038474336260, 0.9825731694317960, 0.9821028622712400, 0.9828262076447620); - $OverheadMultiplierByBitrate[224] = array(0, 0.9836516298538770, 0.9824718601823890, 0.9818302180625380, 0.9823735101626480); - $OverheadMultiplierByBitrate[256] = array(0, 0.9845863022094920, 0.9837229411967540, 0.9824521662210830, 0.9828645172100790); - $OverheadMultiplierByBitrate[320] = array(0, 0.9849565280263180, 0.9837683142805110, 0.9822885275960400, 0.9824424382727190); - $OverheadMultiplierByBitrate[384] = array(0, 0.9856094774357600, 0.9844573394432720, 0.9825970399837330, 0.9824673808303890); - - $BitrateToUseMin = 32; - $BitrateToUseMax = 32; - $previousBitrate = 32; - foreach ($OverheadMultiplierByBitrate as $key => $value) { - if ($AudioBitrate >= $previousBitrate) { - $BitrateToUseMin = $previousBitrate; - } - if ($AudioBitrate < $key) { - $BitrateToUseMax = $key; - break; - } - $previousBitrate = $key; - } - $FactorA = ($BitrateToUseMax - $AudioBitrate) / ($BitrateToUseMax - $BitrateToUseMin); - - $VideoBitrateLog10 = log10($VideoBitrate); - $VideoFactorMin1 = $OverheadMultiplierByBitrate[$BitrateToUseMin][floor($VideoBitrateLog10)]; - $VideoFactorMin2 = $OverheadMultiplierByBitrate[$BitrateToUseMax][floor($VideoBitrateLog10)]; - $VideoFactorMax1 = $OverheadMultiplierByBitrate[$BitrateToUseMin][ceil($VideoBitrateLog10)]; - $VideoFactorMax2 = $OverheadMultiplierByBitrate[$BitrateToUseMax][ceil($VideoBitrateLog10)]; - $FactorV = $VideoBitrateLog10 - floor($VideoBitrateLog10); - - $OverheadPercentage = $VideoFactorMin1 * $FactorA * $FactorV; - $OverheadPercentage += $VideoFactorMin2 * (1 - $FactorA) * $FactorV; - $OverheadPercentage += $VideoFactorMax1 * $FactorA * (1 - $FactorV); - $OverheadPercentage += $VideoFactorMax2 * (1 - $FactorA) * (1 - $FactorV); - - return $OverheadPercentage; - } - - - public function MPEGvideoFramerateLookup($rawframerate) { - $MPEGvideoFramerateLookup = array(0, 23.976, 24, 25, 29.97, 30, 50, 59.94, 60); - return (isset($MPEGvideoFramerateLookup[$rawframerate]) ? (float) $MPEGvideoFramerateLookup[$rawframerate] : (float) 0); - } - - public function MPEGvideoAspectRatioLookup($rawaspectratio) { - $MPEGvideoAspectRatioLookup = array(0, 1, 0.6735, 0.7031, 0.7615, 0.8055, 0.8437, 0.8935, 0.9157, 0.9815, 1.0255, 1.0695, 1.0950, 1.1575, 1.2015, 0); - return (isset($MPEGvideoAspectRatioLookup[$rawaspectratio]) ? (float) $MPEGvideoAspectRatioLookup[$rawaspectratio] : (float) 0); - } - - public function MPEGvideoAspectRatioTextLookup($rawaspectratio) { - $MPEGvideoAspectRatioTextLookup = array('forbidden', 'square pixels', '0.6735', '16:9, 625 line, PAL', '0.7615', '0.8055', '16:9, 525 line, NTSC', '0.8935', '4:3, 625 line, PAL, CCIR601', '0.9815', '1.0255', '1.0695', '4:3, 525 line, NTSC, CCIR601', '1.1575', '1.2015', 'reserved'); - return (isset($MPEGvideoAspectRatioTextLookup[$rawaspectratio]) ? $MPEGvideoAspectRatioTextLookup[$rawaspectratio] : ''); - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio-video.nsv.php b/src/Classes/Vendor/getid3/module.audio-video.nsv.php deleted file mode 100755 index 3191eb3b2..000000000 --- a/src/Classes/Vendor/getid3/module.audio-video.nsv.php +++ /dev/null @@ -1,223 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.nsv.php // -// module for analyzing Nullsoft NSV files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_nsv extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $NSVheader = fread($this->getid3->fp, 4); - - switch ($NSVheader) { - case 'NSVs': - if ($this->getNSVsHeaderFilepointer(0)) { - $info['fileformat'] = 'nsv'; - $info['audio']['dataformat'] = 'nsv'; - $info['video']['dataformat'] = 'nsv'; - $info['audio']['lossless'] = false; - $info['video']['lossless'] = false; - } - break; - - case 'NSVf': - if ($this->getNSVfHeaderFilepointer(0)) { - $info['fileformat'] = 'nsv'; - $info['audio']['dataformat'] = 'nsv'; - $info['video']['dataformat'] = 'nsv'; - $info['audio']['lossless'] = false; - $info['video']['lossless'] = false; - $this->getNSVsHeaderFilepointer($info['nsv']['NSVf']['header_length']); - } - break; - - default: - $info['error'][] = 'Expecting "NSVs" or "NSVf" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($NSVheader).'"'; - return false; - break; - } - - if (!isset($info['nsv']['NSVf'])) { - $info['warning'][] = 'NSVf header not present - cannot calculate playtime or bitrate'; - } - - return true; - } - - public function getNSVsHeaderFilepointer($fileoffset) { - $info = &$this->getid3->info; - fseek($this->getid3->fp, $fileoffset, SEEK_SET); - $NSVsheader = fread($this->getid3->fp, 28); - $offset = 0; - - $info['nsv']['NSVs']['identifier'] = substr($NSVsheader, $offset, 4); - $offset += 4; - - if ($info['nsv']['NSVs']['identifier'] != 'NSVs') { - $info['error'][] = 'expected "NSVs" at offset ('.$fileoffset.'), found "'.$info['nsv']['NSVs']['identifier'].'" instead'; - unset($info['nsv']['NSVs']); - return false; - } - - $info['nsv']['NSVs']['offset'] = $fileoffset; - - $info['nsv']['NSVs']['video_codec'] = substr($NSVsheader, $offset, 4); - $offset += 4; - $info['nsv']['NSVs']['audio_codec'] = substr($NSVsheader, $offset, 4); - $offset += 4; - $info['nsv']['NSVs']['resolution_x'] = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 2)); - $offset += 2; - $info['nsv']['NSVs']['resolution_y'] = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 2)); - $offset += 2; - - $info['nsv']['NSVs']['framerate_index'] = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1)); - $offset += 1; - //$info['nsv']['NSVs']['unknown1b'] = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1)); - $offset += 1; - //$info['nsv']['NSVs']['unknown1c'] = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1)); - $offset += 1; - //$info['nsv']['NSVs']['unknown1d'] = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1)); - $offset += 1; - //$info['nsv']['NSVs']['unknown2a'] = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1)); - $offset += 1; - //$info['nsv']['NSVs']['unknown2b'] = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1)); - $offset += 1; - //$info['nsv']['NSVs']['unknown2c'] = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1)); - $offset += 1; - //$info['nsv']['NSVs']['unknown2d'] = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1)); - $offset += 1; - - switch ($info['nsv']['NSVs']['audio_codec']) { - case 'PCM ': - $info['nsv']['NSVs']['bits_channel'] = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1)); - $offset += 1; - $info['nsv']['NSVs']['channels'] = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 1)); - $offset += 1; - $info['nsv']['NSVs']['sample_rate'] = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 2)); - $offset += 2; - - $info['audio']['sample_rate'] = $info['nsv']['NSVs']['sample_rate']; - break; - - case 'MP3 ': - case 'NONE': - default: - //$info['nsv']['NSVs']['unknown3'] = getid3_lib::LittleEndian2Int(substr($NSVsheader, $offset, 4)); - $offset += 4; - break; - } - - $info['video']['resolution_x'] = $info['nsv']['NSVs']['resolution_x']; - $info['video']['resolution_y'] = $info['nsv']['NSVs']['resolution_y']; - $info['nsv']['NSVs']['frame_rate'] = $this->NSVframerateLookup($info['nsv']['NSVs']['framerate_index']); - $info['video']['frame_rate'] = $info['nsv']['NSVs']['frame_rate']; - $info['video']['bits_per_sample'] = 24; - $info['video']['pixel_aspect_ratio'] = (float) 1; - - return true; - } - - public function getNSVfHeaderFilepointer($fileoffset, $getTOCoffsets=false) { - $info = &$this->getid3->info; - fseek($this->getid3->fp, $fileoffset, SEEK_SET); - $NSVfheader = fread($this->getid3->fp, 28); - $offset = 0; - - $info['nsv']['NSVf']['identifier'] = substr($NSVfheader, $offset, 4); - $offset += 4; - - if ($info['nsv']['NSVf']['identifier'] != 'NSVf') { - $info['error'][] = 'expected "NSVf" at offset ('.$fileoffset.'), found "'.$info['nsv']['NSVf']['identifier'].'" instead'; - unset($info['nsv']['NSVf']); - return false; - } - - $info['nsv']['NSVs']['offset'] = $fileoffset; - - $info['nsv']['NSVf']['header_length'] = getid3_lib::LittleEndian2Int(substr($NSVfheader, $offset, 4)); - $offset += 4; - $info['nsv']['NSVf']['file_size'] = getid3_lib::LittleEndian2Int(substr($NSVfheader, $offset, 4)); - $offset += 4; - - if ($info['nsv']['NSVf']['file_size'] > $info['avdataend']) { - $info['warning'][] = 'truncated file - NSVf header indicates '.$info['nsv']['NSVf']['file_size'].' bytes, file actually '.$info['avdataend'].' bytes'; - } - - $info['nsv']['NSVf']['playtime_ms'] = getid3_lib::LittleEndian2Int(substr($NSVfheader, $offset, 4)); - $offset += 4; - $info['nsv']['NSVf']['meta_size'] = getid3_lib::LittleEndian2Int(substr($NSVfheader, $offset, 4)); - $offset += 4; - $info['nsv']['NSVf']['TOC_entries_1'] = getid3_lib::LittleEndian2Int(substr($NSVfheader, $offset, 4)); - $offset += 4; - $info['nsv']['NSVf']['TOC_entries_2'] = getid3_lib::LittleEndian2Int(substr($NSVfheader, $offset, 4)); - $offset += 4; - - if ($info['nsv']['NSVf']['playtime_ms'] == 0) { - $info['error'][] = 'Corrupt NSV file: NSVf.playtime_ms == zero'; - return false; - } - - $NSVfheader .= fread($this->getid3->fp, $info['nsv']['NSVf']['meta_size'] + (4 * $info['nsv']['NSVf']['TOC_entries_1']) + (4 * $info['nsv']['NSVf']['TOC_entries_2'])); - $NSVfheaderlength = strlen($NSVfheader); - $info['nsv']['NSVf']['metadata'] = substr($NSVfheader, $offset, $info['nsv']['NSVf']['meta_size']); - $offset += $info['nsv']['NSVf']['meta_size']; - - if ($getTOCoffsets) { - $TOCcounter = 0; - while ($TOCcounter < $info['nsv']['NSVf']['TOC_entries_1']) { - if ($TOCcounter < $info['nsv']['NSVf']['TOC_entries_1']) { - $info['nsv']['NSVf']['TOC_1'][$TOCcounter] = getid3_lib::LittleEndian2Int(substr($NSVfheader, $offset, 4)); - $offset += 4; - $TOCcounter++; - } - } - } - - if (trim($info['nsv']['NSVf']['metadata']) != '') { - $info['nsv']['NSVf']['metadata'] = str_replace('`', "\x01", $info['nsv']['NSVf']['metadata']); - $CommentPairArray = explode("\x01".' ', $info['nsv']['NSVf']['metadata']); - foreach ($CommentPairArray as $CommentPair) { - if (strstr($CommentPair, '='."\x01")) { - list($key, $value) = explode('='."\x01", $CommentPair, 2); - $info['nsv']['comments'][strtolower($key)][] = trim(str_replace("\x01", '', $value)); - } - } - } - - $info['playtime_seconds'] = $info['nsv']['NSVf']['playtime_ms'] / 1000; - $info['bitrate'] = ($info['nsv']['NSVf']['file_size'] * 8) / $info['playtime_seconds']; - - return true; - } - - - public static function NSVframerateLookup($framerateindex) { - if ($framerateindex <= 127) { - return (float) $framerateindex; - } - static $NSVframerateLookup = array(); - if (empty($NSVframerateLookup)) { - $NSVframerateLookup[129] = (float) 29.970; - $NSVframerateLookup[131] = (float) 23.976; - $NSVframerateLookup[133] = (float) 14.985; - $NSVframerateLookup[197] = (float) 59.940; - $NSVframerateLookup[199] = (float) 47.952; - } - return (isset($NSVframerateLookup[$framerateindex]) ? $NSVframerateLookup[$framerateindex] : false); - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio-video.quicktime.php b/src/Classes/Vendor/getid3/module.audio-video.quicktime.php deleted file mode 100755 index ce0d31f2f..000000000 --- a/src/Classes/Vendor/getid3/module.audio-video.quicktime.php +++ /dev/null @@ -1,2145 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio-video.quicktime.php // -// module for analyzing Quicktime and MP3-in-MP4 files // -// dependencies: module.audio.mp3.php // -// /// -///////////////////////////////////////////////////////////////// - -getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.mp3.php', __FILE__, true); - -class getid3_quicktime extends getid3_handler -{ - - public $ReturnAtomData = true; - public $ParseAllPossibleAtoms = false; - - public function Analyze() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'quicktime'; - $info['quicktime']['hinting'] = false; - $info['quicktime']['controller'] = 'standard'; // may be overridden if 'ctyp' atom is present - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - - $offset = 0; - $atomcounter = 0; - - while ($offset < $info['avdataend']) { - if (!getid3_lib::intValueSupported($offset)) { - $info['error'][] = 'Unable to parse atom at offset '.$offset.' because beyond '.round(PHP_INT_MAX / 1073741824).'GB limit of PHP filesystem functions'; - break; - } - fseek($this->getid3->fp, $offset, SEEK_SET); - $AtomHeader = fread($this->getid3->fp, 8); - - $atomsize = getid3_lib::BigEndian2Int(substr($AtomHeader, 0, 4)); - $atomname = substr($AtomHeader, 4, 4); - - // 64-bit MOV patch by jlegateØktnc*com - if ($atomsize == 1) { - $atomsize = getid3_lib::BigEndian2Int(fread($this->getid3->fp, 8)); - } - - $info['quicktime'][$atomname]['name'] = $atomname; - $info['quicktime'][$atomname]['size'] = $atomsize; - $info['quicktime'][$atomname]['offset'] = $offset; - - if (($offset + $atomsize) > $info['avdataend']) { - $info['error'][] = 'Atom at offset '.$offset.' claims to go beyond end-of-file (length: '.$atomsize.' bytes)'; - return false; - } - - if ($atomsize == 0) { - // Furthermore, for historical reasons the list of atoms is optionally - // terminated by a 32-bit integer set to 0. If you are writing a program - // to read user data atoms, you should allow for the terminating 0. - break; - } - switch ($atomname) { - case 'mdat': // Media DATa atom - // 'mdat' contains the actual data for the audio/video - if (($atomsize > 8) && (!isset($info['avdataend_tmp']) || ($info['quicktime'][$atomname]['size'] > ($info['avdataend_tmp'] - $info['avdataoffset'])))) { - - $info['avdataoffset'] = $info['quicktime'][$atomname]['offset'] + 8; - $OldAVDataEnd = $info['avdataend']; - $info['avdataend'] = $info['quicktime'][$atomname]['offset'] + $info['quicktime'][$atomname]['size']; - - $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); - $getid3_temp->info['avdataoffset'] = $info['avdataoffset']; - $getid3_temp->info['avdataend'] = $info['avdataend']; - $getid3_mp3 = new getid3_mp3($getid3_temp); - if ($getid3_mp3->MPEGaudioHeaderValid($getid3_mp3->MPEGaudioHeaderDecode(fread($this->getid3->fp, 4)))) { - $getid3_mp3->getOnlyMPEGaudioInfo($getid3_temp->info['avdataoffset'], false); - if (!empty($getid3_temp->info['warning'])) { - foreach ($getid3_temp->info['warning'] as $value) { - $info['warning'][] = $value; - } - } - if (!empty($getid3_temp->info['mpeg'])) { - $info['mpeg'] = $getid3_temp->info['mpeg']; - if (isset($info['mpeg']['audio'])) { - $info['audio']['dataformat'] = 'mp3'; - $info['audio']['codec'] = (!empty($info['mpeg']['audio']['encoder']) ? $info['mpeg']['audio']['encoder'] : (!empty($info['mpeg']['audio']['codec']) ? $info['mpeg']['audio']['codec'] : (!empty($info['mpeg']['audio']['LAME']) ? 'LAME' :'mp3'))); - $info['audio']['sample_rate'] = $info['mpeg']['audio']['sample_rate']; - $info['audio']['channels'] = $info['mpeg']['audio']['channels']; - $info['audio']['bitrate'] = $info['mpeg']['audio']['bitrate']; - $info['audio']['bitrate_mode'] = strtolower($info['mpeg']['audio']['bitrate_mode']); - $info['bitrate'] = $info['audio']['bitrate']; - } - } - } - unset($getid3_mp3, $getid3_temp); - $info['avdataend'] = $OldAVDataEnd; - unset($OldAVDataEnd); - - } - break; - - case 'free': // FREE space atom - case 'skip': // SKIP atom - case 'wide': // 64-bit expansion placeholder atom - // 'free', 'skip' and 'wide' are just padding, contains no useful data at all - break; - - default: - $atomHierarchy = array(); - $info['quicktime'][$atomname] = $this->QuicktimeParseAtom($atomname, $atomsize, fread($this->getid3->fp, $atomsize), $offset, $atomHierarchy, $this->ParseAllPossibleAtoms); - break; - } - - $offset += $atomsize; - $atomcounter++; - } - - if (!empty($info['avdataend_tmp'])) { - // this value is assigned to a temp value and then erased because - // otherwise any atoms beyond the 'mdat' atom would not get parsed - $info['avdataend'] = $info['avdataend_tmp']; - unset($info['avdataend_tmp']); - } - - if (!isset($info['bitrate']) && isset($info['playtime_seconds'])) { - $info['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; - } - if (isset($info['bitrate']) && !isset($info['audio']['bitrate']) && !isset($info['quicktime']['video'])) { - $info['audio']['bitrate'] = $info['bitrate']; - } - if (!empty($info['playtime_seconds']) && !isset($info['video']['frame_rate']) && !empty($info['quicktime']['stts_framecount'])) { - foreach ($info['quicktime']['stts_framecount'] as $key => $samples_count) { - $samples_per_second = $samples_count / $info['playtime_seconds']; - if ($samples_per_second > 240) { - // has to be audio samples - } else { - $info['video']['frame_rate'] = $samples_per_second; - break; - } - } - } - if (($info['audio']['dataformat'] == 'mp4') && empty($info['video']['resolution_x'])) { - $info['fileformat'] = 'mp4'; - $info['mime_type'] = 'audio/mp4'; - unset($info['video']['dataformat']); - } - - if (!$this->ReturnAtomData) { - unset($info['quicktime']['moov']); - } - - if (empty($info['audio']['dataformat']) && !empty($info['quicktime']['audio'])) { - $info['audio']['dataformat'] = 'quicktime'; - } - if (empty($info['video']['dataformat']) && !empty($info['quicktime']['video'])) { - $info['video']['dataformat'] = 'quicktime'; - } - - return true; - } - - public function QuicktimeParseAtom($atomname, $atomsize, $atom_data, $baseoffset, &$atomHierarchy, $ParseAllPossibleAtoms) { - // http://developer.apple.com/techpubs/quicktime/qtdevdocs/APIREF/INDEX/atomalphaindex.htm - - $info = &$this->getid3->info; - - $atom_parent = array_pop($atomHierarchy); - array_push($atomHierarchy, $atomname); - $atom_structure['hierarchy'] = implode(' ', $atomHierarchy); - $atom_structure['name'] = $atomname; - $atom_structure['size'] = $atomsize; - $atom_structure['offset'] = $baseoffset; -//echo getid3_lib::PrintHexBytes(substr($atom_data, 0, 8)).'
    '; -//echo getid3_lib::PrintHexBytes(substr($atom_data, 0, 8), false).'

    '; - switch ($atomname) { - case 'moov': // MOVie container atom - case 'trak': // TRAcK container atom - case 'clip': // CLIPping container atom - case 'matt': // track MATTe container atom - case 'edts': // EDiTS container atom - case 'tref': // Track REFerence container atom - case 'mdia': // MeDIA container atom - case 'minf': // Media INFormation container atom - case 'dinf': // Data INFormation container atom - case 'udta': // User DaTA container atom - case 'cmov': // Compressed MOVie container atom - case 'rmra': // Reference Movie Record Atom - case 'rmda': // Reference Movie Descriptor Atom - case 'gmhd': // Generic Media info HeaDer atom (seen on QTVR) - $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); - break; - - case 'ilst': // Item LiST container atom - $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); - - // some "ilst" atoms contain data atoms that have a numeric name, and the data is far more accessible if the returned array is compacted - $allnumericnames = true; - foreach ($atom_structure['subatoms'] as $subatomarray) { - if (!is_integer($subatomarray['name']) || (count($subatomarray['subatoms']) != 1)) { - $allnumericnames = false; - break; - } - } - if ($allnumericnames) { - $newData = array(); - foreach ($atom_structure['subatoms'] as $subatomarray) { - foreach ($subatomarray['subatoms'] as $newData_subatomarray) { - unset($newData_subatomarray['hierarchy'], $newData_subatomarray['name']); - $newData[$subatomarray['name']] = $newData_subatomarray; - break; - } - } - $atom_structure['data'] = $newData; - unset($atom_structure['subatoms']); - } - break; - - case "\x00\x00\x00\x01": - case "\x00\x00\x00\x02": - case "\x00\x00\x00\x03": - case "\x00\x00\x00\x04": - case "\x00\x00\x00\x05": - $atomname = getid3_lib::BigEndian2Int($atomname); - $atom_structure['name'] = $atomname; - $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); - break; - - case 'stbl': // Sample TaBLe container atom - $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); - $isVideo = false; - $framerate = 0; - $framecount = 0; - foreach ($atom_structure['subatoms'] as $key => $value_array) { - if (isset($value_array['sample_description_table'])) { - foreach ($value_array['sample_description_table'] as $key2 => $value_array2) { - if (isset($value_array2['data_format'])) { - switch ($value_array2['data_format']) { - case 'avc1': - case 'mp4v': - // video data - $isVideo = true; - break; - case 'mp4a': - // audio data - break; - } - } - } - } elseif (isset($value_array['time_to_sample_table'])) { - foreach ($value_array['time_to_sample_table'] as $key2 => $value_array2) { - if (isset($value_array2['sample_count']) && isset($value_array2['sample_duration']) && ($value_array2['sample_duration'] > 0)) { - $framerate = round($info['quicktime']['time_scale'] / $value_array2['sample_duration'], 3); - $framecount = $value_array2['sample_count']; - } - } - } - } - if ($isVideo && $framerate) { - $info['quicktime']['video']['frame_rate'] = $framerate; - $info['video']['frame_rate'] = $info['quicktime']['video']['frame_rate']; - } - if ($isVideo && $framecount) { - $info['quicktime']['video']['frame_count'] = $framecount; - } - break; - - - case 'aART': // Album ARTist - case 'catg': // CaTeGory - case 'covr': // COVeR artwork - case 'cpil': // ComPILation - case 'cprt': // CoPyRighT - case 'desc': // DESCription - case 'disk': // DISK number - case 'egid': // Episode Global ID - case 'gnre': // GeNRE - case 'keyw': // KEYWord - case 'ldes': - case 'pcst': // PodCaST - case 'pgap': // GAPless Playback - case 'purd': // PURchase Date - case 'purl': // Podcast URL - case 'rati': - case 'rndu': - case 'rpdu': - case 'rtng': // RaTiNG - case 'stik': - case 'tmpo': // TeMPO (BPM) - case 'trkn': // TRacK Number - case 'tves': // TV EpiSode - case 'tvnn': // TV Network Name - case 'tvsh': // TV SHow Name - case 'tvsn': // TV SeasoN - case 'akID': // iTunes store account type - case 'apID': - case 'atID': - case 'cmID': - case 'cnID': - case 'geID': - case 'plID': - case 'sfID': // iTunes store country - case '©alb': // ALBum - case '©art': // ARTist - case '©ART': - case '©aut': - case '©cmt': // CoMmenT - case '©com': // COMposer - case '©cpy': - case '©day': // content created year - case '©dir': - case '©ed1': - case '©ed2': - case '©ed3': - case '©ed4': - case '©ed5': - case '©ed6': - case '©ed7': - case '©ed8': - case '©ed9': - case '©enc': - case '©fmt': - case '©gen': // GENre - case '©grp': // GRouPing - case '©hst': - case '©inf': - case '©lyr': // LYRics - case '©mak': - case '©mod': - case '©nam': // full NAMe - case '©ope': - case '©PRD': - case '©prd': - case '©prf': - case '©req': - case '©src': - case '©swr': - case '©too': // encoder - case '©trk': // TRacK - case '©url': - case '©wrn': - case '©wrt': // WRiTer - case '----': // itunes specific - if ($atom_parent == 'udta') { - // User data atom handler - $atom_structure['data_length'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 2)); - $atom_structure['language_id'] = getid3_lib::BigEndian2Int(substr($atom_data, 2, 2)); - $atom_structure['data'] = substr($atom_data, 4); - - $atom_structure['language'] = $this->QuicktimeLanguageLookup($atom_structure['language_id']); - if (empty($info['comments']['language']) || (!in_array($atom_structure['language'], $info['comments']['language']))) { - $info['comments']['language'][] = $atom_structure['language']; - } - } else { - // Apple item list box atom handler - $atomoffset = 0; - if (substr($atom_data, 2, 2) == "\x10\xB5") { - // not sure what it means, but observed on iPhone4 data. - // Each $atom_data has 2 bytes of datasize, plus 0x10B5, then data - while ($atomoffset < strlen($atom_data)) { - $boxsmallsize = getid3_lib::BigEndian2Int(substr($atom_data, $atomoffset, 2)); - $boxsmalltype = substr($atom_data, $atomoffset + 2, 2); - $boxsmalldata = substr($atom_data, $atomoffset + 4, $boxsmallsize); - switch ($boxsmalltype) { - case "\x10\xB5": - $atom_structure['data'] = $boxsmalldata; - break; - default: - $info['warning'][] = 'Unknown QuickTime smallbox type: "'.getid3_lib::PrintHexBytes($boxsmalltype).'" at offset '.$baseoffset; - $atom_structure['data'] = $atom_data; - break; - } - $atomoffset += (4 + $boxsmallsize); - } - } else { - while ($atomoffset < strlen($atom_data)) { - $boxsize = getid3_lib::BigEndian2Int(substr($atom_data, $atomoffset, 4)); - $boxtype = substr($atom_data, $atomoffset + 4, 4); - $boxdata = substr($atom_data, $atomoffset + 8, $boxsize - 8); - if ($boxsize <= 1) { - $info['warning'][] = 'Invalid QuickTime atom box size "'.$boxsize.'" in atom "'.$atomname.'" at offset: '.($atom_structure['offset'] + $atomoffset); - $atom_structure['data'] = null; - $atomoffset = strlen($atom_data); - break; - } - $atomoffset += $boxsize; - - switch ($boxtype) { - case 'mean': - case 'name': - $atom_structure[$boxtype] = substr($boxdata, 4); - break; - - case 'data': - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($boxdata, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($boxdata, 1, 3)); - switch ($atom_structure['flags_raw']) { - case 0: // data flag - case 21: // tmpo/cpil flag - switch ($atomname) { - case 'cpil': - case 'pcst': - case 'pgap': - $atom_structure['data'] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 1)); - break; - - case 'tmpo': - $atom_structure['data'] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 2)); - break; - - case 'disk': - case 'trkn': - $num = getid3_lib::BigEndian2Int(substr($boxdata, 10, 2)); - $num_total = getid3_lib::BigEndian2Int(substr($boxdata, 12, 2)); - $atom_structure['data'] = empty($num) ? '' : $num; - $atom_structure['data'] .= empty($num_total) ? '' : '/'.$num_total; - break; - - case 'gnre': - $GenreID = getid3_lib::BigEndian2Int(substr($boxdata, 8, 4)); - $atom_structure['data'] = getid3_id3v1::LookupGenreName($GenreID - 1); - break; - - case 'rtng': - $atom_structure[$atomname] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 1)); - $atom_structure['data'] = $this->QuicktimeContentRatingLookup($atom_structure[$atomname]); - break; - - case 'stik': - $atom_structure[$atomname] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 1)); - $atom_structure['data'] = $this->QuicktimeSTIKLookup($atom_structure[$atomname]); - break; - - case 'sfID': - $atom_structure[$atomname] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 4)); - $atom_structure['data'] = $this->QuicktimeStoreFrontCodeLookup($atom_structure[$atomname]); - break; - - case 'egid': - case 'purl': - $atom_structure['data'] = substr($boxdata, 8); - break; - - default: - $atom_structure['data'] = getid3_lib::BigEndian2Int(substr($boxdata, 8, 4)); - } - break; - - case 1: // text flag - case 13: // image flag - default: - $atom_structure['data'] = substr($boxdata, 8); - break; - - } - break; - - default: - $info['warning'][] = 'Unknown QuickTime box type: "'.getid3_lib::PrintHexBytes($boxtype).'" at offset '.$baseoffset; - $atom_structure['data'] = $atom_data; - - } - } - } - } - $this->CopyToAppropriateCommentsSection($atomname, $atom_structure['data'], $atom_structure['name']); - break; - - - case 'play': // auto-PLAY atom - $atom_structure['autoplay'] = (bool) getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - - $info['quicktime']['autoplay'] = $atom_structure['autoplay']; - break; - - - case 'WLOC': // Window LOCation atom - $atom_structure['location_x'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 2)); - $atom_structure['location_y'] = getid3_lib::BigEndian2Int(substr($atom_data, 2, 2)); - break; - - - case 'LOOP': // LOOPing atom - case 'SelO': // play SELection Only atom - case 'AllF': // play ALL Frames atom - $atom_structure['data'] = getid3_lib::BigEndian2Int($atom_data); - break; - - - case 'name': // - case 'MCPS': // Media Cleaner PRo - case '@PRM': // adobe PReMiere version - case '@PRQ': // adobe PRemiere Quicktime version - $atom_structure['data'] = $atom_data; - break; - - - case 'cmvd': // Compressed MooV Data atom - // Code by ubergeekØubergeek*tv based on information from - // http://developer.apple.com/quicktime/icefloe/dispatch012.html - $atom_structure['unCompressedSize'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); - - $CompressedFileData = substr($atom_data, 4); - if ($UncompressedHeader = @gzuncompress($CompressedFileData)) { - $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($UncompressedHeader, 0, $atomHierarchy, $ParseAllPossibleAtoms); - } else { - $info['warning'][] = 'Error decompressing compressed MOV atom at offset '.$atom_structure['offset']; - } - break; - - - case 'dcom': // Data COMpression atom - $atom_structure['compression_id'] = $atom_data; - $atom_structure['compression_text'] = $this->QuicktimeDCOMLookup($atom_data); - break; - - - case 'rdrf': // Reference movie Data ReFerence atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); - $atom_structure['flags']['internal_data'] = (bool) ($atom_structure['flags_raw'] & 0x000001); - - $atom_structure['reference_type_name'] = substr($atom_data, 4, 4); - $atom_structure['reference_length'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); - switch ($atom_structure['reference_type_name']) { - case 'url ': - $atom_structure['url'] = $this->NoNullString(substr($atom_data, 12)); - break; - - case 'alis': - $atom_structure['file_alias'] = substr($atom_data, 12); - break; - - case 'rsrc': - $atom_structure['resource_alias'] = substr($atom_data, 12); - break; - - default: - $atom_structure['data'] = substr($atom_data, 12); - break; - } - break; - - - case 'rmqu': // Reference Movie QUality atom - $atom_structure['movie_quality'] = getid3_lib::BigEndian2Int($atom_data); - break; - - - case 'rmcs': // Reference Movie Cpu Speed atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['cpu_speed_rating'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); - break; - - - case 'rmvc': // Reference Movie Version Check atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['gestalt_selector'] = substr($atom_data, 4, 4); - $atom_structure['gestalt_value_mask'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); - $atom_structure['gestalt_value'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 4)); - $atom_structure['gestalt_check_type'] = getid3_lib::BigEndian2Int(substr($atom_data, 14, 2)); - break; - - - case 'rmcd': // Reference Movie Component check atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['component_type'] = substr($atom_data, 4, 4); - $atom_structure['component_subtype'] = substr($atom_data, 8, 4); - $atom_structure['component_manufacturer'] = substr($atom_data, 12, 4); - $atom_structure['component_flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 16, 4)); - $atom_structure['component_flags_mask'] = getid3_lib::BigEndian2Int(substr($atom_data, 20, 4)); - $atom_structure['component_min_version'] = getid3_lib::BigEndian2Int(substr($atom_data, 24, 4)); - break; - - - case 'rmdr': // Reference Movie Data Rate atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['data_rate'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - - $atom_structure['data_rate_bps'] = $atom_structure['data_rate'] * 10; - break; - - - case 'rmla': // Reference Movie Language Atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['language_id'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); - - $atom_structure['language'] = $this->QuicktimeLanguageLookup($atom_structure['language_id']); - if (empty($info['comments']['language']) || (!in_array($atom_structure['language'], $info['comments']['language']))) { - $info['comments']['language'][] = $atom_structure['language']; - } - break; - - - case 'rmla': // Reference Movie Language Atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['track_id'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); - break; - - - case 'ptv ': // Print To Video - defines a movie's full screen mode - // http://developer.apple.com/documentation/QuickTime/APIREF/SOURCESIV/at_ptv-_pg.htm - $atom_structure['display_size_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 2)); - $atom_structure['reserved_1'] = getid3_lib::BigEndian2Int(substr($atom_data, 2, 2)); // hardcoded: 0x0000 - $atom_structure['reserved_2'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); // hardcoded: 0x0000 - $atom_structure['slide_show_flag'] = getid3_lib::BigEndian2Int(substr($atom_data, 6, 1)); - $atom_structure['play_on_open_flag'] = getid3_lib::BigEndian2Int(substr($atom_data, 7, 1)); - - $atom_structure['flags']['play_on_open'] = (bool) $atom_structure['play_on_open_flag']; - $atom_structure['flags']['slide_show'] = (bool) $atom_structure['slide_show_flag']; - - $ptv_lookup[0] = 'normal'; - $ptv_lookup[1] = 'double'; - $ptv_lookup[2] = 'half'; - $ptv_lookup[3] = 'full'; - $ptv_lookup[4] = 'current'; - if (isset($ptv_lookup[$atom_structure['display_size_raw']])) { - $atom_structure['display_size'] = $ptv_lookup[$atom_structure['display_size_raw']]; - } else { - $info['warning'][] = 'unknown "ptv " display constant ('.$atom_structure['display_size_raw'].')'; - } - break; - - - case 'stsd': // Sample Table Sample Description atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $stsdEntriesDataOffset = 8; - for ($i = 0; $i < $atom_structure['number_entries']; $i++) { - $atom_structure['sample_description_table'][$i]['size'] = getid3_lib::BigEndian2Int(substr($atom_data, $stsdEntriesDataOffset, 4)); - $stsdEntriesDataOffset += 4; - $atom_structure['sample_description_table'][$i]['data_format'] = substr($atom_data, $stsdEntriesDataOffset, 4); - $stsdEntriesDataOffset += 4; - $atom_structure['sample_description_table'][$i]['reserved'] = getid3_lib::BigEndian2Int(substr($atom_data, $stsdEntriesDataOffset, 6)); - $stsdEntriesDataOffset += 6; - $atom_structure['sample_description_table'][$i]['reference_index'] = getid3_lib::BigEndian2Int(substr($atom_data, $stsdEntriesDataOffset, 2)); - $stsdEntriesDataOffset += 2; - $atom_structure['sample_description_table'][$i]['data'] = substr($atom_data, $stsdEntriesDataOffset, ($atom_structure['sample_description_table'][$i]['size'] - 4 - 4 - 6 - 2)); - $stsdEntriesDataOffset += ($atom_structure['sample_description_table'][$i]['size'] - 4 - 4 - 6 - 2); - - $atom_structure['sample_description_table'][$i]['encoder_version'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 0, 2)); - $atom_structure['sample_description_table'][$i]['encoder_revision'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 2, 2)); - $atom_structure['sample_description_table'][$i]['encoder_vendor'] = substr($atom_structure['sample_description_table'][$i]['data'], 4, 4); - - switch ($atom_structure['sample_description_table'][$i]['encoder_vendor']) { - - case "\x00\x00\x00\x00": - // audio atom - $atom_structure['sample_description_table'][$i]['audio_channels'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 8, 2)); - $atom_structure['sample_description_table'][$i]['audio_bit_depth'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 10, 2)); - $atom_structure['sample_description_table'][$i]['audio_compression_id'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 12, 2)); - $atom_structure['sample_description_table'][$i]['audio_packet_size'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 14, 2)); - $atom_structure['sample_description_table'][$i]['audio_sample_rate'] = getid3_lib::FixedPoint16_16(substr($atom_structure['sample_description_table'][$i]['data'], 16, 4)); - - switch ($atom_structure['sample_description_table'][$i]['data_format']) { - case 'avc1': - case 'mp4v': - $info['fileformat'] = 'mp4'; - $info['video']['fourcc'] = $atom_structure['sample_description_table'][$i]['data_format']; - //$info['warning'][] = 'This version of getID3() ['.$this->getid3->version().'] does not fully support MPEG-4 audio/video streams'; // 2011-02-18: why am I warning about this again? What's not supported? - break; - - case 'qtvr': - $info['video']['dataformat'] = 'quicktimevr'; - break; - - case 'mp4a': - default: - $info['quicktime']['audio']['codec'] = $this->QuicktimeAudioCodecLookup($atom_structure['sample_description_table'][$i]['data_format']); - $info['quicktime']['audio']['sample_rate'] = $atom_structure['sample_description_table'][$i]['audio_sample_rate']; - $info['quicktime']['audio']['channels'] = $atom_structure['sample_description_table'][$i]['audio_channels']; - $info['quicktime']['audio']['bit_depth'] = $atom_structure['sample_description_table'][$i]['audio_bit_depth']; - $info['audio']['codec'] = $info['quicktime']['audio']['codec']; - $info['audio']['sample_rate'] = $info['quicktime']['audio']['sample_rate']; - $info['audio']['channels'] = $info['quicktime']['audio']['channels']; - $info['audio']['bits_per_sample'] = $info['quicktime']['audio']['bit_depth']; - switch ($atom_structure['sample_description_table'][$i]['data_format']) { - case 'raw ': // PCM - case 'alac': // Apple Lossless Audio Codec - $info['audio']['lossless'] = true; - break; - default: - $info['audio']['lossless'] = false; - break; - } - break; - } - break; - - default: - switch ($atom_structure['sample_description_table'][$i]['data_format']) { - case 'mp4s': - $info['fileformat'] = 'mp4'; - break; - - default: - // video atom - $atom_structure['sample_description_table'][$i]['video_temporal_quality'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 8, 4)); - $atom_structure['sample_description_table'][$i]['video_spatial_quality'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 12, 4)); - $atom_structure['sample_description_table'][$i]['video_frame_width'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 16, 2)); - $atom_structure['sample_description_table'][$i]['video_frame_height'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 18, 2)); - $atom_structure['sample_description_table'][$i]['video_resolution_x'] = getid3_lib::FixedPoint16_16(substr($atom_structure['sample_description_table'][$i]['data'], 20, 4)); - $atom_structure['sample_description_table'][$i]['video_resolution_y'] = getid3_lib::FixedPoint16_16(substr($atom_structure['sample_description_table'][$i]['data'], 24, 4)); - $atom_structure['sample_description_table'][$i]['video_data_size'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 28, 4)); - $atom_structure['sample_description_table'][$i]['video_frame_count'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 32, 2)); - $atom_structure['sample_description_table'][$i]['video_encoder_name_len'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 34, 1)); - $atom_structure['sample_description_table'][$i]['video_encoder_name'] = substr($atom_structure['sample_description_table'][$i]['data'], 35, $atom_structure['sample_description_table'][$i]['video_encoder_name_len']); - $atom_structure['sample_description_table'][$i]['video_pixel_color_depth'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 66, 2)); - $atom_structure['sample_description_table'][$i]['video_color_table_id'] = getid3_lib::BigEndian2Int(substr($atom_structure['sample_description_table'][$i]['data'], 68, 2)); - - $atom_structure['sample_description_table'][$i]['video_pixel_color_type'] = (($atom_structure['sample_description_table'][$i]['video_pixel_color_depth'] > 32) ? 'grayscale' : 'color'); - $atom_structure['sample_description_table'][$i]['video_pixel_color_name'] = $this->QuicktimeColorNameLookup($atom_structure['sample_description_table'][$i]['video_pixel_color_depth']); - - if ($atom_structure['sample_description_table'][$i]['video_pixel_color_name'] != 'invalid') { - $info['quicktime']['video']['codec_fourcc'] = $atom_structure['sample_description_table'][$i]['data_format']; - $info['quicktime']['video']['codec_fourcc_lookup'] = $this->QuicktimeVideoCodecLookup($atom_structure['sample_description_table'][$i]['data_format']); - $info['quicktime']['video']['codec'] = (($atom_structure['sample_description_table'][$i]['video_encoder_name_len'] > 0) ? $atom_structure['sample_description_table'][$i]['video_encoder_name'] : $atom_structure['sample_description_table'][$i]['data_format']); - $info['quicktime']['video']['color_depth'] = $atom_structure['sample_description_table'][$i]['video_pixel_color_depth']; - $info['quicktime']['video']['color_depth_name'] = $atom_structure['sample_description_table'][$i]['video_pixel_color_name']; - - $info['video']['codec'] = $info['quicktime']['video']['codec']; - $info['video']['bits_per_sample'] = $info['quicktime']['video']['color_depth']; - } - $info['video']['lossless'] = false; - $info['video']['pixel_aspect_ratio'] = (float) 1; - break; - } - break; - } - switch (strtolower($atom_structure['sample_description_table'][$i]['data_format'])) { - case 'mp4a': - $info['audio']['dataformat'] = 'mp4'; - $info['quicktime']['audio']['codec'] = 'mp4'; - break; - - case '3ivx': - case '3iv1': - case '3iv2': - $info['video']['dataformat'] = '3ivx'; - break; - - case 'xvid': - $info['video']['dataformat'] = 'xvid'; - break; - - case 'mp4v': - $info['video']['dataformat'] = 'mpeg4'; - break; - - case 'divx': - case 'div1': - case 'div2': - case 'div3': - case 'div4': - case 'div5': - case 'div6': - $info['video']['dataformat'] = 'divx'; - break; - - default: - // do nothing - break; - } - unset($atom_structure['sample_description_table'][$i]['data']); - } - break; - - - case 'stts': // Sample Table Time-to-Sample atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $sttsEntriesDataOffset = 8; - //$FrameRateCalculatorArray = array(); - $frames_count = 0; - for ($i = 0; $i < $atom_structure['number_entries']; $i++) { - $atom_structure['time_to_sample_table'][$i]['sample_count'] = getid3_lib::BigEndian2Int(substr($atom_data, $sttsEntriesDataOffset, 4)); - $sttsEntriesDataOffset += 4; - $atom_structure['time_to_sample_table'][$i]['sample_duration'] = getid3_lib::BigEndian2Int(substr($atom_data, $sttsEntriesDataOffset, 4)); - $sttsEntriesDataOffset += 4; - - $frames_count += $atom_structure['time_to_sample_table'][$i]['sample_count']; - - // THIS SECTION REPLACED WITH CODE IN "stbl" ATOM - //if (!empty($info['quicktime']['time_scale']) && ($atom_structure['time_to_sample_table'][$i]['sample_duration'] > 0)) { - // $stts_new_framerate = $info['quicktime']['time_scale'] / $atom_structure['time_to_sample_table'][$i]['sample_duration']; - // if ($stts_new_framerate <= 60) { - // // some atoms have durations of "1" giving a very large framerate, which probably is not right - // $info['video']['frame_rate'] = max($info['video']['frame_rate'], $stts_new_framerate); - // } - //} - // - //$FrameRateCalculatorArray[($info['quicktime']['time_scale'] / $atom_structure['time_to_sample_table'][$i]['sample_duration'])] += $atom_structure['time_to_sample_table'][$i]['sample_count']; - } - $info['quicktime']['stts_framecount'][] = $frames_count; - //$sttsFramesTotal = 0; - //$sttsSecondsTotal = 0; - //foreach ($FrameRateCalculatorArray as $frames_per_second => $frame_count) { - // if (($frames_per_second > 60) || ($frames_per_second < 1)) { - // // not video FPS information, probably audio information - // $sttsFramesTotal = 0; - // $sttsSecondsTotal = 0; - // break; - // } - // $sttsFramesTotal += $frame_count; - // $sttsSecondsTotal += $frame_count / $frames_per_second; - //} - //if (($sttsFramesTotal > 0) && ($sttsSecondsTotal > 0)) { - // if (($sttsFramesTotal / $sttsSecondsTotal) > $info['video']['frame_rate']) { - // $info['video']['frame_rate'] = $sttsFramesTotal / $sttsSecondsTotal; - // } - //} - break; - - - case 'stss': // Sample Table Sync Sample (key frames) atom - if ($ParseAllPossibleAtoms) { - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $stssEntriesDataOffset = 8; - for ($i = 0; $i < $atom_structure['number_entries']; $i++) { - $atom_structure['time_to_sample_table'][$i] = getid3_lib::BigEndian2Int(substr($atom_data, $stssEntriesDataOffset, 4)); - $stssEntriesDataOffset += 4; - } - } - break; - - - case 'stsc': // Sample Table Sample-to-Chunk atom - if ($ParseAllPossibleAtoms) { - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $stscEntriesDataOffset = 8; - for ($i = 0; $i < $atom_structure['number_entries']; $i++) { - $atom_structure['sample_to_chunk_table'][$i]['first_chunk'] = getid3_lib::BigEndian2Int(substr($atom_data, $stscEntriesDataOffset, 4)); - $stscEntriesDataOffset += 4; - $atom_structure['sample_to_chunk_table'][$i]['samples_per_chunk'] = getid3_lib::BigEndian2Int(substr($atom_data, $stscEntriesDataOffset, 4)); - $stscEntriesDataOffset += 4; - $atom_structure['sample_to_chunk_table'][$i]['sample_description'] = getid3_lib::BigEndian2Int(substr($atom_data, $stscEntriesDataOffset, 4)); - $stscEntriesDataOffset += 4; - } - } - break; - - - case 'stsz': // Sample Table SiZe atom - if ($ParseAllPossibleAtoms) { - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['sample_size'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); - $stszEntriesDataOffset = 12; - if ($atom_structure['sample_size'] == 0) { - for ($i = 0; $i < $atom_structure['number_entries']; $i++) { - $atom_structure['sample_size_table'][$i] = getid3_lib::BigEndian2Int(substr($atom_data, $stszEntriesDataOffset, 4)); - $stszEntriesDataOffset += 4; - } - } - } - break; - - - case 'stco': // Sample Table Chunk Offset atom - if ($ParseAllPossibleAtoms) { - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $stcoEntriesDataOffset = 8; - for ($i = 0; $i < $atom_structure['number_entries']; $i++) { - $atom_structure['chunk_offset_table'][$i] = getid3_lib::BigEndian2Int(substr($atom_data, $stcoEntriesDataOffset, 4)); - $stcoEntriesDataOffset += 4; - } - } - break; - - - case 'co64': // Chunk Offset 64-bit (version of "stco" that supports > 2GB files) - if ($ParseAllPossibleAtoms) { - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $stcoEntriesDataOffset = 8; - for ($i = 0; $i < $atom_structure['number_entries']; $i++) { - $atom_structure['chunk_offset_table'][$i] = getid3_lib::BigEndian2Int(substr($atom_data, $stcoEntriesDataOffset, 8)); - $stcoEntriesDataOffset += 8; - } - } - break; - - - case 'dref': // Data REFerence atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $drefDataOffset = 8; - for ($i = 0; $i < $atom_structure['number_entries']; $i++) { - $atom_structure['data_references'][$i]['size'] = getid3_lib::BigEndian2Int(substr($atom_data, $drefDataOffset, 4)); - $drefDataOffset += 4; - $atom_structure['data_references'][$i]['type'] = substr($atom_data, $drefDataOffset, 4); - $drefDataOffset += 4; - $atom_structure['data_references'][$i]['version'] = getid3_lib::BigEndian2Int(substr($atom_data, $drefDataOffset, 1)); - $drefDataOffset += 1; - $atom_structure['data_references'][$i]['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, $drefDataOffset, 3)); // hardcoded: 0x0000 - $drefDataOffset += 3; - $atom_structure['data_references'][$i]['data'] = substr($atom_data, $drefDataOffset, ($atom_structure['data_references'][$i]['size'] - 4 - 4 - 1 - 3)); - $drefDataOffset += ($atom_structure['data_references'][$i]['size'] - 4 - 4 - 1 - 3); - - $atom_structure['data_references'][$i]['flags']['self_reference'] = (bool) ($atom_structure['data_references'][$i]['flags_raw'] & 0x001); - } - break; - - - case 'gmin': // base Media INformation atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['graphics_mode'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); - $atom_structure['opcolor_red'] = getid3_lib::BigEndian2Int(substr($atom_data, 6, 2)); - $atom_structure['opcolor_green'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 2)); - $atom_structure['opcolor_blue'] = getid3_lib::BigEndian2Int(substr($atom_data, 10, 2)); - $atom_structure['balance'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 2)); - $atom_structure['reserved'] = getid3_lib::BigEndian2Int(substr($atom_data, 14, 2)); - break; - - - case 'smhd': // Sound Media information HeaDer atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['balance'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); - $atom_structure['reserved'] = getid3_lib::BigEndian2Int(substr($atom_data, 6, 2)); - break; - - - case 'vmhd': // Video Media information HeaDer atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); - $atom_structure['graphics_mode'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); - $atom_structure['opcolor_red'] = getid3_lib::BigEndian2Int(substr($atom_data, 6, 2)); - $atom_structure['opcolor_green'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 2)); - $atom_structure['opcolor_blue'] = getid3_lib::BigEndian2Int(substr($atom_data, 10, 2)); - - $atom_structure['flags']['no_lean_ahead'] = (bool) ($atom_structure['flags_raw'] & 0x001); - break; - - - case 'hdlr': // HanDLeR reference atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['component_type'] = substr($atom_data, 4, 4); - $atom_structure['component_subtype'] = substr($atom_data, 8, 4); - $atom_structure['component_manufacturer'] = substr($atom_data, 12, 4); - $atom_structure['component_flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 16, 4)); - $atom_structure['component_flags_mask'] = getid3_lib::BigEndian2Int(substr($atom_data, 20, 4)); - $atom_structure['component_name'] = $this->Pascal2String(substr($atom_data, 24)); - - if (($atom_structure['component_subtype'] == 'STpn') && ($atom_structure['component_manufacturer'] == 'zzzz')) { - $info['video']['dataformat'] = 'quicktimevr'; - } - break; - - - case 'mdhd': // MeDia HeaDer atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['creation_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $atom_structure['modify_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); - $atom_structure['time_scale'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 4)); - $atom_structure['duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 16, 4)); - $atom_structure['language_id'] = getid3_lib::BigEndian2Int(substr($atom_data, 20, 2)); - $atom_structure['quality'] = getid3_lib::BigEndian2Int(substr($atom_data, 22, 2)); - - if ($atom_structure['time_scale'] == 0) { - $info['error'][] = 'Corrupt Quicktime file: mdhd.time_scale == zero'; - return false; - } - $info['quicktime']['time_scale'] = (isset($info['quicktime']['time_scale']) ? max($info['quicktime']['time_scale'], $atom_structure['time_scale']) : $atom_structure['time_scale']); - - $atom_structure['creation_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['creation_time']); - $atom_structure['modify_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['modify_time']); - $atom_structure['playtime_seconds'] = $atom_structure['duration'] / $atom_structure['time_scale']; - $atom_structure['language'] = $this->QuicktimeLanguageLookup($atom_structure['language_id']); - if (empty($info['comments']['language']) || (!in_array($atom_structure['language'], $info['comments']['language']))) { - $info['comments']['language'][] = $atom_structure['language']; - } - break; - - - case 'pnot': // Preview atom - $atom_structure['modification_date'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); // "standard Macintosh format" - $atom_structure['version_number'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); // hardcoded: 0x00 - $atom_structure['atom_type'] = substr($atom_data, 6, 4); // usually: 'PICT' - $atom_structure['atom_index'] = getid3_lib::BigEndian2Int(substr($atom_data, 10, 2)); // usually: 0x01 - - $atom_structure['modification_date_unix'] = getid3_lib::DateMac2Unix($atom_structure['modification_date']); - break; - - - case 'crgn': // Clipping ReGioN atom - $atom_structure['region_size'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 2)); // The Region size, Region boundary box, - $atom_structure['boundary_box'] = getid3_lib::BigEndian2Int(substr($atom_data, 2, 8)); // and Clipping region data fields - $atom_structure['clipping_data'] = substr($atom_data, 10); // constitute a QuickDraw region. - break; - - - case 'load': // track LOAD settings atom - $atom_structure['preload_start_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); - $atom_structure['preload_duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $atom_structure['preload_flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); - $atom_structure['default_hints_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 4)); - - $atom_structure['default_hints']['double_buffer'] = (bool) ($atom_structure['default_hints_raw'] & 0x0020); - $atom_structure['default_hints']['high_quality'] = (bool) ($atom_structure['default_hints_raw'] & 0x0100); - break; - - - case 'tmcd': // TiMe CoDe atom - case 'chap': // CHAPter list atom - case 'sync': // SYNChronization atom - case 'scpt': // tranSCriPT atom - case 'ssrc': // non-primary SouRCe atom - for ($i = 0; $i < (strlen($atom_data) % 4); $i++) { - $atom_structure['track_id'][$i] = getid3_lib::BigEndian2Int(substr($atom_data, $i * 4, 4)); - } - break; - - - case 'elst': // Edit LiST atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['number_entries'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - for ($i = 0; $i < $atom_structure['number_entries']; $i++ ) { - $atom_structure['edit_list'][$i]['track_duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($i * 12) + 0, 4)); - $atom_structure['edit_list'][$i]['media_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($i * 12) + 4, 4)); - $atom_structure['edit_list'][$i]['media_rate'] = getid3_lib::FixedPoint16_16(substr($atom_data, 8 + ($i * 12) + 8, 4)); - } - break; - - - case 'kmat': // compressed MATte atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); // hardcoded: 0x0000 - $atom_structure['matte_data_raw'] = substr($atom_data, 4); - break; - - - case 'ctab': // Color TABle atom - $atom_structure['color_table_seed'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); // hardcoded: 0x00000000 - $atom_structure['color_table_flags'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 2)); // hardcoded: 0x8000 - $atom_structure['color_table_size'] = getid3_lib::BigEndian2Int(substr($atom_data, 6, 2)) + 1; - for ($colortableentry = 0; $colortableentry < $atom_structure['color_table_size']; $colortableentry++) { - $atom_structure['color_table'][$colortableentry]['alpha'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($colortableentry * 8) + 0, 2)); - $atom_structure['color_table'][$colortableentry]['red'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($colortableentry * 8) + 2, 2)); - $atom_structure['color_table'][$colortableentry]['green'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($colortableentry * 8) + 4, 2)); - $atom_structure['color_table'][$colortableentry]['blue'] = getid3_lib::BigEndian2Int(substr($atom_data, 8 + ($colortableentry * 8) + 6, 2)); - } - break; - - - case 'mvhd': // MoVie HeaDer atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); - $atom_structure['creation_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $atom_structure['modify_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); - $atom_structure['time_scale'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 4)); - $atom_structure['duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 16, 4)); - $atom_structure['preferred_rate'] = getid3_lib::FixedPoint16_16(substr($atom_data, 20, 4)); - $atom_structure['preferred_volume'] = getid3_lib::FixedPoint8_8(substr($atom_data, 24, 2)); - $atom_structure['reserved'] = substr($atom_data, 26, 10); - $atom_structure['matrix_a'] = getid3_lib::FixedPoint16_16(substr($atom_data, 36, 4)); - $atom_structure['matrix_b'] = getid3_lib::FixedPoint16_16(substr($atom_data, 40, 4)); - $atom_structure['matrix_u'] = getid3_lib::FixedPoint2_30(substr($atom_data, 44, 4)); - $atom_structure['matrix_c'] = getid3_lib::FixedPoint16_16(substr($atom_data, 48, 4)); - $atom_structure['matrix_d'] = getid3_lib::FixedPoint16_16(substr($atom_data, 52, 4)); - $atom_structure['matrix_v'] = getid3_lib::FixedPoint2_30(substr($atom_data, 56, 4)); - $atom_structure['matrix_x'] = getid3_lib::FixedPoint16_16(substr($atom_data, 60, 4)); - $atom_structure['matrix_y'] = getid3_lib::FixedPoint16_16(substr($atom_data, 64, 4)); - $atom_structure['matrix_w'] = getid3_lib::FixedPoint2_30(substr($atom_data, 68, 4)); - $atom_structure['preview_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 72, 4)); - $atom_structure['preview_duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 76, 4)); - $atom_structure['poster_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 80, 4)); - $atom_structure['selection_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 84, 4)); - $atom_structure['selection_duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 88, 4)); - $atom_structure['current_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 92, 4)); - $atom_structure['next_track_id'] = getid3_lib::BigEndian2Int(substr($atom_data, 96, 4)); - - if ($atom_structure['time_scale'] == 0) { - $info['error'][] = 'Corrupt Quicktime file: mvhd.time_scale == zero'; - return false; - } - $atom_structure['creation_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['creation_time']); - $atom_structure['modify_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['modify_time']); - $info['quicktime']['time_scale'] = (isset($info['quicktime']['time_scale']) ? max($info['quicktime']['time_scale'], $atom_structure['time_scale']) : $atom_structure['time_scale']); - $info['quicktime']['display_scale'] = $atom_structure['matrix_a']; - $info['playtime_seconds'] = $atom_structure['duration'] / $atom_structure['time_scale']; - break; - - - case 'tkhd': // TracK HeaDer atom - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); - $atom_structure['creation_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $atom_structure['modify_time'] = getid3_lib::BigEndian2Int(substr($atom_data, 8, 4)); - $atom_structure['trackid'] = getid3_lib::BigEndian2Int(substr($atom_data, 12, 4)); - $atom_structure['reserved1'] = getid3_lib::BigEndian2Int(substr($atom_data, 16, 4)); - $atom_structure['duration'] = getid3_lib::BigEndian2Int(substr($atom_data, 20, 4)); - $atom_structure['reserved2'] = getid3_lib::BigEndian2Int(substr($atom_data, 24, 8)); - $atom_structure['layer'] = getid3_lib::BigEndian2Int(substr($atom_data, 32, 2)); - $atom_structure['alternate_group'] = getid3_lib::BigEndian2Int(substr($atom_data, 34, 2)); - $atom_structure['volume'] = getid3_lib::FixedPoint8_8(substr($atom_data, 36, 2)); - $atom_structure['reserved3'] = getid3_lib::BigEndian2Int(substr($atom_data, 38, 2)); - $atom_structure['matrix_a'] = getid3_lib::FixedPoint16_16(substr($atom_data, 40, 4)); - $atom_structure['matrix_b'] = getid3_lib::FixedPoint16_16(substr($atom_data, 44, 4)); - $atom_structure['matrix_u'] = getid3_lib::FixedPoint16_16(substr($atom_data, 48, 4)); - $atom_structure['matrix_c'] = getid3_lib::FixedPoint16_16(substr($atom_data, 52, 4)); - $atom_structure['matrix_d'] = getid3_lib::FixedPoint16_16(substr($atom_data, 56, 4)); - $atom_structure['matrix_v'] = getid3_lib::FixedPoint16_16(substr($atom_data, 60, 4)); - $atom_structure['matrix_x'] = getid3_lib::FixedPoint2_30(substr($atom_data, 64, 4)); - $atom_structure['matrix_y'] = getid3_lib::FixedPoint2_30(substr($atom_data, 68, 4)); - $atom_structure['matrix_w'] = getid3_lib::FixedPoint2_30(substr($atom_data, 72, 4)); - $atom_structure['width'] = getid3_lib::FixedPoint16_16(substr($atom_data, 76, 4)); - $atom_structure['height'] = getid3_lib::FixedPoint16_16(substr($atom_data, 80, 4)); - - $atom_structure['flags']['enabled'] = (bool) ($atom_structure['flags_raw'] & 0x0001); - $atom_structure['flags']['in_movie'] = (bool) ($atom_structure['flags_raw'] & 0x0002); - $atom_structure['flags']['in_preview'] = (bool) ($atom_structure['flags_raw'] & 0x0004); - $atom_structure['flags']['in_poster'] = (bool) ($atom_structure['flags_raw'] & 0x0008); - $atom_structure['creation_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['creation_time']); - $atom_structure['modify_time_unix'] = getid3_lib::DateMac2Unix($atom_structure['modify_time']); - - if ($atom_structure['flags']['enabled'] == 1) { - if (!isset($info['video']['resolution_x']) || !isset($info['video']['resolution_y'])) { - $info['video']['resolution_x'] = $atom_structure['width']; - $info['video']['resolution_y'] = $atom_structure['height']; - } - $info['video']['resolution_x'] = max($info['video']['resolution_x'], $atom_structure['width']); - $info['video']['resolution_y'] = max($info['video']['resolution_y'], $atom_structure['height']); - $info['quicktime']['video']['resolution_x'] = $info['video']['resolution_x']; - $info['quicktime']['video']['resolution_y'] = $info['video']['resolution_y']; - } else { - // see: http://www.getid3.org/phpBB3/viewtopic.php?t=1295 - //if (isset($info['video']['resolution_x'])) { unset($info['video']['resolution_x']); } - //if (isset($info['video']['resolution_y'])) { unset($info['video']['resolution_y']); } - //if (isset($info['quicktime']['video'])) { unset($info['quicktime']['video']); } - } - break; - - - case 'iods': // Initial Object DeScriptor atom - // http://www.koders.com/c/fid1FAB3E762903DC482D8A246D4A4BF9F28E049594.aspx?s=windows.h - // http://libquicktime.sourcearchive.com/documentation/1.0.2plus-pdebian/iods_8c-source.html - $offset = 0; - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); - $offset += 1; - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 3)); - $offset += 3; - $atom_structure['mp4_iod_tag'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); - $offset += 1; - $atom_structure['length'] = $this->quicktime_read_mp4_descr_length($atom_data, $offset); - //$offset already adjusted by quicktime_read_mp4_descr_length() - $atom_structure['object_descriptor_id'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 2)); - $offset += 2; - $atom_structure['od_profile_level'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); - $offset += 1; - $atom_structure['scene_profile_level'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); - $offset += 1; - $atom_structure['audio_profile_id'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); - $offset += 1; - $atom_structure['video_profile_id'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); - $offset += 1; - $atom_structure['graphics_profile_level'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); - $offset += 1; - - $atom_structure['num_iods_tracks'] = ($atom_structure['length'] - 7) / 6; // 6 bytes would only be right if all tracks use 1-byte length fields - for ($i = 0; $i < $atom_structure['num_iods_tracks']; $i++) { - $atom_structure['track'][$i]['ES_ID_IncTag'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 1)); - $offset += 1; - $atom_structure['track'][$i]['length'] = $this->quicktime_read_mp4_descr_length($atom_data, $offset); - //$offset already adjusted by quicktime_read_mp4_descr_length() - $atom_structure['track'][$i]['track_id'] = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 4)); - $offset += 4; - } - - $atom_structure['audio_profile_name'] = $this->QuicktimeIODSaudioProfileName($atom_structure['audio_profile_id']); - $atom_structure['video_profile_name'] = $this->QuicktimeIODSvideoProfileName($atom_structure['video_profile_id']); - break; - - case 'ftyp': // FileTYPe (?) atom (for MP4 it seems) - $atom_structure['signature'] = substr($atom_data, 0, 4); - $atom_structure['unknown_1'] = getid3_lib::BigEndian2Int(substr($atom_data, 4, 4)); - $atom_structure['fourcc'] = substr($atom_data, 8, 4); - break; - - case 'mdat': // Media DATa atom - case 'free': // FREE space atom - case 'skip': // SKIP atom - case 'wide': // 64-bit expansion placeholder atom - // 'mdat' data is too big to deal with, contains no useful metadata - // 'free', 'skip' and 'wide' are just padding, contains no useful data at all - - // When writing QuickTime files, it is sometimes necessary to update an atom's size. - // It is impossible to update a 32-bit atom to a 64-bit atom since the 32-bit atom - // is only 8 bytes in size, and the 64-bit atom requires 16 bytes. Therefore, QuickTime - // puts an 8-byte placeholder atom before any atoms it may have to update the size of. - // In this way, if the atom needs to be converted from a 32-bit to a 64-bit atom, the - // placeholder atom can be overwritten to obtain the necessary 8 extra bytes. - // The placeholder atom has a type of kWideAtomPlaceholderType ( 'wide' ). - break; - - - case 'nsav': // NoSAVe atom - // http://developer.apple.com/technotes/tn/tn2038.html - $atom_structure['data'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); - break; - - case 'ctyp': // Controller TYPe atom (seen on QTVR) - // http://homepages.slingshot.co.nz/~helmboy/quicktime/formats/qtm-layout.txt - // some controller names are: - // 0x00 + 'std' for linear movie - // 'none' for no controls - $atom_structure['ctyp'] = substr($atom_data, 0, 4); - $info['quicktime']['controller'] = $atom_structure['ctyp']; - switch ($atom_structure['ctyp']) { - case 'qtvr': - $info['video']['dataformat'] = 'quicktimevr'; - break; - } - break; - - case 'pano': // PANOrama track (seen on QTVR) - $atom_structure['pano'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 4)); - break; - - case 'hint': // HINT track - case 'hinf': // - case 'hinv': // - case 'hnti': // - $info['quicktime']['hinting'] = true; - break; - - case 'imgt': // IMaGe Track reference (kQTVRImageTrackRefType) (seen on QTVR) - for ($i = 0; $i < ($atom_structure['size'] - 8); $i += 4) { - $atom_structure['imgt'][] = getid3_lib::BigEndian2Int(substr($atom_data, $i, 4)); - } - break; - - - // Observed-but-not-handled atom types are just listed here to prevent warnings being generated - case 'FXTC': // Something to do with Adobe After Effects (?) - case 'PrmA': - case 'code': - case 'FIEL': // this is NOT "fiel" (Field Ordering) as describe here: http://developer.apple.com/documentation/QuickTime/QTFF/QTFFChap3/chapter_4_section_2.html - case 'tapt': // TrackApertureModeDimensionsAID - http://developer.apple.com/documentation/QuickTime/Reference/QT7-1_Update_Reference/Constants/Constants.html - // tapt seems to be used to compute the video size [http://www.getid3.org/phpBB3/viewtopic.php?t=838] - // * http://lists.apple.com/archives/quicktime-api/2006/Aug/msg00014.html - // * http://handbrake.fr/irclogs/handbrake-dev/handbrake-dev20080128_pg2.html - case 'ctts':// STCompositionOffsetAID - http://developer.apple.com/documentation/QuickTime/Reference/QTRef_Constants/Reference/reference.html - case 'cslg':// STCompositionShiftLeastGreatestAID - http://developer.apple.com/documentation/QuickTime/Reference/QTRef_Constants/Reference/reference.html - case 'sdtp':// STSampleDependencyAID - http://developer.apple.com/documentation/QuickTime/Reference/QTRef_Constants/Reference/reference.html - case 'stps':// STPartialSyncSampleAID - http://developer.apple.com/documentation/QuickTime/Reference/QTRef_Constants/Reference/reference.html - //$atom_structure['data'] = $atom_data; - break; - - case '©xyz': // GPS latitude+longitude+altitude - $atom_structure['data'] = $atom_data; - if (preg_match('#([\\+\\-][0-9\\.]+)([\\+\\-][0-9\\.]+)([\\+\\-][0-9\\.]+)?/$#i', $atom_data, $matches)) { - @list($all, $latitude, $longitude, $altitude) = $matches; - $info['quicktime']['comments']['gps_latitude'][] = floatval($latitude); - $info['quicktime']['comments']['gps_longitude'][] = floatval($longitude); - if (!empty($altitude)) { - $info['quicktime']['comments']['gps_altitude'][] = floatval($altitude); - } - } else { - $info['warning'][] = 'QuickTime atom "©xyz" data does not match expected data pattern at offset '.$baseoffset.'. Please report as getID3() bug.'; - } - break; - - case 'NCDT': - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html - // Nikon-specific QuickTime tags found in the NCDT atom of MOV videos from some Nikon cameras such as the Coolpix S8000 and D5100 - $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 4, $atomHierarchy, $ParseAllPossibleAtoms); - break; - case 'NCTH': // Nikon Camera THumbnail image - case 'NCVW': // Nikon Camera preVieW image - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html - if (preg_match('/^\xFF\xD8\xFF/', $atom_data)) { - $atom_structure['data'] = $atom_data; - $atom_structure['image_mime'] = 'image/jpeg'; - $atom_structure['description'] = (($atomname == 'NCTH') ? 'Nikon Camera Thumbnail Image' : (($atomname == 'NCVW') ? 'Nikon Camera Preview Image' : 'Nikon preview image')); - $info['quicktime']['comments']['picture'][] = array('image_mime'=>$atom_structure['image_mime'], 'data'=>$atom_data, 'description'=>$atom_structure['description']); - } - break; - case 'NCHD': // MakerNoteVersion - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html - $atom_structure['data'] = $atom_data; - break; - case 'NCTG': // NikonTags - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html#NCTG - $atom_structure['data'] = $this->QuicktimeParseNikonNCTG($atom_data); - break; - case 'NCDB': // NikonTags - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html - $atom_structure['data'] = $atom_data; - break; - - case "\x00\x00\x00\x00": - case 'meta': // METAdata atom - // some kind of metacontainer, may contain a big data dump such as: - // mdta keys  mdtacom.apple.quicktime.make (mdtacom.apple.quicktime.creationdate ,mdtacom.apple.quicktime.location.ISO6709 $mdtacom.apple.quicktime.software !mdtacom.apple.quicktime.model ilst   data DEApple 0  (data DE2011-05-11T17:54:04+0200 2  *data DE+52.4936+013.3897+040.247/   data DE4.3.1  data DEiPhone 4 - // http://www.geocities.com/xhelmboyx/quicktime/formats/qti-layout.txt - - $atom_structure['version'] = getid3_lib::BigEndian2Int(substr($atom_data, 0, 1)); - $atom_structure['flags_raw'] = getid3_lib::BigEndian2Int(substr($atom_data, 1, 3)); - $atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom(substr($atom_data, 4), $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); - //$atom_structure['subatoms'] = $this->QuicktimeParseContainerAtom($atom_data, $baseoffset + 8, $atomHierarchy, $ParseAllPossibleAtoms); - break; - - case 'data': // metaDATA atom - // seems to be 2 bytes language code (ASCII), 2 bytes unknown (set to 0x10B5 in sample I have), remainder is useful data - $atom_structure['language'] = substr($atom_data, 4 + 0, 2); - $atom_structure['unknown'] = getid3_lib::BigEndian2Int(substr($atom_data, 4 + 2, 2)); - $atom_structure['data'] = substr($atom_data, 4 + 4); - break; - - default: - $info['warning'][] = 'Unknown QuickTime atom type: "'.$atomname.'" ('.trim(getid3_lib::PrintHexBytes($atomname)).') at offset '.$baseoffset; - $atom_structure['data'] = $atom_data; - break; - } - array_pop($atomHierarchy); - return $atom_structure; - } - - public function QuicktimeParseContainerAtom($atom_data, $baseoffset, &$atomHierarchy, $ParseAllPossibleAtoms) { -//echo 'QuicktimeParseContainerAtom('.substr($atom_data, 4, 4).') @ '.$baseoffset.'

    '; - $atom_structure = false; - $subatomoffset = 0; - $subatomcounter = 0; - if ((strlen($atom_data) == 4) && (getid3_lib::BigEndian2Int($atom_data) == 0x00000000)) { - return false; - } - while ($subatomoffset < strlen($atom_data)) { - $subatomsize = getid3_lib::BigEndian2Int(substr($atom_data, $subatomoffset + 0, 4)); - $subatomname = substr($atom_data, $subatomoffset + 4, 4); - $subatomdata = substr($atom_data, $subatomoffset + 8, $subatomsize - 8); - if ($subatomsize == 0) { - // Furthermore, for historical reasons the list of atoms is optionally - // terminated by a 32-bit integer set to 0. If you are writing a program - // to read user data atoms, you should allow for the terminating 0. - return $atom_structure; - } - - $atom_structure[$subatomcounter] = $this->QuicktimeParseAtom($subatomname, $subatomsize, $subatomdata, $baseoffset + $subatomoffset, $atomHierarchy, $ParseAllPossibleAtoms); - - $subatomoffset += $subatomsize; - $subatomcounter++; - } - return $atom_structure; - } - - - public function quicktime_read_mp4_descr_length($data, &$offset) { - // http://libquicktime.sourcearchive.com/documentation/2:1.0.2plus-pdebian-2build1/esds_8c-source.html - $num_bytes = 0; - $length = 0; - do { - $b = ord(substr($data, $offset++, 1)); - $length = ($length << 7) | ($b & 0x7F); - } while (($b & 0x80) && ($num_bytes++ < 4)); - return $length; - } - - - public function QuicktimeLanguageLookup($languageid) { - static $QuicktimeLanguageLookup = array(); - if (empty($QuicktimeLanguageLookup)) { - $QuicktimeLanguageLookup[0] = 'English'; - $QuicktimeLanguageLookup[1] = 'French'; - $QuicktimeLanguageLookup[2] = 'German'; - $QuicktimeLanguageLookup[3] = 'Italian'; - $QuicktimeLanguageLookup[4] = 'Dutch'; - $QuicktimeLanguageLookup[5] = 'Swedish'; - $QuicktimeLanguageLookup[6] = 'Spanish'; - $QuicktimeLanguageLookup[7] = 'Danish'; - $QuicktimeLanguageLookup[8] = 'Portuguese'; - $QuicktimeLanguageLookup[9] = 'Norwegian'; - $QuicktimeLanguageLookup[10] = 'Hebrew'; - $QuicktimeLanguageLookup[11] = 'Japanese'; - $QuicktimeLanguageLookup[12] = 'Arabic'; - $QuicktimeLanguageLookup[13] = 'Finnish'; - $QuicktimeLanguageLookup[14] = 'Greek'; - $QuicktimeLanguageLookup[15] = 'Icelandic'; - $QuicktimeLanguageLookup[16] = 'Maltese'; - $QuicktimeLanguageLookup[17] = 'Turkish'; - $QuicktimeLanguageLookup[18] = 'Croatian'; - $QuicktimeLanguageLookup[19] = 'Chinese (Traditional)'; - $QuicktimeLanguageLookup[20] = 'Urdu'; - $QuicktimeLanguageLookup[21] = 'Hindi'; - $QuicktimeLanguageLookup[22] = 'Thai'; - $QuicktimeLanguageLookup[23] = 'Korean'; - $QuicktimeLanguageLookup[24] = 'Lithuanian'; - $QuicktimeLanguageLookup[25] = 'Polish'; - $QuicktimeLanguageLookup[26] = 'Hungarian'; - $QuicktimeLanguageLookup[27] = 'Estonian'; - $QuicktimeLanguageLookup[28] = 'Lettish'; - $QuicktimeLanguageLookup[28] = 'Latvian'; - $QuicktimeLanguageLookup[29] = 'Saamisk'; - $QuicktimeLanguageLookup[29] = 'Lappish'; - $QuicktimeLanguageLookup[30] = 'Faeroese'; - $QuicktimeLanguageLookup[31] = 'Farsi'; - $QuicktimeLanguageLookup[31] = 'Persian'; - $QuicktimeLanguageLookup[32] = 'Russian'; - $QuicktimeLanguageLookup[33] = 'Chinese (Simplified)'; - $QuicktimeLanguageLookup[34] = 'Flemish'; - $QuicktimeLanguageLookup[35] = 'Irish'; - $QuicktimeLanguageLookup[36] = 'Albanian'; - $QuicktimeLanguageLookup[37] = 'Romanian'; - $QuicktimeLanguageLookup[38] = 'Czech'; - $QuicktimeLanguageLookup[39] = 'Slovak'; - $QuicktimeLanguageLookup[40] = 'Slovenian'; - $QuicktimeLanguageLookup[41] = 'Yiddish'; - $QuicktimeLanguageLookup[42] = 'Serbian'; - $QuicktimeLanguageLookup[43] = 'Macedonian'; - $QuicktimeLanguageLookup[44] = 'Bulgarian'; - $QuicktimeLanguageLookup[45] = 'Ukrainian'; - $QuicktimeLanguageLookup[46] = 'Byelorussian'; - $QuicktimeLanguageLookup[47] = 'Uzbek'; - $QuicktimeLanguageLookup[48] = 'Kazakh'; - $QuicktimeLanguageLookup[49] = 'Azerbaijani'; - $QuicktimeLanguageLookup[50] = 'AzerbaijanAr'; - $QuicktimeLanguageLookup[51] = 'Armenian'; - $QuicktimeLanguageLookup[52] = 'Georgian'; - $QuicktimeLanguageLookup[53] = 'Moldavian'; - $QuicktimeLanguageLookup[54] = 'Kirghiz'; - $QuicktimeLanguageLookup[55] = 'Tajiki'; - $QuicktimeLanguageLookup[56] = 'Turkmen'; - $QuicktimeLanguageLookup[57] = 'Mongolian'; - $QuicktimeLanguageLookup[58] = 'MongolianCyr'; - $QuicktimeLanguageLookup[59] = 'Pashto'; - $QuicktimeLanguageLookup[60] = 'Kurdish'; - $QuicktimeLanguageLookup[61] = 'Kashmiri'; - $QuicktimeLanguageLookup[62] = 'Sindhi'; - $QuicktimeLanguageLookup[63] = 'Tibetan'; - $QuicktimeLanguageLookup[64] = 'Nepali'; - $QuicktimeLanguageLookup[65] = 'Sanskrit'; - $QuicktimeLanguageLookup[66] = 'Marathi'; - $QuicktimeLanguageLookup[67] = 'Bengali'; - $QuicktimeLanguageLookup[68] = 'Assamese'; - $QuicktimeLanguageLookup[69] = 'Gujarati'; - $QuicktimeLanguageLookup[70] = 'Punjabi'; - $QuicktimeLanguageLookup[71] = 'Oriya'; - $QuicktimeLanguageLookup[72] = 'Malayalam'; - $QuicktimeLanguageLookup[73] = 'Kannada'; - $QuicktimeLanguageLookup[74] = 'Tamil'; - $QuicktimeLanguageLookup[75] = 'Telugu'; - $QuicktimeLanguageLookup[76] = 'Sinhalese'; - $QuicktimeLanguageLookup[77] = 'Burmese'; - $QuicktimeLanguageLookup[78] = 'Khmer'; - $QuicktimeLanguageLookup[79] = 'Lao'; - $QuicktimeLanguageLookup[80] = 'Vietnamese'; - $QuicktimeLanguageLookup[81] = 'Indonesian'; - $QuicktimeLanguageLookup[82] = 'Tagalog'; - $QuicktimeLanguageLookup[83] = 'MalayRoman'; - $QuicktimeLanguageLookup[84] = 'MalayArabic'; - $QuicktimeLanguageLookup[85] = 'Amharic'; - $QuicktimeLanguageLookup[86] = 'Tigrinya'; - $QuicktimeLanguageLookup[87] = 'Galla'; - $QuicktimeLanguageLookup[87] = 'Oromo'; - $QuicktimeLanguageLookup[88] = 'Somali'; - $QuicktimeLanguageLookup[89] = 'Swahili'; - $QuicktimeLanguageLookup[90] = 'Ruanda'; - $QuicktimeLanguageLookup[91] = 'Rundi'; - $QuicktimeLanguageLookup[92] = 'Chewa'; - $QuicktimeLanguageLookup[93] = 'Malagasy'; - $QuicktimeLanguageLookup[94] = 'Esperanto'; - $QuicktimeLanguageLookup[128] = 'Welsh'; - $QuicktimeLanguageLookup[129] = 'Basque'; - $QuicktimeLanguageLookup[130] = 'Catalan'; - $QuicktimeLanguageLookup[131] = 'Latin'; - $QuicktimeLanguageLookup[132] = 'Quechua'; - $QuicktimeLanguageLookup[133] = 'Guarani'; - $QuicktimeLanguageLookup[134] = 'Aymara'; - $QuicktimeLanguageLookup[135] = 'Tatar'; - $QuicktimeLanguageLookup[136] = 'Uighur'; - $QuicktimeLanguageLookup[137] = 'Dzongkha'; - $QuicktimeLanguageLookup[138] = 'JavaneseRom'; - } - return (isset($QuicktimeLanguageLookup[$languageid]) ? $QuicktimeLanguageLookup[$languageid] : 'invalid'); - } - - public function QuicktimeVideoCodecLookup($codecid) { - static $QuicktimeVideoCodecLookup = array(); - if (empty($QuicktimeVideoCodecLookup)) { - $QuicktimeVideoCodecLookup['.SGI'] = 'SGI'; - $QuicktimeVideoCodecLookup['3IV1'] = '3ivx MPEG-4 v1'; - $QuicktimeVideoCodecLookup['3IV2'] = '3ivx MPEG-4 v2'; - $QuicktimeVideoCodecLookup['3IVX'] = '3ivx MPEG-4'; - $QuicktimeVideoCodecLookup['8BPS'] = 'Planar RGB'; - $QuicktimeVideoCodecLookup['avc1'] = 'H.264/MPEG-4 AVC'; - $QuicktimeVideoCodecLookup['avr '] = 'AVR-JPEG'; - $QuicktimeVideoCodecLookup['b16g'] = '16Gray'; - $QuicktimeVideoCodecLookup['b32a'] = '32AlphaGray'; - $QuicktimeVideoCodecLookup['b48r'] = '48RGB'; - $QuicktimeVideoCodecLookup['b64a'] = '64ARGB'; - $QuicktimeVideoCodecLookup['base'] = 'Base'; - $QuicktimeVideoCodecLookup['clou'] = 'Cloud'; - $QuicktimeVideoCodecLookup['cmyk'] = 'CMYK'; - $QuicktimeVideoCodecLookup['cvid'] = 'Cinepak'; - $QuicktimeVideoCodecLookup['dmb1'] = 'OpenDML JPEG'; - $QuicktimeVideoCodecLookup['dvc '] = 'DVC-NTSC'; - $QuicktimeVideoCodecLookup['dvcp'] = 'DVC-PAL'; - $QuicktimeVideoCodecLookup['dvpn'] = 'DVCPro-NTSC'; - $QuicktimeVideoCodecLookup['dvpp'] = 'DVCPro-PAL'; - $QuicktimeVideoCodecLookup['fire'] = 'Fire'; - $QuicktimeVideoCodecLookup['flic'] = 'FLC'; - $QuicktimeVideoCodecLookup['gif '] = 'GIF'; - $QuicktimeVideoCodecLookup['h261'] = 'H261'; - $QuicktimeVideoCodecLookup['h263'] = 'H263'; - $QuicktimeVideoCodecLookup['IV41'] = 'Indeo4'; - $QuicktimeVideoCodecLookup['jpeg'] = 'JPEG'; - $QuicktimeVideoCodecLookup['kpcd'] = 'PhotoCD'; - $QuicktimeVideoCodecLookup['mjpa'] = 'Motion JPEG-A'; - $QuicktimeVideoCodecLookup['mjpb'] = 'Motion JPEG-B'; - $QuicktimeVideoCodecLookup['msvc'] = 'Microsoft Video1'; - $QuicktimeVideoCodecLookup['myuv'] = 'MPEG YUV420'; - $QuicktimeVideoCodecLookup['path'] = 'Vector'; - $QuicktimeVideoCodecLookup['png '] = 'PNG'; - $QuicktimeVideoCodecLookup['PNTG'] = 'MacPaint'; - $QuicktimeVideoCodecLookup['qdgx'] = 'QuickDrawGX'; - $QuicktimeVideoCodecLookup['qdrw'] = 'QuickDraw'; - $QuicktimeVideoCodecLookup['raw '] = 'RAW'; - $QuicktimeVideoCodecLookup['ripl'] = 'WaterRipple'; - $QuicktimeVideoCodecLookup['rpza'] = 'Video'; - $QuicktimeVideoCodecLookup['smc '] = 'Graphics'; - $QuicktimeVideoCodecLookup['SVQ1'] = 'Sorenson Video 1'; - $QuicktimeVideoCodecLookup['SVQ1'] = 'Sorenson Video 3'; - $QuicktimeVideoCodecLookup['syv9'] = 'Sorenson YUV9'; - $QuicktimeVideoCodecLookup['tga '] = 'Targa'; - $QuicktimeVideoCodecLookup['tiff'] = 'TIFF'; - $QuicktimeVideoCodecLookup['WRAW'] = 'Windows RAW'; - $QuicktimeVideoCodecLookup['WRLE'] = 'BMP'; - $QuicktimeVideoCodecLookup['y420'] = 'YUV420'; - $QuicktimeVideoCodecLookup['yuv2'] = 'ComponentVideo'; - $QuicktimeVideoCodecLookup['yuvs'] = 'ComponentVideoUnsigned'; - $QuicktimeVideoCodecLookup['yuvu'] = 'ComponentVideoSigned'; - } - return (isset($QuicktimeVideoCodecLookup[$codecid]) ? $QuicktimeVideoCodecLookup[$codecid] : ''); - } - - public function QuicktimeAudioCodecLookup($codecid) { - static $QuicktimeAudioCodecLookup = array(); - if (empty($QuicktimeAudioCodecLookup)) { - $QuicktimeAudioCodecLookup['.mp3'] = 'Fraunhofer MPEG Layer-III alias'; - $QuicktimeAudioCodecLookup['aac '] = 'ISO/IEC 14496-3 AAC'; - $QuicktimeAudioCodecLookup['agsm'] = 'Apple GSM 10:1'; - $QuicktimeAudioCodecLookup['alac'] = 'Apple Lossless Audio Codec'; - $QuicktimeAudioCodecLookup['alaw'] = 'A-law 2:1'; - $QuicktimeAudioCodecLookup['conv'] = 'Sample Format'; - $QuicktimeAudioCodecLookup['dvca'] = 'DV'; - $QuicktimeAudioCodecLookup['dvi '] = 'DV 4:1'; - $QuicktimeAudioCodecLookup['eqal'] = 'Frequency Equalizer'; - $QuicktimeAudioCodecLookup['fl32'] = '32-bit Floating Point'; - $QuicktimeAudioCodecLookup['fl64'] = '64-bit Floating Point'; - $QuicktimeAudioCodecLookup['ima4'] = 'Interactive Multimedia Association 4:1'; - $QuicktimeAudioCodecLookup['in24'] = '24-bit Integer'; - $QuicktimeAudioCodecLookup['in32'] = '32-bit Integer'; - $QuicktimeAudioCodecLookup['lpc '] = 'LPC 23:1'; - $QuicktimeAudioCodecLookup['MAC3'] = 'Macintosh Audio Compression/Expansion (MACE) 3:1'; - $QuicktimeAudioCodecLookup['MAC6'] = 'Macintosh Audio Compression/Expansion (MACE) 6:1'; - $QuicktimeAudioCodecLookup['mixb'] = '8-bit Mixer'; - $QuicktimeAudioCodecLookup['mixw'] = '16-bit Mixer'; - $QuicktimeAudioCodecLookup['mp4a'] = 'ISO/IEC 14496-3 AAC'; - $QuicktimeAudioCodecLookup['MS'."\x00\x02"] = 'Microsoft ADPCM'; - $QuicktimeAudioCodecLookup['MS'."\x00\x11"] = 'DV IMA'; - $QuicktimeAudioCodecLookup['MS'."\x00\x55"] = 'Fraunhofer MPEG Layer III'; - $QuicktimeAudioCodecLookup['NONE'] = 'No Encoding'; - $QuicktimeAudioCodecLookup['Qclp'] = 'Qualcomm PureVoice'; - $QuicktimeAudioCodecLookup['QDM2'] = 'QDesign Music 2'; - $QuicktimeAudioCodecLookup['QDMC'] = 'QDesign Music 1'; - $QuicktimeAudioCodecLookup['ratb'] = '8-bit Rate'; - $QuicktimeAudioCodecLookup['ratw'] = '16-bit Rate'; - $QuicktimeAudioCodecLookup['raw '] = 'raw PCM'; - $QuicktimeAudioCodecLookup['sour'] = 'Sound Source'; - $QuicktimeAudioCodecLookup['sowt'] = 'signed/two\'s complement (Little Endian)'; - $QuicktimeAudioCodecLookup['str1'] = 'Iomega MPEG layer II'; - $QuicktimeAudioCodecLookup['str2'] = 'Iomega MPEG *layer II'; - $QuicktimeAudioCodecLookup['str3'] = 'Iomega MPEG **layer II'; - $QuicktimeAudioCodecLookup['str4'] = 'Iomega MPEG ***layer II'; - $QuicktimeAudioCodecLookup['twos'] = 'signed/two\'s complement (Big Endian)'; - $QuicktimeAudioCodecLookup['ulaw'] = 'mu-law 2:1'; - } - return (isset($QuicktimeAudioCodecLookup[$codecid]) ? $QuicktimeAudioCodecLookup[$codecid] : ''); - } - - public function QuicktimeDCOMLookup($compressionid) { - static $QuicktimeDCOMLookup = array(); - if (empty($QuicktimeDCOMLookup)) { - $QuicktimeDCOMLookup['zlib'] = 'ZLib Deflate'; - $QuicktimeDCOMLookup['adec'] = 'Apple Compression'; - } - return (isset($QuicktimeDCOMLookup[$compressionid]) ? $QuicktimeDCOMLookup[$compressionid] : ''); - } - - public function QuicktimeColorNameLookup($colordepthid) { - static $QuicktimeColorNameLookup = array(); - if (empty($QuicktimeColorNameLookup)) { - $QuicktimeColorNameLookup[1] = '2-color (monochrome)'; - $QuicktimeColorNameLookup[2] = '4-color'; - $QuicktimeColorNameLookup[4] = '16-color'; - $QuicktimeColorNameLookup[8] = '256-color'; - $QuicktimeColorNameLookup[16] = 'thousands (16-bit color)'; - $QuicktimeColorNameLookup[24] = 'millions (24-bit color)'; - $QuicktimeColorNameLookup[32] = 'millions+ (32-bit color)'; - $QuicktimeColorNameLookup[33] = 'black & white'; - $QuicktimeColorNameLookup[34] = '4-gray'; - $QuicktimeColorNameLookup[36] = '16-gray'; - $QuicktimeColorNameLookup[40] = '256-gray'; - } - return (isset($QuicktimeColorNameLookup[$colordepthid]) ? $QuicktimeColorNameLookup[$colordepthid] : 'invalid'); - } - - public function QuicktimeSTIKLookup($stik) { - static $QuicktimeSTIKLookup = array(); - if (empty($QuicktimeSTIKLookup)) { - $QuicktimeSTIKLookup[0] = 'Movie'; - $QuicktimeSTIKLookup[1] = 'Normal'; - $QuicktimeSTIKLookup[2] = 'Audiobook'; - $QuicktimeSTIKLookup[5] = 'Whacked Bookmark'; - $QuicktimeSTIKLookup[6] = 'Music Video'; - $QuicktimeSTIKLookup[9] = 'Short Film'; - $QuicktimeSTIKLookup[10] = 'TV Show'; - $QuicktimeSTIKLookup[11] = 'Booklet'; - $QuicktimeSTIKLookup[14] = 'Ringtone'; - $QuicktimeSTIKLookup[21] = 'Podcast'; - } - return (isset($QuicktimeSTIKLookup[$stik]) ? $QuicktimeSTIKLookup[$stik] : 'invalid'); - } - - public function QuicktimeIODSaudioProfileName($audio_profile_id) { - static $QuicktimeIODSaudioProfileNameLookup = array(); - if (empty($QuicktimeIODSaudioProfileNameLookup)) { - $QuicktimeIODSaudioProfileNameLookup = array( - 0x00 => 'ISO Reserved (0x00)', - 0x01 => 'Main Audio Profile @ Level 1', - 0x02 => 'Main Audio Profile @ Level 2', - 0x03 => 'Main Audio Profile @ Level 3', - 0x04 => 'Main Audio Profile @ Level 4', - 0x05 => 'Scalable Audio Profile @ Level 1', - 0x06 => 'Scalable Audio Profile @ Level 2', - 0x07 => 'Scalable Audio Profile @ Level 3', - 0x08 => 'Scalable Audio Profile @ Level 4', - 0x09 => 'Speech Audio Profile @ Level 1', - 0x0A => 'Speech Audio Profile @ Level 2', - 0x0B => 'Synthetic Audio Profile @ Level 1', - 0x0C => 'Synthetic Audio Profile @ Level 2', - 0x0D => 'Synthetic Audio Profile @ Level 3', - 0x0E => 'High Quality Audio Profile @ Level 1', - 0x0F => 'High Quality Audio Profile @ Level 2', - 0x10 => 'High Quality Audio Profile @ Level 3', - 0x11 => 'High Quality Audio Profile @ Level 4', - 0x12 => 'High Quality Audio Profile @ Level 5', - 0x13 => 'High Quality Audio Profile @ Level 6', - 0x14 => 'High Quality Audio Profile @ Level 7', - 0x15 => 'High Quality Audio Profile @ Level 8', - 0x16 => 'Low Delay Audio Profile @ Level 1', - 0x17 => 'Low Delay Audio Profile @ Level 2', - 0x18 => 'Low Delay Audio Profile @ Level 3', - 0x19 => 'Low Delay Audio Profile @ Level 4', - 0x1A => 'Low Delay Audio Profile @ Level 5', - 0x1B => 'Low Delay Audio Profile @ Level 6', - 0x1C => 'Low Delay Audio Profile @ Level 7', - 0x1D => 'Low Delay Audio Profile @ Level 8', - 0x1E => 'Natural Audio Profile @ Level 1', - 0x1F => 'Natural Audio Profile @ Level 2', - 0x20 => 'Natural Audio Profile @ Level 3', - 0x21 => 'Natural Audio Profile @ Level 4', - 0x22 => 'Mobile Audio Internetworking Profile @ Level 1', - 0x23 => 'Mobile Audio Internetworking Profile @ Level 2', - 0x24 => 'Mobile Audio Internetworking Profile @ Level 3', - 0x25 => 'Mobile Audio Internetworking Profile @ Level 4', - 0x26 => 'Mobile Audio Internetworking Profile @ Level 5', - 0x27 => 'Mobile Audio Internetworking Profile @ Level 6', - 0x28 => 'AAC Profile @ Level 1', - 0x29 => 'AAC Profile @ Level 2', - 0x2A => 'AAC Profile @ Level 4', - 0x2B => 'AAC Profile @ Level 5', - 0x2C => 'High Efficiency AAC Profile @ Level 2', - 0x2D => 'High Efficiency AAC Profile @ Level 3', - 0x2E => 'High Efficiency AAC Profile @ Level 4', - 0x2F => 'High Efficiency AAC Profile @ Level 5', - 0xFE => 'Not part of MPEG-4 audio profiles', - 0xFF => 'No audio capability required', - ); - } - return (isset($QuicktimeIODSaudioProfileNameLookup[$audio_profile_id]) ? $QuicktimeIODSaudioProfileNameLookup[$audio_profile_id] : 'ISO Reserved / User Private'); - } - - - public function QuicktimeIODSvideoProfileName($video_profile_id) { - static $QuicktimeIODSvideoProfileNameLookup = array(); - if (empty($QuicktimeIODSvideoProfileNameLookup)) { - $QuicktimeIODSvideoProfileNameLookup = array( - 0x00 => 'Reserved (0x00) Profile', - 0x01 => 'Simple Profile @ Level 1', - 0x02 => 'Simple Profile @ Level 2', - 0x03 => 'Simple Profile @ Level 3', - 0x08 => 'Simple Profile @ Level 0', - 0x10 => 'Simple Scalable Profile @ Level 0', - 0x11 => 'Simple Scalable Profile @ Level 1', - 0x12 => 'Simple Scalable Profile @ Level 2', - 0x15 => 'AVC/H264 Profile', - 0x21 => 'Core Profile @ Level 1', - 0x22 => 'Core Profile @ Level 2', - 0x32 => 'Main Profile @ Level 2', - 0x33 => 'Main Profile @ Level 3', - 0x34 => 'Main Profile @ Level 4', - 0x42 => 'N-bit Profile @ Level 2', - 0x51 => 'Scalable Texture Profile @ Level 1', - 0x61 => 'Simple Face Animation Profile @ Level 1', - 0x62 => 'Simple Face Animation Profile @ Level 2', - 0x63 => 'Simple FBA Profile @ Level 1', - 0x64 => 'Simple FBA Profile @ Level 2', - 0x71 => 'Basic Animated Texture Profile @ Level 1', - 0x72 => 'Basic Animated Texture Profile @ Level 2', - 0x81 => 'Hybrid Profile @ Level 1', - 0x82 => 'Hybrid Profile @ Level 2', - 0x91 => 'Advanced Real Time Simple Profile @ Level 1', - 0x92 => 'Advanced Real Time Simple Profile @ Level 2', - 0x93 => 'Advanced Real Time Simple Profile @ Level 3', - 0x94 => 'Advanced Real Time Simple Profile @ Level 4', - 0xA1 => 'Core Scalable Profile @ Level1', - 0xA2 => 'Core Scalable Profile @ Level2', - 0xA3 => 'Core Scalable Profile @ Level3', - 0xB1 => 'Advanced Coding Efficiency Profile @ Level 1', - 0xB2 => 'Advanced Coding Efficiency Profile @ Level 2', - 0xB3 => 'Advanced Coding Efficiency Profile @ Level 3', - 0xB4 => 'Advanced Coding Efficiency Profile @ Level 4', - 0xC1 => 'Advanced Core Profile @ Level 1', - 0xC2 => 'Advanced Core Profile @ Level 2', - 0xD1 => 'Advanced Scalable Texture @ Level1', - 0xD2 => 'Advanced Scalable Texture @ Level2', - 0xE1 => 'Simple Studio Profile @ Level 1', - 0xE2 => 'Simple Studio Profile @ Level 2', - 0xE3 => 'Simple Studio Profile @ Level 3', - 0xE4 => 'Simple Studio Profile @ Level 4', - 0xE5 => 'Core Studio Profile @ Level 1', - 0xE6 => 'Core Studio Profile @ Level 2', - 0xE7 => 'Core Studio Profile @ Level 3', - 0xE8 => 'Core Studio Profile @ Level 4', - 0xF0 => 'Advanced Simple Profile @ Level 0', - 0xF1 => 'Advanced Simple Profile @ Level 1', - 0xF2 => 'Advanced Simple Profile @ Level 2', - 0xF3 => 'Advanced Simple Profile @ Level 3', - 0xF4 => 'Advanced Simple Profile @ Level 4', - 0xF5 => 'Advanced Simple Profile @ Level 5', - 0xF7 => 'Advanced Simple Profile @ Level 3b', - 0xF8 => 'Fine Granularity Scalable Profile @ Level 0', - 0xF9 => 'Fine Granularity Scalable Profile @ Level 1', - 0xFA => 'Fine Granularity Scalable Profile @ Level 2', - 0xFB => 'Fine Granularity Scalable Profile @ Level 3', - 0xFC => 'Fine Granularity Scalable Profile @ Level 4', - 0xFD => 'Fine Granularity Scalable Profile @ Level 5', - 0xFE => 'Not part of MPEG-4 Visual profiles', - 0xFF => 'No visual capability required', - ); - } - return (isset($QuicktimeIODSvideoProfileNameLookup[$video_profile_id]) ? $QuicktimeIODSvideoProfileNameLookup[$video_profile_id] : 'ISO Reserved Profile'); - } - - - public function QuicktimeContentRatingLookup($rtng) { - static $QuicktimeContentRatingLookup = array(); - if (empty($QuicktimeContentRatingLookup)) { - $QuicktimeContentRatingLookup[0] = 'None'; - $QuicktimeContentRatingLookup[2] = 'Clean'; - $QuicktimeContentRatingLookup[4] = 'Explicit'; - } - return (isset($QuicktimeContentRatingLookup[$rtng]) ? $QuicktimeContentRatingLookup[$rtng] : 'invalid'); - } - - public function QuicktimeStoreAccountTypeLookup($akid) { - static $QuicktimeStoreAccountTypeLookup = array(); - if (empty($QuicktimeStoreAccountTypeLookup)) { - $QuicktimeStoreAccountTypeLookup[0] = 'iTunes'; - $QuicktimeStoreAccountTypeLookup[1] = 'AOL'; - } - return (isset($QuicktimeStoreAccountTypeLookup[$akid]) ? $QuicktimeStoreAccountTypeLookup[$akid] : 'invalid'); - } - - public function QuicktimeStoreFrontCodeLookup($sfid) { - static $QuicktimeStoreFrontCodeLookup = array(); - if (empty($QuicktimeStoreFrontCodeLookup)) { - $QuicktimeStoreFrontCodeLookup[143460] = 'Australia'; - $QuicktimeStoreFrontCodeLookup[143445] = 'Austria'; - $QuicktimeStoreFrontCodeLookup[143446] = 'Belgium'; - $QuicktimeStoreFrontCodeLookup[143455] = 'Canada'; - $QuicktimeStoreFrontCodeLookup[143458] = 'Denmark'; - $QuicktimeStoreFrontCodeLookup[143447] = 'Finland'; - $QuicktimeStoreFrontCodeLookup[143442] = 'France'; - $QuicktimeStoreFrontCodeLookup[143443] = 'Germany'; - $QuicktimeStoreFrontCodeLookup[143448] = 'Greece'; - $QuicktimeStoreFrontCodeLookup[143449] = 'Ireland'; - $QuicktimeStoreFrontCodeLookup[143450] = 'Italy'; - $QuicktimeStoreFrontCodeLookup[143462] = 'Japan'; - $QuicktimeStoreFrontCodeLookup[143451] = 'Luxembourg'; - $QuicktimeStoreFrontCodeLookup[143452] = 'Netherlands'; - $QuicktimeStoreFrontCodeLookup[143461] = 'New Zealand'; - $QuicktimeStoreFrontCodeLookup[143457] = 'Norway'; - $QuicktimeStoreFrontCodeLookup[143453] = 'Portugal'; - $QuicktimeStoreFrontCodeLookup[143454] = 'Spain'; - $QuicktimeStoreFrontCodeLookup[143456] = 'Sweden'; - $QuicktimeStoreFrontCodeLookup[143459] = 'Switzerland'; - $QuicktimeStoreFrontCodeLookup[143444] = 'United Kingdom'; - $QuicktimeStoreFrontCodeLookup[143441] = 'United States'; - } - return (isset($QuicktimeStoreFrontCodeLookup[$sfid]) ? $QuicktimeStoreFrontCodeLookup[$sfid] : 'invalid'); - } - - public function QuicktimeParseNikonNCTG($atom_data) { - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html#NCTG - // Nikon-specific QuickTime tags found in the NCDT atom of MOV videos from some Nikon cameras such as the Coolpix S8000 and D5100 - // Data is stored as records of: - // * 4 bytes record type - // * 2 bytes size of data field type: - // 0x0001 = flag (size field *= 1-byte) - // 0x0002 = char (size field *= 1-byte) - // 0x0003 = DWORD+ (size field *= 2-byte), values are stored CDAB - // 0x0004 = QWORD+ (size field *= 4-byte), values are stored EFGHABCD - // 0x0005 = float (size field *= 8-byte), values are stored aaaabbbb where value is aaaa/bbbb; possibly multiple sets of values appended together - // 0x0007 = bytes (size field *= 1-byte), values are stored as ?????? - // 0x0008 = ????? (size field *= 2-byte), values are stored as ?????? - // * 2 bytes data size field - // * ? bytes data (string data may be null-padded; datestamp fields are in the format "2011:05:25 20:24:15") - // all integers are stored BigEndian - - $NCTGtagName = array( - 0x00000001 => 'Make', - 0x00000002 => 'Model', - 0x00000003 => 'Software', - 0x00000011 => 'CreateDate', - 0x00000012 => 'DateTimeOriginal', - 0x00000013 => 'FrameCount', - 0x00000016 => 'FrameRate', - 0x00000022 => 'FrameWidth', - 0x00000023 => 'FrameHeight', - 0x00000032 => 'AudioChannels', - 0x00000033 => 'AudioBitsPerSample', - 0x00000034 => 'AudioSampleRate', - 0x02000001 => 'MakerNoteVersion', - 0x02000005 => 'WhiteBalance', - 0x0200000b => 'WhiteBalanceFineTune', - 0x0200001e => 'ColorSpace', - 0x02000023 => 'PictureControlData', - 0x02000024 => 'WorldTime', - 0x02000032 => 'UnknownInfo', - 0x02000083 => 'LensType', - 0x02000084 => 'Lens', - ); - - $offset = 0; - $datalength = strlen($atom_data); - $parsed = array(); - while ($offset < $datalength) { -//echo getid3_lib::PrintHexBytes(substr($atom_data, $offset, 4)).'
    '; - $record_type = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 4)); $offset += 4; - $data_size_type = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 2)); $offset += 2; - $data_size = getid3_lib::BigEndian2Int(substr($atom_data, $offset, 2)); $offset += 2; - switch ($data_size_type) { - case 0x0001: // 0x0001 = flag (size field *= 1-byte) - $data = getid3_lib::BigEndian2Int(substr($atom_data, $offset, $data_size * 1)); - $offset += ($data_size * 1); - break; - case 0x0002: // 0x0002 = char (size field *= 1-byte) - $data = substr($atom_data, $offset, $data_size * 1); - $offset += ($data_size * 1); - $data = rtrim($data, "\x00"); - break; - case 0x0003: // 0x0003 = DWORD+ (size field *= 2-byte), values are stored CDAB - $data = ''; - for ($i = $data_size - 1; $i >= 0; $i--) { - $data .= substr($atom_data, $offset + ($i * 2), 2); - } - $data = getid3_lib::BigEndian2Int($data); - $offset += ($data_size * 2); - break; - case 0x0004: // 0x0004 = QWORD+ (size field *= 4-byte), values are stored EFGHABCD - $data = ''; - for ($i = $data_size - 1; $i >= 0; $i--) { - $data .= substr($atom_data, $offset + ($i * 4), 4); - } - $data = getid3_lib::BigEndian2Int($data); - $offset += ($data_size * 4); - break; - case 0x0005: // 0x0005 = float (size field *= 8-byte), values are stored aaaabbbb where value is aaaa/bbbb; possibly multiple sets of values appended together - $data = array(); - for ($i = 0; $i < $data_size; $i++) { - $numerator = getid3_lib::BigEndian2Int(substr($atom_data, $offset + ($i * 8) + 0, 4)); - $denomninator = getid3_lib::BigEndian2Int(substr($atom_data, $offset + ($i * 8) + 4, 4)); - if ($denomninator == 0) { - $data[$i] = false; - } else { - $data[$i] = (double) $numerator / $denomninator; - } - } - $offset += (8 * $data_size); - if (count($data) == 1) { - $data = $data[0]; - } - break; - case 0x0007: // 0x0007 = bytes (size field *= 1-byte), values are stored as ?????? - $data = substr($atom_data, $offset, $data_size * 1); - $offset += ($data_size * 1); - break; - case 0x0008: // 0x0008 = ????? (size field *= 2-byte), values are stored as ?????? - $data = substr($atom_data, $offset, $data_size * 2); - $offset += ($data_size * 2); - break; - default: -echo 'QuicktimeParseNikonNCTG()::unknown $data_size_type: '.$data_size_type.'
    '; - break 2; - } - - switch ($record_type) { - case 0x00000011: // CreateDate - case 0x00000012: // DateTimeOriginal - $data = strtotime($data); - break; - case 0x0200001e: // ColorSpace - switch ($data) { - case 1: - $data = 'sRGB'; - break; - case 2: - $data = 'Adobe RGB'; - break; - } - break; - case 0x02000023: // PictureControlData - $PictureControlAdjust = array(0=>'default', 1=>'quick', 2=>'full'); - $FilterEffect = array(0x80=>'off', 0x81=>'yellow', 0x82=>'orange', 0x83=>'red', 0x84=>'green', 0xff=>'n/a'); - $ToningEffect = array(0x80=>'b&w', 0x81=>'sepia', 0x82=>'cyanotype', 0x83=>'red', 0x84=>'yellow', 0x85=>'green', 0x86=>'blue-green', 0x87=>'blue', 0x88=>'purple-blue', 0x89=>'red-purple', 0xff=>'n/a'); - $data = array( - 'PictureControlVersion' => substr($data, 0, 4), - 'PictureControlName' => rtrim(substr($data, 4, 20), "\x00"), - 'PictureControlBase' => rtrim(substr($data, 24, 20), "\x00"), - //'?' => substr($data, 44, 4), - 'PictureControlAdjust' => $PictureControlAdjust[ord(substr($data, 48, 1))], - 'PictureControlQuickAdjust' => ord(substr($data, 49, 1)), - 'Sharpness' => ord(substr($data, 50, 1)), - 'Contrast' => ord(substr($data, 51, 1)), - 'Brightness' => ord(substr($data, 52, 1)), - 'Saturation' => ord(substr($data, 53, 1)), - 'HueAdjustment' => ord(substr($data, 54, 1)), - 'FilterEffect' => $FilterEffect[ord(substr($data, 55, 1))], - 'ToningEffect' => $ToningEffect[ord(substr($data, 56, 1))], - 'ToningSaturation' => ord(substr($data, 57, 1)), - ); - break; - case 0x02000024: // WorldTime - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/Nikon.html#WorldTime - // timezone is stored as offset from GMT in minutes - $timezone = getid3_lib::BigEndian2Int(substr($data, 0, 2)); - if ($timezone & 0x8000) { - $timezone = 0 - (0x10000 - $timezone); - } - $timezone /= 60; - - $dst = (bool) getid3_lib::BigEndian2Int(substr($data, 2, 1)); - switch (getid3_lib::BigEndian2Int(substr($data, 3, 1))) { - case 2: - $datedisplayformat = 'D/M/Y'; break; - case 1: - $datedisplayformat = 'M/D/Y'; break; - case 0: - default: - $datedisplayformat = 'Y/M/D'; break; - } - - $data = array('timezone'=>floatval($timezone), 'dst'=>$dst, 'display'=>$datedisplayformat); - break; - case 0x02000083: // LensType - $data = array( - //'_' => $data, - 'mf' => (bool) ($data & 0x01), - 'd' => (bool) ($data & 0x02), - 'g' => (bool) ($data & 0x04), - 'vr' => (bool) ($data & 0x08), - ); - break; - } - $tag_name = (isset($NCTGtagName[$record_type]) ? $NCTGtagName[$record_type] : '0x'.str_pad(dechex($record_type), 8, '0', STR_PAD_LEFT)); - $parsed[$tag_name] = $data; - } - return $parsed; - } - - - public function CopyToAppropriateCommentsSection($keyname, $data, $boxname='') { - static $handyatomtranslatorarray = array(); - if (empty($handyatomtranslatorarray)) { - $handyatomtranslatorarray['©cpy'] = 'copyright'; - $handyatomtranslatorarray['©day'] = 'creation_date'; // iTunes 4.0 - $handyatomtranslatorarray['©dir'] = 'director'; - $handyatomtranslatorarray['©ed1'] = 'edit1'; - $handyatomtranslatorarray['©ed2'] = 'edit2'; - $handyatomtranslatorarray['©ed3'] = 'edit3'; - $handyatomtranslatorarray['©ed4'] = 'edit4'; - $handyatomtranslatorarray['©ed5'] = 'edit5'; - $handyatomtranslatorarray['©ed6'] = 'edit6'; - $handyatomtranslatorarray['©ed7'] = 'edit7'; - $handyatomtranslatorarray['©ed8'] = 'edit8'; - $handyatomtranslatorarray['©ed9'] = 'edit9'; - $handyatomtranslatorarray['©fmt'] = 'format'; - $handyatomtranslatorarray['©inf'] = 'information'; - $handyatomtranslatorarray['©prd'] = 'producer'; - $handyatomtranslatorarray['©prf'] = 'performers'; - $handyatomtranslatorarray['©req'] = 'system_requirements'; - $handyatomtranslatorarray['©src'] = 'source_credit'; - $handyatomtranslatorarray['©wrt'] = 'writer'; - - // http://www.geocities.com/xhelmboyx/quicktime/formats/qtm-layout.txt - $handyatomtranslatorarray['©nam'] = 'title'; // iTunes 4.0 - $handyatomtranslatorarray['©cmt'] = 'comment'; // iTunes 4.0 - $handyatomtranslatorarray['©wrn'] = 'warning'; - $handyatomtranslatorarray['©hst'] = 'host_computer'; - $handyatomtranslatorarray['©mak'] = 'make'; - $handyatomtranslatorarray['©mod'] = 'model'; - $handyatomtranslatorarray['©PRD'] = 'product'; - $handyatomtranslatorarray['©swr'] = 'software'; - $handyatomtranslatorarray['©aut'] = 'author'; - $handyatomtranslatorarray['©ART'] = 'artist'; - $handyatomtranslatorarray['©trk'] = 'track'; - $handyatomtranslatorarray['©alb'] = 'album'; // iTunes 4.0 - $handyatomtranslatorarray['©com'] = 'comment'; - $handyatomtranslatorarray['©gen'] = 'genre'; // iTunes 4.0 - $handyatomtranslatorarray['©ope'] = 'composer'; - $handyatomtranslatorarray['©url'] = 'url'; - $handyatomtranslatorarray['©enc'] = 'encoder'; - - // http://atomicparsley.sourceforge.net/mpeg-4files.html - $handyatomtranslatorarray['©art'] = 'artist'; // iTunes 4.0 - $handyatomtranslatorarray['aART'] = 'album_artist'; - $handyatomtranslatorarray['trkn'] = 'track_number'; // iTunes 4.0 - $handyatomtranslatorarray['disk'] = 'disc_number'; // iTunes 4.0 - $handyatomtranslatorarray['gnre'] = 'genre'; // iTunes 4.0 - $handyatomtranslatorarray['©too'] = 'encoder'; // iTunes 4.0 - $handyatomtranslatorarray['tmpo'] = 'bpm'; // iTunes 4.0 - $handyatomtranslatorarray['cprt'] = 'copyright'; // iTunes 4.0? - $handyatomtranslatorarray['cpil'] = 'compilation'; // iTunes 4.0 - $handyatomtranslatorarray['covr'] = 'picture'; // iTunes 4.0 - $handyatomtranslatorarray['rtng'] = 'rating'; // iTunes 4.0 - $handyatomtranslatorarray['©grp'] = 'grouping'; // iTunes 4.2 - $handyatomtranslatorarray['stik'] = 'stik'; // iTunes 4.9 - $handyatomtranslatorarray['pcst'] = 'podcast'; // iTunes 4.9 - $handyatomtranslatorarray['catg'] = 'category'; // iTunes 4.9 - $handyatomtranslatorarray['keyw'] = 'keyword'; // iTunes 4.9 - $handyatomtranslatorarray['purl'] = 'podcast_url'; // iTunes 4.9 - $handyatomtranslatorarray['egid'] = 'episode_guid'; // iTunes 4.9 - $handyatomtranslatorarray['desc'] = 'description'; // iTunes 5.0 - $handyatomtranslatorarray['©lyr'] = 'lyrics'; // iTunes 5.0 - $handyatomtranslatorarray['tvnn'] = 'tv_network_name'; // iTunes 6.0 - $handyatomtranslatorarray['tvsh'] = 'tv_show_name'; // iTunes 6.0 - $handyatomtranslatorarray['tvsn'] = 'tv_season'; // iTunes 6.0 - $handyatomtranslatorarray['tves'] = 'tv_episode'; // iTunes 6.0 - $handyatomtranslatorarray['purd'] = 'purchase_date'; // iTunes 6.0.2 - $handyatomtranslatorarray['pgap'] = 'gapless_playback'; // iTunes 7.0 - - // http://www.geocities.com/xhelmboyx/quicktime/formats/mp4-layout.txt - - - - // boxnames: - /* - $handyatomtranslatorarray['iTunSMPB'] = 'iTunSMPB'; - $handyatomtranslatorarray['iTunNORM'] = 'iTunNORM'; - $handyatomtranslatorarray['Encoding Params'] = 'Encoding Params'; - $handyatomtranslatorarray['replaygain_track_gain'] = 'replaygain_track_gain'; - $handyatomtranslatorarray['replaygain_track_peak'] = 'replaygain_track_peak'; - $handyatomtranslatorarray['replaygain_track_minmax'] = 'replaygain_track_minmax'; - $handyatomtranslatorarray['MusicIP PUID'] = 'MusicIP PUID'; - $handyatomtranslatorarray['MusicBrainz Artist Id'] = 'MusicBrainz Artist Id'; - $handyatomtranslatorarray['MusicBrainz Album Id'] = 'MusicBrainz Album Id'; - $handyatomtranslatorarray['MusicBrainz Album Artist Id'] = 'MusicBrainz Album Artist Id'; - $handyatomtranslatorarray['MusicBrainz Track Id'] = 'MusicBrainz Track Id'; - $handyatomtranslatorarray['MusicBrainz Disc Id'] = 'MusicBrainz Disc Id'; - - // http://age.hobba.nl/audio/tag_frame_reference.html - $handyatomtranslatorarray['PLAY_COUNTER'] = 'play_counter'; // Foobar2000 - http://www.getid3.org/phpBB3/viewtopic.php?t=1355 - $handyatomtranslatorarray['MEDIATYPE'] = 'mediatype'; // Foobar2000 - http://www.getid3.org/phpBB3/viewtopic.php?t=1355 - */ - } - $info = &$this->getid3->info; - $comment_key = ''; - if ($boxname && ($boxname != $keyname)) { - $comment_key = (isset($handyatomtranslatorarray[$boxname]) ? $handyatomtranslatorarray[$boxname] : $boxname); - } elseif (isset($handyatomtranslatorarray[$keyname])) { - $comment_key = $handyatomtranslatorarray[$keyname]; - } - if ($comment_key) { - if ($comment_key == 'picture') { - if (!is_array($data)) { - $image_mime = ''; - if (preg_match('#^\x89\x50\x4E\x47\x0D\x0A\x1A\x0A#', $data)) { - $image_mime = 'image/png'; - } elseif (preg_match('#^\xFF\xD8\xFF#', $data)) { - $image_mime = 'image/jpeg'; - } elseif (preg_match('#^GIF#', $data)) { - $image_mime = 'image/gif'; - } elseif (preg_match('#^BM#', $data)) { - $image_mime = 'image/bmp'; - } - $data = array('data'=>$data, 'image_mime'=>$image_mime); - } - } - $info['quicktime']['comments'][$comment_key][] = $data; - } - return true; - } - - public function NoNullString($nullterminatedstring) { - // remove the single null terminator on null terminated strings - if (substr($nullterminatedstring, strlen($nullterminatedstring) - 1, 1) === "\x00") { - return substr($nullterminatedstring, 0, strlen($nullterminatedstring) - 1); - } - return $nullterminatedstring; - } - - public function Pascal2String($pascalstring) { - // Pascal strings have 1 unsigned byte at the beginning saying how many chars (1-255) are in the string - return substr($pascalstring, 1); - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio-video.real.php b/src/Classes/Vendor/getid3/module.audio-video.real.php deleted file mode 100755 index 0226ac499..000000000 --- a/src/Classes/Vendor/getid3/module.audio-video.real.php +++ /dev/null @@ -1,527 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio-video.real.php // -// module for analyzing Real Audio/Video files // -// dependencies: module.audio-video.riff.php // -// /// -///////////////////////////////////////////////////////////////// - -getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); - -class getid3_real extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'real'; - $info['bitrate'] = 0; - $info['playtime_seconds'] = 0; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $ChunkCounter = 0; - while (ftell($this->getid3->fp) < $info['avdataend']) { - $ChunkData = fread($this->getid3->fp, 8); - $ChunkName = substr($ChunkData, 0, 4); - $ChunkSize = getid3_lib::BigEndian2Int(substr($ChunkData, 4, 4)); - - if ($ChunkName == '.ra'."\xFD") { - $ChunkData .= fread($this->getid3->fp, $ChunkSize - 8); - if ($this->ParseOldRAheader(substr($ChunkData, 0, 128), $info['real']['old_ra_header'])) { - $info['audio']['dataformat'] = 'real'; - $info['audio']['lossless'] = false; - $info['audio']['sample_rate'] = $info['real']['old_ra_header']['sample_rate']; - $info['audio']['bits_per_sample'] = $info['real']['old_ra_header']['bits_per_sample']; - $info['audio']['channels'] = $info['real']['old_ra_header']['channels']; - - $info['playtime_seconds'] = 60 * ($info['real']['old_ra_header']['audio_bytes'] / $info['real']['old_ra_header']['bytes_per_minute']); - $info['audio']['bitrate'] = 8 * ($info['real']['old_ra_header']['audio_bytes'] / $info['playtime_seconds']); - $info['audio']['codec'] = $this->RealAudioCodecFourCClookup($info['real']['old_ra_header']['fourcc'], $info['audio']['bitrate']); - - foreach ($info['real']['old_ra_header']['comments'] as $key => $valuearray) { - if (strlen(trim($valuearray[0])) > 0) { - $info['real']['comments'][$key][] = trim($valuearray[0]); - } - } - return true; - } - $info['error'][] = 'There was a problem parsing this RealAudio file. Please submit it for analysis to info@getid3.org'; - unset($info['bitrate']); - unset($info['playtime_seconds']); - return false; - } - - // shortcut - $info['real']['chunks'][$ChunkCounter] = array(); - $thisfile_real_chunks_currentchunk = &$info['real']['chunks'][$ChunkCounter]; - - $thisfile_real_chunks_currentchunk['name'] = $ChunkName; - $thisfile_real_chunks_currentchunk['offset'] = ftell($this->getid3->fp) - 8; - $thisfile_real_chunks_currentchunk['length'] = $ChunkSize; - if (($thisfile_real_chunks_currentchunk['offset'] + $thisfile_real_chunks_currentchunk['length']) > $info['avdataend']) { - $info['warning'][] = 'Chunk "'.$thisfile_real_chunks_currentchunk['name'].'" at offset '.$thisfile_real_chunks_currentchunk['offset'].' claims to be '.$thisfile_real_chunks_currentchunk['length'].' bytes long, which is beyond end of file'; - return false; - } - - if ($ChunkSize > ($this->getid3->fread_buffer_size() + 8)) { - - $ChunkData .= fread($this->getid3->fp, $this->getid3->fread_buffer_size() - 8); - fseek($this->getid3->fp, $thisfile_real_chunks_currentchunk['offset'] + $ChunkSize, SEEK_SET); - - } elseif(($ChunkSize - 8) > 0) { - - $ChunkData .= fread($this->getid3->fp, $ChunkSize - 8); - - } - $offset = 8; - - switch ($ChunkName) { - - case '.RMF': // RealMedia File Header - $thisfile_real_chunks_currentchunk['object_version'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 2)); - $offset += 2; - switch ($thisfile_real_chunks_currentchunk['object_version']) { - - case 0: - $thisfile_real_chunks_currentchunk['file_version'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['headers_count'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - break; - - default: - //$info['warning'][] = 'Expected .RMF-object_version to be "0", actual value is "'.$thisfile_real_chunks_currentchunk['object_version'].'" (should not be a problem)'; - break; - - } - break; - - - case 'PROP': // Properties Header - $thisfile_real_chunks_currentchunk['object_version'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 2)); - $offset += 2; - if ($thisfile_real_chunks_currentchunk['object_version'] == 0) { - $thisfile_real_chunks_currentchunk['max_bit_rate'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['avg_bit_rate'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['max_packet_size'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['avg_packet_size'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['num_packets'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['duration'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['preroll'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['index_offset'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['data_offset'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['num_streams'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 2)); - $offset += 2; - $thisfile_real_chunks_currentchunk['flags_raw'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 2)); - $offset += 2; - $info['playtime_seconds'] = $thisfile_real_chunks_currentchunk['duration'] / 1000; - if ($thisfile_real_chunks_currentchunk['duration'] > 0) { - $info['bitrate'] += $thisfile_real_chunks_currentchunk['avg_bit_rate']; - } - $thisfile_real_chunks_currentchunk['flags']['save_enabled'] = (bool) ($thisfile_real_chunks_currentchunk['flags_raw'] & 0x0001); - $thisfile_real_chunks_currentchunk['flags']['perfect_play'] = (bool) ($thisfile_real_chunks_currentchunk['flags_raw'] & 0x0002); - $thisfile_real_chunks_currentchunk['flags']['live_broadcast'] = (bool) ($thisfile_real_chunks_currentchunk['flags_raw'] & 0x0004); - } - break; - - case 'MDPR': // Media Properties Header - $thisfile_real_chunks_currentchunk['object_version'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 2)); - $offset += 2; - if ($thisfile_real_chunks_currentchunk['object_version'] == 0) { - $thisfile_real_chunks_currentchunk['stream_number'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 2)); - $offset += 2; - $thisfile_real_chunks_currentchunk['max_bit_rate'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['avg_bit_rate'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['max_packet_size'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['avg_packet_size'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['start_time'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['preroll'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['duration'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['stream_name_size'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 1)); - $offset += 1; - $thisfile_real_chunks_currentchunk['stream_name'] = substr($ChunkData, $offset, $thisfile_real_chunks_currentchunk['stream_name_size']); - $offset += $thisfile_real_chunks_currentchunk['stream_name_size']; - $thisfile_real_chunks_currentchunk['mime_type_size'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 1)); - $offset += 1; - $thisfile_real_chunks_currentchunk['mime_type'] = substr($ChunkData, $offset, $thisfile_real_chunks_currentchunk['mime_type_size']); - $offset += $thisfile_real_chunks_currentchunk['mime_type_size']; - $thisfile_real_chunks_currentchunk['type_specific_len'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['type_specific_data'] = substr($ChunkData, $offset, $thisfile_real_chunks_currentchunk['type_specific_len']); - $offset += $thisfile_real_chunks_currentchunk['type_specific_len']; - - // shortcut - $thisfile_real_chunks_currentchunk_typespecificdata = &$thisfile_real_chunks_currentchunk['type_specific_data']; - - switch ($thisfile_real_chunks_currentchunk['mime_type']) { - case 'video/x-pn-realvideo': - case 'video/x-pn-multirate-realvideo': - // http://www.freelists.org/archives/matroska-devel/07-2003/msg00010.html - - // shortcut - $thisfile_real_chunks_currentchunk['video_info'] = array(); - $thisfile_real_chunks_currentchunk_videoinfo = &$thisfile_real_chunks_currentchunk['video_info']; - - $thisfile_real_chunks_currentchunk_videoinfo['dwSize'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, 0, 4)); - $thisfile_real_chunks_currentchunk_videoinfo['fourcc1'] = substr($thisfile_real_chunks_currentchunk_typespecificdata, 4, 4); - $thisfile_real_chunks_currentchunk_videoinfo['fourcc2'] = substr($thisfile_real_chunks_currentchunk_typespecificdata, 8, 4); - $thisfile_real_chunks_currentchunk_videoinfo['width'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, 12, 2)); - $thisfile_real_chunks_currentchunk_videoinfo['height'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, 14, 2)); - $thisfile_real_chunks_currentchunk_videoinfo['bits_per_sample'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, 16, 2)); - //$thisfile_real_chunks_currentchunk_videoinfo['unknown1'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, 18, 2)); - //$thisfile_real_chunks_currentchunk_videoinfo['unknown2'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, 20, 2)); - $thisfile_real_chunks_currentchunk_videoinfo['frames_per_second'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, 22, 2)); - //$thisfile_real_chunks_currentchunk_videoinfo['unknown3'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, 24, 2)); - //$thisfile_real_chunks_currentchunk_videoinfo['unknown4'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, 26, 2)); - //$thisfile_real_chunks_currentchunk_videoinfo['unknown5'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, 28, 2)); - //$thisfile_real_chunks_currentchunk_videoinfo['unknown6'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, 30, 2)); - //$thisfile_real_chunks_currentchunk_videoinfo['unknown7'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, 32, 2)); - //$thisfile_real_chunks_currentchunk_videoinfo['unknown8'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, 34, 2)); - //$thisfile_real_chunks_currentchunk_videoinfo['unknown9'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, 36, 2)); - - $thisfile_real_chunks_currentchunk_videoinfo['codec'] = getid3_riff::fourccLookup($thisfile_real_chunks_currentchunk_videoinfo['fourcc2']); - - $info['video']['resolution_x'] = $thisfile_real_chunks_currentchunk_videoinfo['width']; - $info['video']['resolution_y'] = $thisfile_real_chunks_currentchunk_videoinfo['height']; - $info['video']['frame_rate'] = (float) $thisfile_real_chunks_currentchunk_videoinfo['frames_per_second']; - $info['video']['codec'] = $thisfile_real_chunks_currentchunk_videoinfo['codec']; - $info['video']['bits_per_sample'] = $thisfile_real_chunks_currentchunk_videoinfo['bits_per_sample']; - break; - - case 'audio/x-pn-realaudio': - case 'audio/x-pn-multirate-realaudio': - $this->ParseOldRAheader($thisfile_real_chunks_currentchunk_typespecificdata, $thisfile_real_chunks_currentchunk['parsed_audio_data']); - - $info['audio']['sample_rate'] = $thisfile_real_chunks_currentchunk['parsed_audio_data']['sample_rate']; - $info['audio']['bits_per_sample'] = $thisfile_real_chunks_currentchunk['parsed_audio_data']['bits_per_sample']; - $info['audio']['channels'] = $thisfile_real_chunks_currentchunk['parsed_audio_data']['channels']; - if (!empty($info['audio']['dataformat'])) { - foreach ($info['audio'] as $key => $value) { - if ($key != 'streams') { - $info['audio']['streams'][$thisfile_real_chunks_currentchunk['stream_number']][$key] = $value; - } - } - } - break; - - case 'logical-fileinfo': - // shortcut - $thisfile_real_chunks_currentchunk['logical_fileinfo'] = array(); - $thisfile_real_chunks_currentchunk_logicalfileinfo = &$thisfile_real_chunks_currentchunk['logical_fileinfo']; - - $thisfile_real_chunks_currentchunk_logicalfileinfo_offset = 0; - $thisfile_real_chunks_currentchunk_logicalfileinfo['logical_fileinfo_length'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, $thisfile_real_chunks_currentchunk_logicalfileinfo_offset, 4)); - $thisfile_real_chunks_currentchunk_logicalfileinfo_offset += 4; - - //$thisfile_real_chunks_currentchunk_logicalfileinfo['unknown1'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, $thisfile_real_chunks_currentchunk_logicalfileinfo_offset, 4)); - $thisfile_real_chunks_currentchunk_logicalfileinfo_offset += 4; - - $thisfile_real_chunks_currentchunk_logicalfileinfo['num_tags'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, $thisfile_real_chunks_currentchunk_logicalfileinfo_offset, 4)); - $thisfile_real_chunks_currentchunk_logicalfileinfo_offset += 4; - - //$thisfile_real_chunks_currentchunk_logicalfileinfo['unknown2'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, $thisfile_real_chunks_currentchunk_logicalfileinfo_offset, 4)); - $thisfile_real_chunks_currentchunk_logicalfileinfo_offset += 4; - - //$thisfile_real_chunks_currentchunk_logicalfileinfo['d'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, $thisfile_real_chunks_currentchunk_logicalfileinfo_offset, 1)); - - //$thisfile_real_chunks_currentchunk_logicalfileinfo['one_type'] = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, $thisfile_real_chunks_currentchunk_logicalfileinfo_offset, 4)); - //$thisfile_real_chunks_currentchunk_logicalfileinfo_thislength = getid3_lib::BigEndian2Int(substr($thisfile_real_chunks_currentchunk_typespecificdata, 4 + $thisfile_real_chunks_currentchunk_logicalfileinfo_offset, 2)); - //$thisfile_real_chunks_currentchunk_logicalfileinfo['one'] = substr($thisfile_real_chunks_currentchunk_typespecificdata, 6 + $thisfile_real_chunks_currentchunk_logicalfileinfo_offset, $thisfile_real_chunks_currentchunk_logicalfileinfo_thislength); - //$thisfile_real_chunks_currentchunk_logicalfileinfo_offset += (6 + $thisfile_real_chunks_currentchunk_logicalfileinfo_thislength); - - break; - - } - - - if (empty($info['playtime_seconds'])) { - $info['playtime_seconds'] = max($info['playtime_seconds'], ($thisfile_real_chunks_currentchunk['duration'] + $thisfile_real_chunks_currentchunk['start_time']) / 1000); - } - if ($thisfile_real_chunks_currentchunk['duration'] > 0) { - switch ($thisfile_real_chunks_currentchunk['mime_type']) { - case 'audio/x-pn-realaudio': - case 'audio/x-pn-multirate-realaudio': - $info['audio']['bitrate'] = (isset($info['audio']['bitrate']) ? $info['audio']['bitrate'] : 0) + $thisfile_real_chunks_currentchunk['avg_bit_rate']; - $info['audio']['codec'] = $this->RealAudioCodecFourCClookup($thisfile_real_chunks_currentchunk['parsed_audio_data']['fourcc'], $info['audio']['bitrate']); - $info['audio']['dataformat'] = 'real'; - $info['audio']['lossless'] = false; - break; - - case 'video/x-pn-realvideo': - case 'video/x-pn-multirate-realvideo': - $info['video']['bitrate'] = (isset($info['video']['bitrate']) ? $info['video']['bitrate'] : 0) + $thisfile_real_chunks_currentchunk['avg_bit_rate']; - $info['video']['bitrate_mode'] = 'cbr'; - $info['video']['dataformat'] = 'real'; - $info['video']['lossless'] = false; - $info['video']['pixel_aspect_ratio'] = (float) 1; - break; - - case 'audio/x-ralf-mpeg4-generic': - $info['audio']['bitrate'] = (isset($info['audio']['bitrate']) ? $info['audio']['bitrate'] : 0) + $thisfile_real_chunks_currentchunk['avg_bit_rate']; - $info['audio']['codec'] = 'RealAudio Lossless'; - $info['audio']['dataformat'] = 'real'; - $info['audio']['lossless'] = true; - break; - } - $info['bitrate'] = (isset($info['video']['bitrate']) ? $info['video']['bitrate'] : 0) + (isset($info['audio']['bitrate']) ? $info['audio']['bitrate'] : 0); - } - } - break; - - case 'CONT': // Content Description Header (text comments) - $thisfile_real_chunks_currentchunk['object_version'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 2)); - $offset += 2; - if ($thisfile_real_chunks_currentchunk['object_version'] == 0) { - $thisfile_real_chunks_currentchunk['title_len'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 2)); - $offset += 2; - $thisfile_real_chunks_currentchunk['title'] = (string) substr($ChunkData, $offset, $thisfile_real_chunks_currentchunk['title_len']); - $offset += $thisfile_real_chunks_currentchunk['title_len']; - - $thisfile_real_chunks_currentchunk['artist_len'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 2)); - $offset += 2; - $thisfile_real_chunks_currentchunk['artist'] = (string) substr($ChunkData, $offset, $thisfile_real_chunks_currentchunk['artist_len']); - $offset += $thisfile_real_chunks_currentchunk['artist_len']; - - $thisfile_real_chunks_currentchunk['copyright_len'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 2)); - $offset += 2; - $thisfile_real_chunks_currentchunk['copyright'] = (string) substr($ChunkData, $offset, $thisfile_real_chunks_currentchunk['copyright_len']); - $offset += $thisfile_real_chunks_currentchunk['copyright_len']; - - $thisfile_real_chunks_currentchunk['comment_len'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 2)); - $offset += 2; - $thisfile_real_chunks_currentchunk['comment'] = (string) substr($ChunkData, $offset, $thisfile_real_chunks_currentchunk['comment_len']); - $offset += $thisfile_real_chunks_currentchunk['comment_len']; - - - $commentkeystocopy = array('title'=>'title', 'artist'=>'artist', 'copyright'=>'copyright', 'comment'=>'comment'); - foreach ($commentkeystocopy as $key => $val) { - if ($thisfile_real_chunks_currentchunk[$key]) { - $info['real']['comments'][$val][] = trim($thisfile_real_chunks_currentchunk[$key]); - } - } - - } - break; - - - case 'DATA': // Data Chunk Header - // do nothing - break; - - case 'INDX': // Index Section Header - $thisfile_real_chunks_currentchunk['object_version'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 2)); - $offset += 2; - if ($thisfile_real_chunks_currentchunk['object_version'] == 0) { - $thisfile_real_chunks_currentchunk['num_indices'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - $thisfile_real_chunks_currentchunk['stream_number'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 2)); - $offset += 2; - $thisfile_real_chunks_currentchunk['next_index_header'] = getid3_lib::BigEndian2Int(substr($ChunkData, $offset, 4)); - $offset += 4; - - if ($thisfile_real_chunks_currentchunk['next_index_header'] == 0) { - // last index chunk found, ignore rest of file - break 2; - } else { - // non-last index chunk, seek to next index chunk (skipping actual index data) - fseek($this->getid3->fp, $thisfile_real_chunks_currentchunk['next_index_header'], SEEK_SET); - } - } - break; - - default: - $info['warning'][] = 'Unhandled RealMedia chunk "'.$ChunkName.'" at offset '.$thisfile_real_chunks_currentchunk['offset']; - break; - } - $ChunkCounter++; - } - - if (!empty($info['audio']['streams'])) { - $info['audio']['bitrate'] = 0; - foreach ($info['audio']['streams'] as $key => $valuearray) { - $info['audio']['bitrate'] += $valuearray['bitrate']; - } - } - - return true; - } - - - public function ParseOldRAheader($OldRAheaderData, &$ParsedArray) { - // http://www.freelists.org/archives/matroska-devel/07-2003/msg00010.html - - $ParsedArray = array(); - $ParsedArray['magic'] = substr($OldRAheaderData, 0, 4); - if ($ParsedArray['magic'] != '.ra'."\xFD") { - return false; - } - $ParsedArray['version1'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 4, 2)); - - if ($ParsedArray['version1'] < 3) { - - return false; - - } elseif ($ParsedArray['version1'] == 3) { - - $ParsedArray['fourcc1'] = '.ra3'; - $ParsedArray['bits_per_sample'] = 16; // hard-coded for old versions? - $ParsedArray['sample_rate'] = 8000; // hard-coded for old versions? - - $ParsedArray['header_size'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 6, 2)); - $ParsedArray['channels'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 8, 2)); // always 1 (?) - //$ParsedArray['unknown1'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 10, 2)); - //$ParsedArray['unknown2'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 12, 2)); - //$ParsedArray['unknown3'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 14, 2)); - $ParsedArray['bytes_per_minute'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 16, 2)); - $ParsedArray['audio_bytes'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 18, 4)); - $ParsedArray['comments_raw'] = substr($OldRAheaderData, 22, $ParsedArray['header_size'] - 22 + 1); // not including null terminator - - $commentoffset = 0; - $commentlength = getid3_lib::BigEndian2Int(substr($ParsedArray['comments_raw'], $commentoffset++, 1)); - $ParsedArray['comments']['title'][] = substr($ParsedArray['comments_raw'], $commentoffset, $commentlength); - $commentoffset += $commentlength; - - $commentlength = getid3_lib::BigEndian2Int(substr($ParsedArray['comments_raw'], $commentoffset++, 1)); - $ParsedArray['comments']['artist'][] = substr($ParsedArray['comments_raw'], $commentoffset, $commentlength); - $commentoffset += $commentlength; - - $commentlength = getid3_lib::BigEndian2Int(substr($ParsedArray['comments_raw'], $commentoffset++, 1)); - $ParsedArray['comments']['copyright'][] = substr($ParsedArray['comments_raw'], $commentoffset, $commentlength); - $commentoffset += $commentlength; - - $commentoffset++; // final null terminator (?) - $commentoffset++; // fourcc length (?) should be 4 - $ParsedArray['fourcc'] = substr($OldRAheaderData, 23 + $commentoffset, 4); - - } elseif ($ParsedArray['version1'] <= 5) { - - //$ParsedArray['unknown1'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 6, 2)); - $ParsedArray['fourcc1'] = substr($OldRAheaderData, 8, 4); - $ParsedArray['file_size'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 12, 4)); - $ParsedArray['version2'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 16, 2)); - $ParsedArray['header_size'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 18, 4)); - $ParsedArray['codec_flavor_id'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 22, 2)); - $ParsedArray['coded_frame_size'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 24, 4)); - $ParsedArray['audio_bytes'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 28, 4)); - $ParsedArray['bytes_per_minute'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 32, 4)); - //$ParsedArray['unknown5'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 36, 4)); - $ParsedArray['sub_packet_h'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 40, 2)); - $ParsedArray['frame_size'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 42, 2)); - $ParsedArray['sub_packet_size'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 44, 2)); - //$ParsedArray['unknown6'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 46, 2)); - - switch ($ParsedArray['version1']) { - - case 4: - $ParsedArray['sample_rate'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 48, 2)); - //$ParsedArray['unknown8'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 50, 2)); - $ParsedArray['bits_per_sample'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 52, 2)); - $ParsedArray['channels'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 54, 2)); - $ParsedArray['length_fourcc2'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 56, 1)); - $ParsedArray['fourcc2'] = substr($OldRAheaderData, 57, 4); - $ParsedArray['length_fourcc3'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 61, 1)); - $ParsedArray['fourcc3'] = substr($OldRAheaderData, 62, 4); - //$ParsedArray['unknown9'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 66, 1)); - //$ParsedArray['unknown10'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 67, 2)); - $ParsedArray['comments_raw'] = substr($OldRAheaderData, 69, $ParsedArray['header_size'] - 69 + 16); - - $commentoffset = 0; - $commentlength = getid3_lib::BigEndian2Int(substr($ParsedArray['comments_raw'], $commentoffset++, 1)); - $ParsedArray['comments']['title'][] = substr($ParsedArray['comments_raw'], $commentoffset, $commentlength); - $commentoffset += $commentlength; - - $commentlength = getid3_lib::BigEndian2Int(substr($ParsedArray['comments_raw'], $commentoffset++, 1)); - $ParsedArray['comments']['artist'][] = substr($ParsedArray['comments_raw'], $commentoffset, $commentlength); - $commentoffset += $commentlength; - - $commentlength = getid3_lib::BigEndian2Int(substr($ParsedArray['comments_raw'], $commentoffset++, 1)); - $ParsedArray['comments']['copyright'][] = substr($ParsedArray['comments_raw'], $commentoffset, $commentlength); - $commentoffset += $commentlength; - break; - - case 5: - $ParsedArray['sample_rate'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 48, 4)); - $ParsedArray['sample_rate2'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 52, 4)); - $ParsedArray['bits_per_sample'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 56, 4)); - $ParsedArray['channels'] = getid3_lib::BigEndian2Int(substr($OldRAheaderData, 60, 2)); - $ParsedArray['genr'] = substr($OldRAheaderData, 62, 4); - $ParsedArray['fourcc3'] = substr($OldRAheaderData, 66, 4); - $ParsedArray['comments'] = array(); - break; - } - $ParsedArray['fourcc'] = $ParsedArray['fourcc3']; - - } - foreach ($ParsedArray['comments'] as $key => $value) { - if ($ParsedArray['comments'][$key][0] === false) { - $ParsedArray['comments'][$key][0] = ''; - } - } - - return true; - } - - public function RealAudioCodecFourCClookup($fourcc, $bitrate) { - static $RealAudioCodecFourCClookup = array(); - if (empty($RealAudioCodecFourCClookup)) { - // http://www.its.msstate.edu/net/real/reports/config/tags.stats - // http://www.freelists.org/archives/matroska-devel/06-2003/fullthread18.html - - $RealAudioCodecFourCClookup['14_4'][8000] = 'RealAudio v2 (14.4kbps)'; - $RealAudioCodecFourCClookup['14.4'][8000] = 'RealAudio v2 (14.4kbps)'; - $RealAudioCodecFourCClookup['lpcJ'][8000] = 'RealAudio v2 (14.4kbps)'; - $RealAudioCodecFourCClookup['28_8'][15200] = 'RealAudio v2 (28.8kbps)'; - $RealAudioCodecFourCClookup['28.8'][15200] = 'RealAudio v2 (28.8kbps)'; - $RealAudioCodecFourCClookup['sipr'][4933] = 'RealAudio v4 (5kbps Voice)'; - $RealAudioCodecFourCClookup['sipr'][6444] = 'RealAudio v4 (6.5kbps Voice)'; - $RealAudioCodecFourCClookup['sipr'][8444] = 'RealAudio v4 (8.5kbps Voice)'; - $RealAudioCodecFourCClookup['sipr'][16000] = 'RealAudio v4 (16kbps Wideband)'; - $RealAudioCodecFourCClookup['dnet'][8000] = 'RealAudio v3 (8kbps Music)'; - $RealAudioCodecFourCClookup['dnet'][16000] = 'RealAudio v3 (16kbps Music Low Response)'; - $RealAudioCodecFourCClookup['dnet'][15963] = 'RealAudio v3 (16kbps Music Mid/High Response)'; - $RealAudioCodecFourCClookup['dnet'][20000] = 'RealAudio v3 (20kbps Music Stereo)'; - $RealAudioCodecFourCClookup['dnet'][32000] = 'RealAudio v3 (32kbps Music Mono)'; - $RealAudioCodecFourCClookup['dnet'][31951] = 'RealAudio v3 (32kbps Music Stereo)'; - $RealAudioCodecFourCClookup['dnet'][39965] = 'RealAudio v3 (40kbps Music Mono)'; - $RealAudioCodecFourCClookup['dnet'][40000] = 'RealAudio v3 (40kbps Music Stereo)'; - $RealAudioCodecFourCClookup['dnet'][79947] = 'RealAudio v3 (80kbps Music Mono)'; - $RealAudioCodecFourCClookup['dnet'][80000] = 'RealAudio v3 (80kbps Music Stereo)'; - - $RealAudioCodecFourCClookup['dnet'][0] = 'RealAudio v3'; - $RealAudioCodecFourCClookup['sipr'][0] = 'RealAudio v4'; - $RealAudioCodecFourCClookup['cook'][0] = 'RealAudio G2'; - $RealAudioCodecFourCClookup['atrc'][0] = 'RealAudio 8'; - } - $roundbitrate = intval(round($bitrate)); - if (isset($RealAudioCodecFourCClookup[$fourcc][$roundbitrate])) { - return $RealAudioCodecFourCClookup[$fourcc][$roundbitrate]; - } elseif (isset($RealAudioCodecFourCClookup[$fourcc][0])) { - return $RealAudioCodecFourCClookup[$fourcc][0]; - } - return $fourcc; - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio-video.riff.php b/src/Classes/Vendor/getid3/module.audio-video.riff.php deleted file mode 100755 index ab4b3b611..000000000 --- a/src/Classes/Vendor/getid3/module.audio-video.riff.php +++ /dev/null @@ -1,2435 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio-video.riff.php // -// module for analyzing RIFF files // -// multiple formats supported by this module: // -// Wave, AVI, AIFF/AIFC, (MP3,AC3)/RIFF, Wavpack v3, 8SVX // -// dependencies: module.audio.mp3.php // -// module.audio.ac3.php // -// module.audio.dts.php // -// /// -///////////////////////////////////////////////////////////////// - -/** -* @todo Parse AC-3/DTS audio inside WAVE correctly -* @todo Rewrite RIFF parser totally -*/ - -getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.mp3.php', __FILE__, true); -getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.ac3.php', __FILE__, true); -getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.dts.php', __FILE__, true); - -class getid3_riff extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - // initialize these values to an empty array, otherwise they default to NULL - // and you can't append array values to a NULL value - $info['riff'] = array('raw'=>array()); - - // Shortcuts - $thisfile_riff = &$info['riff']; - $thisfile_riff_raw = &$thisfile_riff['raw']; - $thisfile_audio = &$info['audio']; - $thisfile_video = &$info['video']; - $thisfile_audio_dataformat = &$thisfile_audio['dataformat']; - $thisfile_riff_audio = &$thisfile_riff['audio']; - $thisfile_riff_video = &$thisfile_riff['video']; - - $Original['avdataoffset'] = $info['avdataoffset']; - $Original['avdataend'] = $info['avdataend']; - - $this->fseek($info['avdataoffset']); - $RIFFheader = $this->fread(12); - $offset = $this->ftell(); - $RIFFtype = substr($RIFFheader, 0, 4); - $RIFFsize = substr($RIFFheader, 4, 4); - $RIFFsubtype = substr($RIFFheader, 8, 4); - - switch ($RIFFtype) { - - case 'FORM': // AIFF, AIFC - $info['fileformat'] = 'aiff'; - $thisfile_riff['header_size'] = $this->EitherEndian2Int($RIFFsize); - $thisfile_riff[$RIFFsubtype] = $this->ParseRIFF($offset, ($offset + $thisfile_riff['header_size'] - 4)); - break; - - case 'RIFF': // AVI, WAV, etc - case 'SDSS': // SDSS is identical to RIFF, just renamed. Used by SmartSound QuickTracks (www.smartsound.com) - case 'RMP3': // RMP3 is identical to RIFF, just renamed. Used by [unknown program] when creating RIFF-MP3s - $info['fileformat'] = 'riff'; - $thisfile_riff['header_size'] = $this->EitherEndian2Int($RIFFsize); - if ($RIFFsubtype == 'RMP3') { - // RMP3 is identical to WAVE, just renamed. Used by [unknown program] when creating RIFF-MP3s - $RIFFsubtype = 'WAVE'; - } - $thisfile_riff[$RIFFsubtype] = $this->ParseRIFF($offset, ($offset + $thisfile_riff['header_size'] - 4)); - if (($info['avdataend'] - $info['filesize']) == 1) { - // LiteWave appears to incorrectly *not* pad actual output file - // to nearest WORD boundary so may appear to be short by one - // byte, in which case - skip warning - $info['avdataend'] = $info['filesize']; - } - - $nextRIFFoffset = $Original['avdataoffset'] + 8 + $thisfile_riff['header_size']; // 8 = "RIFF" + 32-bit offset - while ($nextRIFFoffset < min($info['filesize'], $info['avdataend'])) { - try { - $this->fseek($nextRIFFoffset); - } catch (getid3_exception $e) { - if ($e->getCode() == 10) { - //$this->warning('RIFF parser: '.$e->getMessage()); - $this->error('AVI extends beyond '.round(PHP_INT_MAX / 1073741824).'GB and PHP filesystem functions cannot read that far, playtime may be wrong'); - $this->warning('[avdataend] value may be incorrect, multiple AVIX chunks may be present'); - break; - } else { - throw $e; - } - } - $nextRIFFheader = $this->fread(12); - if ($nextRIFFoffset == ($info['avdataend'] - 1)) { - if (substr($nextRIFFheader, 0, 1) == "\x00") { - // RIFF padded to WORD boundary, we're actually already at the end - break; - } - } - $nextRIFFheaderID = substr($nextRIFFheader, 0, 4); - $nextRIFFsize = $this->EitherEndian2Int(substr($nextRIFFheader, 4, 4)); - $nextRIFFtype = substr($nextRIFFheader, 8, 4); - $chunkdata = array(); - $chunkdata['offset'] = $nextRIFFoffset + 8; - $chunkdata['size'] = $nextRIFFsize; - $nextRIFFoffset = $chunkdata['offset'] + $chunkdata['size']; - - switch ($nextRIFFheaderID) { - - case 'RIFF': - $chunkdata['chunks'] = $this->ParseRIFF($chunkdata['offset'] + 4, $nextRIFFoffset); - - if (!isset($thisfile_riff[$nextRIFFtype])) { - $thisfile_riff[$nextRIFFtype] = array(); - } - $thisfile_riff[$nextRIFFtype][] = $chunkdata; - break; - - case 'JUNK': - // ignore - $thisfile_riff[$nextRIFFheaderID][] = $chunkdata; - break; - - case 'IDVX': - $info['divxtag']['comments'] = self::ParseDIVXTAG($this->fread($chunkdata['size'])); - break; - - default: - if ($info['filesize'] == ($chunkdata['offset'] - 8 + 128)) { - $DIVXTAG = $nextRIFFheader.$this->fread(128 - 12); - if (substr($DIVXTAG, -7) == 'DIVXTAG') { - // DIVXTAG is supposed to be inside an IDVX chunk in a LIST chunk, but some bad encoders just slap it on the end of a file - $this->warning('Found wrongly-structured DIVXTAG at offset '.($this->ftell() - 128).', parsing anyway'); - $info['divxtag']['comments'] = self::ParseDIVXTAG($DIVXTAG); - break 2; - } - } - $this->warning('Expecting "RIFF|JUNK|IDVX" at '.$nextRIFFoffset.', found "'.$nextRIFFheaderID.'" ('.getid3_lib::PrintHexBytes($nextRIFFheaderID).') - skipping rest of file'); - break 2; - - } - - } - if ($RIFFsubtype == 'WAVE') { - $thisfile_riff_WAVE = &$thisfile_riff['WAVE']; - } - break; - - default: - $this->error('Cannot parse RIFF (this is maybe not a RIFF / WAV / AVI file?) - expecting "FORM|RIFF|SDSS|RMP3" found "'.$RIFFsubtype.'" instead'); - unset($info['fileformat']); - return false; - } - - $streamindex = 0; - switch ($RIFFsubtype) { - case 'WAVE': - if (empty($thisfile_audio['bitrate_mode'])) { - $thisfile_audio['bitrate_mode'] = 'cbr'; - } - if (empty($thisfile_audio_dataformat)) { - $thisfile_audio_dataformat = 'wav'; - } - - if (isset($thisfile_riff_WAVE['data'][0]['offset'])) { - $info['avdataoffset'] = $thisfile_riff_WAVE['data'][0]['offset'] + 8; - $info['avdataend'] = $info['avdataoffset'] + $thisfile_riff_WAVE['data'][0]['size']; - } - if (isset($thisfile_riff_WAVE['fmt '][0]['data'])) { - - $thisfile_riff_audio[$streamindex] = self::parseWAVEFORMATex($thisfile_riff_WAVE['fmt '][0]['data']); - $thisfile_audio['wformattag'] = $thisfile_riff_audio[$streamindex]['raw']['wFormatTag']; - if (!isset($thisfile_riff_audio[$streamindex]['bitrate']) || ($thisfile_riff_audio[$streamindex]['bitrate'] == 0)) { - $info['error'][] = 'Corrupt RIFF file: bitrate_audio == zero'; - return false; - } - $thisfile_riff_raw['fmt '] = $thisfile_riff_audio[$streamindex]['raw']; - unset($thisfile_riff_audio[$streamindex]['raw']); - $thisfile_audio['streams'][$streamindex] = $thisfile_riff_audio[$streamindex]; - - $thisfile_audio = getid3_lib::array_merge_noclobber($thisfile_audio, $thisfile_riff_audio[$streamindex]); - if (substr($thisfile_audio['codec'], 0, strlen('unknown: 0x')) == 'unknown: 0x') { - $info['warning'][] = 'Audio codec = '.$thisfile_audio['codec']; - } - $thisfile_audio['bitrate'] = $thisfile_riff_audio[$streamindex]['bitrate']; - - if (empty($info['playtime_seconds'])) { // may already be set (e.g. DTS-WAV) - $info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $thisfile_audio['bitrate']); - } - - $thisfile_audio['lossless'] = false; - if (isset($thisfile_riff_WAVE['data'][0]['offset']) && isset($thisfile_riff_raw['fmt ']['wFormatTag'])) { - switch ($thisfile_riff_raw['fmt ']['wFormatTag']) { - - case 0x0001: // PCM - $thisfile_audio['lossless'] = true; - break; - - case 0x2000: // AC-3 - $thisfile_audio_dataformat = 'ac3'; - break; - - default: - // do nothing - break; - - } - } - $thisfile_audio['streams'][$streamindex]['wformattag'] = $thisfile_audio['wformattag']; - $thisfile_audio['streams'][$streamindex]['bitrate_mode'] = $thisfile_audio['bitrate_mode']; - $thisfile_audio['streams'][$streamindex]['lossless'] = $thisfile_audio['lossless']; - $thisfile_audio['streams'][$streamindex]['dataformat'] = $thisfile_audio_dataformat; - } - - if (isset($thisfile_riff_WAVE['rgad'][0]['data'])) { - - // shortcuts - $rgadData = &$thisfile_riff_WAVE['rgad'][0]['data']; - $thisfile_riff_raw['rgad'] = array('track'=>array(), 'album'=>array()); - $thisfile_riff_raw_rgad = &$thisfile_riff_raw['rgad']; - $thisfile_riff_raw_rgad_track = &$thisfile_riff_raw_rgad['track']; - $thisfile_riff_raw_rgad_album = &$thisfile_riff_raw_rgad['album']; - - $thisfile_riff_raw_rgad['fPeakAmplitude'] = getid3_lib::LittleEndian2Float(substr($rgadData, 0, 4)); - $thisfile_riff_raw_rgad['nRadioRgAdjust'] = $this->EitherEndian2Int(substr($rgadData, 4, 2)); - $thisfile_riff_raw_rgad['nAudiophileRgAdjust'] = $this->EitherEndian2Int(substr($rgadData, 6, 2)); - - $nRadioRgAdjustBitstring = str_pad(getid3_lib::Dec2Bin($thisfile_riff_raw_rgad['nRadioRgAdjust']), 16, '0', STR_PAD_LEFT); - $nAudiophileRgAdjustBitstring = str_pad(getid3_lib::Dec2Bin($thisfile_riff_raw_rgad['nAudiophileRgAdjust']), 16, '0', STR_PAD_LEFT); - $thisfile_riff_raw_rgad_track['name'] = getid3_lib::Bin2Dec(substr($nRadioRgAdjustBitstring, 0, 3)); - $thisfile_riff_raw_rgad_track['originator'] = getid3_lib::Bin2Dec(substr($nRadioRgAdjustBitstring, 3, 3)); - $thisfile_riff_raw_rgad_track['signbit'] = getid3_lib::Bin2Dec(substr($nRadioRgAdjustBitstring, 6, 1)); - $thisfile_riff_raw_rgad_track['adjustment'] = getid3_lib::Bin2Dec(substr($nRadioRgAdjustBitstring, 7, 9)); - $thisfile_riff_raw_rgad_album['name'] = getid3_lib::Bin2Dec(substr($nAudiophileRgAdjustBitstring, 0, 3)); - $thisfile_riff_raw_rgad_album['originator'] = getid3_lib::Bin2Dec(substr($nAudiophileRgAdjustBitstring, 3, 3)); - $thisfile_riff_raw_rgad_album['signbit'] = getid3_lib::Bin2Dec(substr($nAudiophileRgAdjustBitstring, 6, 1)); - $thisfile_riff_raw_rgad_album['adjustment'] = getid3_lib::Bin2Dec(substr($nAudiophileRgAdjustBitstring, 7, 9)); - - $thisfile_riff['rgad']['peakamplitude'] = $thisfile_riff_raw_rgad['fPeakAmplitude']; - if (($thisfile_riff_raw_rgad_track['name'] != 0) && ($thisfile_riff_raw_rgad_track['originator'] != 0)) { - $thisfile_riff['rgad']['track']['name'] = getid3_lib::RGADnameLookup($thisfile_riff_raw_rgad_track['name']); - $thisfile_riff['rgad']['track']['originator'] = getid3_lib::RGADoriginatorLookup($thisfile_riff_raw_rgad_track['originator']); - $thisfile_riff['rgad']['track']['adjustment'] = getid3_lib::RGADadjustmentLookup($thisfile_riff_raw_rgad_track['adjustment'], $thisfile_riff_raw_rgad_track['signbit']); - } - if (($thisfile_riff_raw_rgad_album['name'] != 0) && ($thisfile_riff_raw_rgad_album['originator'] != 0)) { - $thisfile_riff['rgad']['album']['name'] = getid3_lib::RGADnameLookup($thisfile_riff_raw_rgad_album['name']); - $thisfile_riff['rgad']['album']['originator'] = getid3_lib::RGADoriginatorLookup($thisfile_riff_raw_rgad_album['originator']); - $thisfile_riff['rgad']['album']['adjustment'] = getid3_lib::RGADadjustmentLookup($thisfile_riff_raw_rgad_album['adjustment'], $thisfile_riff_raw_rgad_album['signbit']); - } - } - - if (isset($thisfile_riff_WAVE['fact'][0]['data'])) { - $thisfile_riff_raw['fact']['NumberOfSamples'] = $this->EitherEndian2Int(substr($thisfile_riff_WAVE['fact'][0]['data'], 0, 4)); - - // This should be a good way of calculating exact playtime, - // but some sample files have had incorrect number of samples, - // so cannot use this method - - // if (!empty($thisfile_riff_raw['fmt ']['nSamplesPerSec'])) { - // $info['playtime_seconds'] = (float) $thisfile_riff_raw['fact']['NumberOfSamples'] / $thisfile_riff_raw['fmt ']['nSamplesPerSec']; - // } - } - if (!empty($thisfile_riff_raw['fmt ']['nAvgBytesPerSec'])) { - $thisfile_audio['bitrate'] = getid3_lib::CastAsInt($thisfile_riff_raw['fmt ']['nAvgBytesPerSec'] * 8); - } - - if (isset($thisfile_riff_WAVE['bext'][0]['data'])) { - // shortcut - $thisfile_riff_WAVE_bext_0 = &$thisfile_riff_WAVE['bext'][0]; - - $thisfile_riff_WAVE_bext_0['title'] = trim(substr($thisfile_riff_WAVE_bext_0['data'], 0, 256)); - $thisfile_riff_WAVE_bext_0['author'] = trim(substr($thisfile_riff_WAVE_bext_0['data'], 256, 32)); - $thisfile_riff_WAVE_bext_0['reference'] = trim(substr($thisfile_riff_WAVE_bext_0['data'], 288, 32)); - $thisfile_riff_WAVE_bext_0['origin_date'] = substr($thisfile_riff_WAVE_bext_0['data'], 320, 10); - $thisfile_riff_WAVE_bext_0['origin_time'] = substr($thisfile_riff_WAVE_bext_0['data'], 330, 8); - $thisfile_riff_WAVE_bext_0['time_reference'] = getid3_lib::LittleEndian2Int(substr($thisfile_riff_WAVE_bext_0['data'], 338, 8)); - $thisfile_riff_WAVE_bext_0['bwf_version'] = getid3_lib::LittleEndian2Int(substr($thisfile_riff_WAVE_bext_0['data'], 346, 1)); - $thisfile_riff_WAVE_bext_0['reserved'] = substr($thisfile_riff_WAVE_bext_0['data'], 347, 254); - $thisfile_riff_WAVE_bext_0['coding_history'] = explode("\r\n", trim(substr($thisfile_riff_WAVE_bext_0['data'], 601))); - if (preg_match('#^([0-9]{4}).([0-9]{2}).([0-9]{2})$#', $thisfile_riff_WAVE_bext_0['origin_date'], $matches_bext_date)) { - if (preg_match('#^([0-9]{2}).([0-9]{2}).([0-9]{2})$#', $thisfile_riff_WAVE_bext_0['origin_time'], $matches_bext_time)) { - list($dummy, $bext_timestamp['year'], $bext_timestamp['month'], $bext_timestamp['day']) = $matches_bext_date; - list($dummy, $bext_timestamp['hour'], $bext_timestamp['minute'], $bext_timestamp['second']) = $matches_bext_time; - $thisfile_riff_WAVE_bext_0['origin_date_unix'] = gmmktime($bext_timestamp['hour'], $bext_timestamp['minute'], $bext_timestamp['second'], $bext_timestamp['month'], $bext_timestamp['day'], $bext_timestamp['year']); - } else { - $info['warning'][] = 'RIFF.WAVE.BEXT.origin_time is invalid'; - } - } else { - $info['warning'][] = 'RIFF.WAVE.BEXT.origin_date is invalid'; - } - $thisfile_riff['comments']['author'][] = $thisfile_riff_WAVE_bext_0['author']; - $thisfile_riff['comments']['title'][] = $thisfile_riff_WAVE_bext_0['title']; - } - - if (isset($thisfile_riff_WAVE['MEXT'][0]['data'])) { - // shortcut - $thisfile_riff_WAVE_MEXT_0 = &$thisfile_riff_WAVE['MEXT'][0]; - - $thisfile_riff_WAVE_MEXT_0['raw']['sound_information'] = getid3_lib::LittleEndian2Int(substr($thisfile_riff_WAVE_MEXT_0['data'], 0, 2)); - $thisfile_riff_WAVE_MEXT_0['flags']['homogenous'] = (bool) ($thisfile_riff_WAVE_MEXT_0['raw']['sound_information'] & 0x0001); - if ($thisfile_riff_WAVE_MEXT_0['flags']['homogenous']) { - $thisfile_riff_WAVE_MEXT_0['flags']['padding'] = ($thisfile_riff_WAVE_MEXT_0['raw']['sound_information'] & 0x0002) ? false : true; - $thisfile_riff_WAVE_MEXT_0['flags']['22_or_44'] = (bool) ($thisfile_riff_WAVE_MEXT_0['raw']['sound_information'] & 0x0004); - $thisfile_riff_WAVE_MEXT_0['flags']['free_format'] = (bool) ($thisfile_riff_WAVE_MEXT_0['raw']['sound_information'] & 0x0008); - - $thisfile_riff_WAVE_MEXT_0['nominal_frame_size'] = getid3_lib::LittleEndian2Int(substr($thisfile_riff_WAVE_MEXT_0['data'], 2, 2)); - } - $thisfile_riff_WAVE_MEXT_0['anciliary_data_length'] = getid3_lib::LittleEndian2Int(substr($thisfile_riff_WAVE_MEXT_0['data'], 6, 2)); - $thisfile_riff_WAVE_MEXT_0['raw']['anciliary_data_def'] = getid3_lib::LittleEndian2Int(substr($thisfile_riff_WAVE_MEXT_0['data'], 8, 2)); - $thisfile_riff_WAVE_MEXT_0['flags']['anciliary_data_left'] = (bool) ($thisfile_riff_WAVE_MEXT_0['raw']['anciliary_data_def'] & 0x0001); - $thisfile_riff_WAVE_MEXT_0['flags']['anciliary_data_free'] = (bool) ($thisfile_riff_WAVE_MEXT_0['raw']['anciliary_data_def'] & 0x0002); - $thisfile_riff_WAVE_MEXT_0['flags']['anciliary_data_right'] = (bool) ($thisfile_riff_WAVE_MEXT_0['raw']['anciliary_data_def'] & 0x0004); - } - - if (isset($thisfile_riff_WAVE['cart'][0]['data'])) { - // shortcut - $thisfile_riff_WAVE_cart_0 = &$thisfile_riff_WAVE['cart'][0]; - - $thisfile_riff_WAVE_cart_0['version'] = substr($thisfile_riff_WAVE_cart_0['data'], 0, 4); - $thisfile_riff_WAVE_cart_0['title'] = trim(substr($thisfile_riff_WAVE_cart_0['data'], 4, 64)); - $thisfile_riff_WAVE_cart_0['artist'] = trim(substr($thisfile_riff_WAVE_cart_0['data'], 68, 64)); - $thisfile_riff_WAVE_cart_0['cut_id'] = trim(substr($thisfile_riff_WAVE_cart_0['data'], 132, 64)); - $thisfile_riff_WAVE_cart_0['client_id'] = trim(substr($thisfile_riff_WAVE_cart_0['data'], 196, 64)); - $thisfile_riff_WAVE_cart_0['category'] = trim(substr($thisfile_riff_WAVE_cart_0['data'], 260, 64)); - $thisfile_riff_WAVE_cart_0['classification'] = trim(substr($thisfile_riff_WAVE_cart_0['data'], 324, 64)); - $thisfile_riff_WAVE_cart_0['out_cue'] = trim(substr($thisfile_riff_WAVE_cart_0['data'], 388, 64)); - $thisfile_riff_WAVE_cart_0['start_date'] = trim(substr($thisfile_riff_WAVE_cart_0['data'], 452, 10)); - $thisfile_riff_WAVE_cart_0['start_time'] = trim(substr($thisfile_riff_WAVE_cart_0['data'], 462, 8)); - $thisfile_riff_WAVE_cart_0['end_date'] = trim(substr($thisfile_riff_WAVE_cart_0['data'], 470, 10)); - $thisfile_riff_WAVE_cart_0['end_time'] = trim(substr($thisfile_riff_WAVE_cart_0['data'], 480, 8)); - $thisfile_riff_WAVE_cart_0['producer_app_id'] = trim(substr($thisfile_riff_WAVE_cart_0['data'], 488, 64)); - $thisfile_riff_WAVE_cart_0['producer_app_version'] = trim(substr($thisfile_riff_WAVE_cart_0['data'], 552, 64)); - $thisfile_riff_WAVE_cart_0['user_defined_text'] = trim(substr($thisfile_riff_WAVE_cart_0['data'], 616, 64)); - $thisfile_riff_WAVE_cart_0['zero_db_reference'] = getid3_lib::LittleEndian2Int(substr($thisfile_riff_WAVE_cart_0['data'], 680, 4), true); - for ($i = 0; $i < 8; $i++) { - $thisfile_riff_WAVE_cart_0['post_time'][$i]['usage_fourcc'] = substr($thisfile_riff_WAVE_cart_0['data'], 684 + ($i * 8), 4); - $thisfile_riff_WAVE_cart_0['post_time'][$i]['timer_value'] = getid3_lib::LittleEndian2Int(substr($thisfile_riff_WAVE_cart_0['data'], 684 + ($i * 8) + 4, 4)); - } - $thisfile_riff_WAVE_cart_0['url'] = trim(substr($thisfile_riff_WAVE_cart_0['data'], 748, 1024)); - $thisfile_riff_WAVE_cart_0['tag_text'] = explode("\r\n", trim(substr($thisfile_riff_WAVE_cart_0['data'], 1772))); - - $thisfile_riff['comments']['artist'][] = $thisfile_riff_WAVE_cart_0['artist']; - $thisfile_riff['comments']['title'][] = $thisfile_riff_WAVE_cart_0['title']; - } - - if (isset($thisfile_riff_WAVE['SNDM'][0]['data'])) { - // SoundMiner metadata - - // shortcuts - $thisfile_riff_WAVE_SNDM_0 = &$thisfile_riff_WAVE['SNDM'][0]; - $thisfile_riff_WAVE_SNDM_0_data = &$thisfile_riff_WAVE_SNDM_0['data']; - $SNDM_startoffset = 0; - $SNDM_endoffset = $thisfile_riff_WAVE_SNDM_0['size']; - - while ($SNDM_startoffset < $SNDM_endoffset) { - $SNDM_thisTagOffset = 0; - $SNDM_thisTagSize = getid3_lib::BigEndian2Int(substr($thisfile_riff_WAVE_SNDM_0_data, $SNDM_startoffset + $SNDM_thisTagOffset, 4)); - $SNDM_thisTagOffset += 4; - $SNDM_thisTagKey = substr($thisfile_riff_WAVE_SNDM_0_data, $SNDM_startoffset + $SNDM_thisTagOffset, 4); - $SNDM_thisTagOffset += 4; - $SNDM_thisTagDataSize = getid3_lib::BigEndian2Int(substr($thisfile_riff_WAVE_SNDM_0_data, $SNDM_startoffset + $SNDM_thisTagOffset, 2)); - $SNDM_thisTagOffset += 2; - $SNDM_thisTagDataFlags = getid3_lib::BigEndian2Int(substr($thisfile_riff_WAVE_SNDM_0_data, $SNDM_startoffset + $SNDM_thisTagOffset, 2)); - $SNDM_thisTagOffset += 2; - $SNDM_thisTagDataText = substr($thisfile_riff_WAVE_SNDM_0_data, $SNDM_startoffset + $SNDM_thisTagOffset, $SNDM_thisTagDataSize); - $SNDM_thisTagOffset += $SNDM_thisTagDataSize; - - if ($SNDM_thisTagSize != (4 + 4 + 2 + 2 + $SNDM_thisTagDataSize)) { - $info['warning'][] = 'RIFF.WAVE.SNDM.data contains tag not expected length (expected: '.$SNDM_thisTagSize.', found: '.(4 + 4 + 2 + 2 + $SNDM_thisTagDataSize).') at offset '.$SNDM_startoffset.' (file offset '.($thisfile_riff_WAVE_SNDM_0['offset'] + $SNDM_startoffset).')'; - break; - } elseif ($SNDM_thisTagSize <= 0) { - $info['warning'][] = 'RIFF.WAVE.SNDM.data contains zero-size tag at offset '.$SNDM_startoffset.' (file offset '.($thisfile_riff_WAVE_SNDM_0['offset'] + $SNDM_startoffset).')'; - break; - } - $SNDM_startoffset += $SNDM_thisTagSize; - - $thisfile_riff_WAVE_SNDM_0['parsed_raw'][$SNDM_thisTagKey] = $SNDM_thisTagDataText; - if ($parsedkey = self::waveSNDMtagLookup($SNDM_thisTagKey)) { - $thisfile_riff_WAVE_SNDM_0['parsed'][$parsedkey] = $SNDM_thisTagDataText; - } else { - $info['warning'][] = 'RIFF.WAVE.SNDM contains unknown tag "'.$SNDM_thisTagKey.'" at offset '.$SNDM_startoffset.' (file offset '.($thisfile_riff_WAVE_SNDM_0['offset'] + $SNDM_startoffset).')'; - } - } - - $tagmapping = array( - 'tracktitle'=>'title', - 'category' =>'genre', - 'cdtitle' =>'album', - 'tracktitle'=>'title', - ); - foreach ($tagmapping as $fromkey => $tokey) { - if (isset($thisfile_riff_WAVE_SNDM_0['parsed'][$fromkey])) { - $thisfile_riff['comments'][$tokey][] = $thisfile_riff_WAVE_SNDM_0['parsed'][$fromkey]; - } - } - } - - if (isset($thisfile_riff_WAVE['iXML'][0]['data'])) { - // requires functions simplexml_load_string and get_object_vars - if ($parsedXML = getid3_lib::XML2array($thisfile_riff_WAVE['iXML'][0]['data'])) { - $thisfile_riff_WAVE['iXML'][0]['parsed'] = $parsedXML; - if (isset($parsedXML['SPEED']['MASTER_SPEED'])) { - @list($numerator, $denominator) = explode('/', $parsedXML['SPEED']['MASTER_SPEED']); - $thisfile_riff_WAVE['iXML'][0]['master_speed'] = $numerator / ($denominator ? $denominator : 1000); - } - if (isset($parsedXML['SPEED']['TIMECODE_RATE'])) { - @list($numerator, $denominator) = explode('/', $parsedXML['SPEED']['TIMECODE_RATE']); - $thisfile_riff_WAVE['iXML'][0]['timecode_rate'] = $numerator / ($denominator ? $denominator : 1000); - } - if (isset($parsedXML['SPEED']['TIMESTAMP_SAMPLES_SINCE_MIDNIGHT_LO']) && !empty($parsedXML['SPEED']['TIMESTAMP_SAMPLE_RATE']) && !empty($thisfile_riff_WAVE['iXML'][0]['timecode_rate'])) { - $samples_since_midnight = floatval(ltrim($parsedXML['SPEED']['TIMESTAMP_SAMPLES_SINCE_MIDNIGHT_HI'].$parsedXML['SPEED']['TIMESTAMP_SAMPLES_SINCE_MIDNIGHT_LO'], '0')); - $thisfile_riff_WAVE['iXML'][0]['timecode_seconds'] = $samples_since_midnight / $parsedXML['SPEED']['TIMESTAMP_SAMPLE_RATE']; - $h = floor( $thisfile_riff_WAVE['iXML'][0]['timecode_seconds'] / 3600); - $m = floor(($thisfile_riff_WAVE['iXML'][0]['timecode_seconds'] - ($h * 3600)) / 60); - $s = floor( $thisfile_riff_WAVE['iXML'][0]['timecode_seconds'] - ($h * 3600) - ($m * 60)); - $f = ($thisfile_riff_WAVE['iXML'][0]['timecode_seconds'] - ($h * 3600) - ($m * 60) - $s) * $thisfile_riff_WAVE['iXML'][0]['timecode_rate']; - $thisfile_riff_WAVE['iXML'][0]['timecode_string'] = sprintf('%02d:%02d:%02d:%05.2f', $h, $m, $s, $f); - $thisfile_riff_WAVE['iXML'][0]['timecode_string_round'] = sprintf('%02d:%02d:%02d:%02d', $h, $m, $s, round($f)); - } - unset($parsedXML); - } - } - - - - if (!isset($thisfile_audio['bitrate']) && isset($thisfile_riff_audio[$streamindex]['bitrate'])) { - $thisfile_audio['bitrate'] = $thisfile_riff_audio[$streamindex]['bitrate']; - $info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $thisfile_audio['bitrate']); - } - - if (!empty($info['wavpack'])) { - $thisfile_audio_dataformat = 'wavpack'; - $thisfile_audio['bitrate_mode'] = 'vbr'; - $thisfile_audio['encoder'] = 'WavPack v'.$info['wavpack']['version']; - - // Reset to the way it was - RIFF parsing will have messed this up - $info['avdataend'] = $Original['avdataend']; - $thisfile_audio['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; - - $this->fseek($info['avdataoffset'] - 44); - $RIFFdata = $this->fread(44); - $OrignalRIFFheaderSize = getid3_lib::LittleEndian2Int(substr($RIFFdata, 4, 4)) + 8; - $OrignalRIFFdataSize = getid3_lib::LittleEndian2Int(substr($RIFFdata, 40, 4)) + 44; - - if ($OrignalRIFFheaderSize > $OrignalRIFFdataSize) { - $info['avdataend'] -= ($OrignalRIFFheaderSize - $OrignalRIFFdataSize); - $this->fseek($info['avdataend']); - $RIFFdata .= $this->fread($OrignalRIFFheaderSize - $OrignalRIFFdataSize); - } - - // move the data chunk after all other chunks (if any) - // so that the RIFF parser doesn't see EOF when trying - // to skip over the data chunk - $RIFFdata = substr($RIFFdata, 0, 36).substr($RIFFdata, 44).substr($RIFFdata, 36, 8); - $getid3_riff = new getid3_riff($this->getid3); - $getid3_riff->ParseRIFFdata($RIFFdata); - unset($getid3_riff); - } - - if (isset($thisfile_riff_raw['fmt ']['wFormatTag'])) { - switch ($thisfile_riff_raw['fmt ']['wFormatTag']) { - case 0x0001: // PCM - if (!empty($info['ac3'])) { - // Dolby Digital WAV files masquerade as PCM-WAV, but they're not - $thisfile_audio['wformattag'] = 0x2000; - $thisfile_audio['codec'] = self::wFormatTagLookup($thisfile_audio['wformattag']); - $thisfile_audio['lossless'] = false; - $thisfile_audio['bitrate'] = $info['ac3']['bitrate']; - $thisfile_audio['sample_rate'] = $info['ac3']['sample_rate']; - } - if (!empty($info['dts'])) { - // Dolby DTS files masquerade as PCM-WAV, but they're not - $thisfile_audio['wformattag'] = 0x2001; - $thisfile_audio['codec'] = self::wFormatTagLookup($thisfile_audio['wformattag']); - $thisfile_audio['lossless'] = false; - $thisfile_audio['bitrate'] = $info['dts']['bitrate']; - $thisfile_audio['sample_rate'] = $info['dts']['sample_rate']; - } - break; - case 0x08AE: // ClearJump LiteWave - $thisfile_audio['bitrate_mode'] = 'vbr'; - $thisfile_audio_dataformat = 'litewave'; - - //typedef struct tagSLwFormat { - // WORD m_wCompFormat; // low byte defines compression method, high byte is compression flags - // DWORD m_dwScale; // scale factor for lossy compression - // DWORD m_dwBlockSize; // number of samples in encoded blocks - // WORD m_wQuality; // alias for the scale factor - // WORD m_wMarkDistance; // distance between marks in bytes - // WORD m_wReserved; - // - // //following paramters are ignored if CF_FILESRC is not set - // DWORD m_dwOrgSize; // original file size in bytes - // WORD m_bFactExists; // indicates if 'fact' chunk exists in the original file - // DWORD m_dwRiffChunkSize; // riff chunk size in the original file - // - // PCMWAVEFORMAT m_OrgWf; // original wave format - // }SLwFormat, *PSLwFormat; - - // shortcut - $thisfile_riff['litewave']['raw'] = array(); - $riff_litewave = &$thisfile_riff['litewave']; - $riff_litewave_raw = &$riff_litewave['raw']; - - $flags = array( - 'compression_method' => 1, - 'compression_flags' => 1, - 'm_dwScale' => 4, - 'm_dwBlockSize' => 4, - 'm_wQuality' => 2, - 'm_wMarkDistance' => 2, - 'm_wReserved' => 2, - 'm_dwOrgSize' => 4, - 'm_bFactExists' => 2, - 'm_dwRiffChunkSize' => 4, - ); - $litewave_offset = 18; - foreach ($flags as $flag => $length) { - $riff_litewave_raw[$flag] = getid3_lib::LittleEndian2Int(substr($thisfile_riff_WAVE['fmt '][0]['data'], $litewave_offset, $length)); - $litewave_offset += $length; - } - - //$riff_litewave['quality_factor'] = intval(round((2000 - $riff_litewave_raw['m_dwScale']) / 20)); - $riff_litewave['quality_factor'] = $riff_litewave_raw['m_wQuality']; - - $riff_litewave['flags']['raw_source'] = ($riff_litewave_raw['compression_flags'] & 0x01) ? false : true; - $riff_litewave['flags']['vbr_blocksize'] = ($riff_litewave_raw['compression_flags'] & 0x02) ? false : true; - $riff_litewave['flags']['seekpoints'] = (bool) ($riff_litewave_raw['compression_flags'] & 0x04); - - $thisfile_audio['lossless'] = (($riff_litewave_raw['m_wQuality'] == 100) ? true : false); - $thisfile_audio['encoder_options'] = '-q'.$riff_litewave['quality_factor']; - break; - - default: - break; - } - } - if ($info['avdataend'] > $info['filesize']) { - switch (!empty($thisfile_audio_dataformat) ? $thisfile_audio_dataformat : '') { - case 'wavpack': // WavPack - case 'lpac': // LPAC - case 'ofr': // OptimFROG - case 'ofs': // OptimFROG DualStream - // lossless compressed audio formats that keep original RIFF headers - skip warning - break; - - case 'litewave': - if (($info['avdataend'] - $info['filesize']) == 1) { - // LiteWave appears to incorrectly *not* pad actual output file - // to nearest WORD boundary so may appear to be short by one - // byte, in which case - skip warning - } else { - // Short by more than one byte, throw warning - $info['warning'][] = 'Probably truncated file - expecting '.$thisfile_riff[$RIFFsubtype]['data'][0]['size'].' bytes of data, only found '.($info['filesize'] - $info['avdataoffset']).' (short by '.($thisfile_riff[$RIFFsubtype]['data'][0]['size'] - ($info['filesize'] - $info['avdataoffset'])).' bytes)'; - $info['avdataend'] = $info['filesize']; - } - break; - - default: - if ((($info['avdataend'] - $info['filesize']) == 1) && (($thisfile_riff[$RIFFsubtype]['data'][0]['size'] % 2) == 0) && ((($info['filesize'] - $info['avdataoffset']) % 2) == 1)) { - // output file appears to be incorrectly *not* padded to nearest WORD boundary - // Output less severe warning - $info['warning'][] = 'File should probably be padded to nearest WORD boundary, but it is not (expecting '.$thisfile_riff[$RIFFsubtype]['data'][0]['size'].' bytes of data, only found '.($info['filesize'] - $info['avdataoffset']).' therefore short by '.($thisfile_riff[$RIFFsubtype]['data'][0]['size'] - ($info['filesize'] - $info['avdataoffset'])).' bytes)'; - $info['avdataend'] = $info['filesize']; - } else { - // Short by more than one byte, throw warning - $info['warning'][] = 'Probably truncated file - expecting '.$thisfile_riff[$RIFFsubtype]['data'][0]['size'].' bytes of data, only found '.($info['filesize'] - $info['avdataoffset']).' (short by '.($thisfile_riff[$RIFFsubtype]['data'][0]['size'] - ($info['filesize'] - $info['avdataoffset'])).' bytes)'; - $info['avdataend'] = $info['filesize']; - } - break; - } - } - if (!empty($info['mpeg']['audio']['LAME']['audio_bytes'])) { - if ((($info['avdataend'] - $info['avdataoffset']) - $info['mpeg']['audio']['LAME']['audio_bytes']) == 1) { - $info['avdataend']--; - $info['warning'][] = 'Extra null byte at end of MP3 data assumed to be RIFF padding and therefore ignored'; - } - } - if (isset($thisfile_audio_dataformat) && ($thisfile_audio_dataformat == 'ac3')) { - unset($thisfile_audio['bits_per_sample']); - if (!empty($info['ac3']['bitrate']) && ($info['ac3']['bitrate'] != $thisfile_audio['bitrate'])) { - $thisfile_audio['bitrate'] = $info['ac3']['bitrate']; - } - } - break; - - case 'AVI ': - $thisfile_video['bitrate_mode'] = 'vbr'; // maybe not, but probably - $thisfile_video['dataformat'] = 'avi'; - $info['mime_type'] = 'video/avi'; - - if (isset($thisfile_riff[$RIFFsubtype]['movi']['offset'])) { - $info['avdataoffset'] = $thisfile_riff[$RIFFsubtype]['movi']['offset'] + 8; - if (isset($thisfile_riff['AVIX'])) { - $info['avdataend'] = $thisfile_riff['AVIX'][(count($thisfile_riff['AVIX']) - 1)]['chunks']['movi']['offset'] + $thisfile_riff['AVIX'][(count($thisfile_riff['AVIX']) - 1)]['chunks']['movi']['size']; - } else { - $info['avdataend'] = $thisfile_riff['AVI ']['movi']['offset'] + $thisfile_riff['AVI ']['movi']['size']; - } - if ($info['avdataend'] > $info['filesize']) { - $info['warning'][] = 'Probably truncated file - expecting '.($info['avdataend'] - $info['avdataoffset']).' bytes of data, only found '.($info['filesize'] - $info['avdataoffset']).' (short by '.($info['avdataend'] - $info['filesize']).' bytes)'; - $info['avdataend'] = $info['filesize']; - } - } - - if (isset($thisfile_riff['AVI ']['hdrl']['strl']['indx'])) { - //$bIndexType = array( - // 0x00 => 'AVI_INDEX_OF_INDEXES', - // 0x01 => 'AVI_INDEX_OF_CHUNKS', - // 0x80 => 'AVI_INDEX_IS_DATA', - //); - //$bIndexSubtype = array( - // 0x01 => array( - // 0x01 => 'AVI_INDEX_2FIELD', - // ), - //); - foreach ($thisfile_riff['AVI ']['hdrl']['strl']['indx'] as $streamnumber => $steamdataarray) { - $ahsisd = &$thisfile_riff['AVI ']['hdrl']['strl']['indx'][$streamnumber]['data']; - - $thisfile_riff_raw['indx'][$streamnumber]['wLongsPerEntry'] = $this->EitherEndian2Int(substr($ahsisd, 0, 2)); - $thisfile_riff_raw['indx'][$streamnumber]['bIndexSubType'] = $this->EitherEndian2Int(substr($ahsisd, 2, 1)); - $thisfile_riff_raw['indx'][$streamnumber]['bIndexType'] = $this->EitherEndian2Int(substr($ahsisd, 3, 1)); - $thisfile_riff_raw['indx'][$streamnumber]['nEntriesInUse'] = $this->EitherEndian2Int(substr($ahsisd, 4, 4)); - $thisfile_riff_raw['indx'][$streamnumber]['dwChunkId'] = substr($ahsisd, 8, 4); - $thisfile_riff_raw['indx'][$streamnumber]['dwReserved'] = $this->EitherEndian2Int(substr($ahsisd, 12, 4)); - - //$thisfile_riff_raw['indx'][$streamnumber]['bIndexType_name'] = $bIndexType[$thisfile_riff_raw['indx'][$streamnumber]['bIndexType']]; - //$thisfile_riff_raw['indx'][$streamnumber]['bIndexSubType_name'] = $bIndexSubtype[$thisfile_riff_raw['indx'][$streamnumber]['bIndexType']][$thisfile_riff_raw['indx'][$streamnumber]['bIndexSubType']]; - - unset($ahsisd); - } - } - if (isset($thisfile_riff['AVI ']['hdrl']['avih'][$streamindex]['data'])) { - $avihData = $thisfile_riff['AVI ']['hdrl']['avih'][$streamindex]['data']; - - // shortcut - $thisfile_riff_raw['avih'] = array(); - $thisfile_riff_raw_avih = &$thisfile_riff_raw['avih']; - - $thisfile_riff_raw_avih['dwMicroSecPerFrame'] = $this->EitherEndian2Int(substr($avihData, 0, 4)); // frame display rate (or 0L) - if ($thisfile_riff_raw_avih['dwMicroSecPerFrame'] == 0) { - $info['error'][] = 'Corrupt RIFF file: avih.dwMicroSecPerFrame == zero'; - return false; - } - - $flags = array( - 'dwMaxBytesPerSec', // max. transfer rate - 'dwPaddingGranularity', // pad to multiples of this size; normally 2K. - 'dwFlags', // the ever-present flags - 'dwTotalFrames', // # frames in file - 'dwInitialFrames', // - 'dwStreams', // - 'dwSuggestedBufferSize', // - 'dwWidth', // - 'dwHeight', // - 'dwScale', // - 'dwRate', // - 'dwStart', // - 'dwLength', // - ); - $avih_offset = 4; - foreach ($flags as $flag) { - $thisfile_riff_raw_avih[$flag] = $this->EitherEndian2Int(substr($avihData, $avih_offset, 4)); - $avih_offset += 4; - } - - $flags = array( - 'hasindex' => 0x00000010, - 'mustuseindex' => 0x00000020, - 'interleaved' => 0x00000100, - 'trustcktype' => 0x00000800, - 'capturedfile' => 0x00010000, - 'copyrighted' => 0x00020010, - ); - foreach ($flags as $flag => $value) { - $thisfile_riff_raw_avih['flags'][$flag] = (bool) ($thisfile_riff_raw_avih['dwFlags'] & $value); - } - - // shortcut - $thisfile_riff_video[$streamindex] = array(); - $thisfile_riff_video_current = &$thisfile_riff_video[$streamindex]; - - if ($thisfile_riff_raw_avih['dwWidth'] > 0) { - $thisfile_riff_video_current['frame_width'] = $thisfile_riff_raw_avih['dwWidth']; - $thisfile_video['resolution_x'] = $thisfile_riff_video_current['frame_width']; - } - if ($thisfile_riff_raw_avih['dwHeight'] > 0) { - $thisfile_riff_video_current['frame_height'] = $thisfile_riff_raw_avih['dwHeight']; - $thisfile_video['resolution_y'] = $thisfile_riff_video_current['frame_height']; - } - if ($thisfile_riff_raw_avih['dwTotalFrames'] > 0) { - $thisfile_riff_video_current['total_frames'] = $thisfile_riff_raw_avih['dwTotalFrames']; - $thisfile_video['total_frames'] = $thisfile_riff_video_current['total_frames']; - } - - $thisfile_riff_video_current['frame_rate'] = round(1000000 / $thisfile_riff_raw_avih['dwMicroSecPerFrame'], 3); - $thisfile_video['frame_rate'] = $thisfile_riff_video_current['frame_rate']; - } - if (isset($thisfile_riff['AVI ']['hdrl']['strl']['strh'][0]['data'])) { - if (is_array($thisfile_riff['AVI ']['hdrl']['strl']['strh'])) { - for ($i = 0; $i < count($thisfile_riff['AVI ']['hdrl']['strl']['strh']); $i++) { - if (isset($thisfile_riff['AVI ']['hdrl']['strl']['strh'][$i]['data'])) { - $strhData = $thisfile_riff['AVI ']['hdrl']['strl']['strh'][$i]['data']; - $strhfccType = substr($strhData, 0, 4); - - if (isset($thisfile_riff['AVI ']['hdrl']['strl']['strf'][$i]['data'])) { - $strfData = $thisfile_riff['AVI ']['hdrl']['strl']['strf'][$i]['data']; - - // shortcut - $thisfile_riff_raw_strf_strhfccType_streamindex = &$thisfile_riff_raw['strf'][$strhfccType][$streamindex]; - - switch ($strhfccType) { - case 'auds': - $thisfile_audio['bitrate_mode'] = 'cbr'; - $thisfile_audio_dataformat = 'wav'; - if (isset($thisfile_riff_audio) && is_array($thisfile_riff_audio)) { - $streamindex = count($thisfile_riff_audio); - } - - $thisfile_riff_audio[$streamindex] = self::parseWAVEFORMATex($strfData); - $thisfile_audio['wformattag'] = $thisfile_riff_audio[$streamindex]['raw']['wFormatTag']; - - // shortcut - $thisfile_audio['streams'][$streamindex] = $thisfile_riff_audio[$streamindex]; - $thisfile_audio_streams_currentstream = &$thisfile_audio['streams'][$streamindex]; - - if ($thisfile_audio_streams_currentstream['bits_per_sample'] == 0) { - unset($thisfile_audio_streams_currentstream['bits_per_sample']); - } - $thisfile_audio_streams_currentstream['wformattag'] = $thisfile_audio_streams_currentstream['raw']['wFormatTag']; - unset($thisfile_audio_streams_currentstream['raw']); - - // shortcut - $thisfile_riff_raw['strf'][$strhfccType][$streamindex] = $thisfile_riff_audio[$streamindex]['raw']; - - unset($thisfile_riff_audio[$streamindex]['raw']); - $thisfile_audio = getid3_lib::array_merge_noclobber($thisfile_audio, $thisfile_riff_audio[$streamindex]); - - $thisfile_audio['lossless'] = false; - switch ($thisfile_riff_raw_strf_strhfccType_streamindex['wFormatTag']) { - case 0x0001: // PCM - $thisfile_audio_dataformat = 'wav'; - $thisfile_audio['lossless'] = true; - break; - - case 0x0050: // MPEG Layer 2 or Layer 1 - $thisfile_audio_dataformat = 'mp2'; // Assume Layer-2 - break; - - case 0x0055: // MPEG Layer 3 - $thisfile_audio_dataformat = 'mp3'; - break; - - case 0x00FF: // AAC - $thisfile_audio_dataformat = 'aac'; - break; - - case 0x0161: // Windows Media v7 / v8 / v9 - case 0x0162: // Windows Media Professional v9 - case 0x0163: // Windows Media Lossess v9 - $thisfile_audio_dataformat = 'wma'; - break; - - case 0x2000: // AC-3 - $thisfile_audio_dataformat = 'ac3'; - break; - - case 0x2001: // DTS - $thisfile_audio_dataformat = 'dts'; - break; - - default: - $thisfile_audio_dataformat = 'wav'; - break; - } - $thisfile_audio_streams_currentstream['dataformat'] = $thisfile_audio_dataformat; - $thisfile_audio_streams_currentstream['lossless'] = $thisfile_audio['lossless']; - $thisfile_audio_streams_currentstream['bitrate_mode'] = $thisfile_audio['bitrate_mode']; - break; - - - case 'iavs': - case 'vids': - // shortcut - $thisfile_riff_raw['strh'][$i] = array(); - $thisfile_riff_raw_strh_current = &$thisfile_riff_raw['strh'][$i]; - - $thisfile_riff_raw_strh_current['fccType'] = substr($strhData, 0, 4); // same as $strhfccType; - $thisfile_riff_raw_strh_current['fccHandler'] = substr($strhData, 4, 4); - $thisfile_riff_raw_strh_current['dwFlags'] = $this->EitherEndian2Int(substr($strhData, 8, 4)); // Contains AVITF_* flags - $thisfile_riff_raw_strh_current['wPriority'] = $this->EitherEndian2Int(substr($strhData, 12, 2)); - $thisfile_riff_raw_strh_current['wLanguage'] = $this->EitherEndian2Int(substr($strhData, 14, 2)); - $thisfile_riff_raw_strh_current['dwInitialFrames'] = $this->EitherEndian2Int(substr($strhData, 16, 4)); - $thisfile_riff_raw_strh_current['dwScale'] = $this->EitherEndian2Int(substr($strhData, 20, 4)); - $thisfile_riff_raw_strh_current['dwRate'] = $this->EitherEndian2Int(substr($strhData, 24, 4)); - $thisfile_riff_raw_strh_current['dwStart'] = $this->EitherEndian2Int(substr($strhData, 28, 4)); - $thisfile_riff_raw_strh_current['dwLength'] = $this->EitherEndian2Int(substr($strhData, 32, 4)); - $thisfile_riff_raw_strh_current['dwSuggestedBufferSize'] = $this->EitherEndian2Int(substr($strhData, 36, 4)); - $thisfile_riff_raw_strh_current['dwQuality'] = $this->EitherEndian2Int(substr($strhData, 40, 4)); - $thisfile_riff_raw_strh_current['dwSampleSize'] = $this->EitherEndian2Int(substr($strhData, 44, 4)); - $thisfile_riff_raw_strh_current['rcFrame'] = $this->EitherEndian2Int(substr($strhData, 48, 4)); - - $thisfile_riff_video_current['codec'] = self::fourccLookup($thisfile_riff_raw_strh_current['fccHandler']); - $thisfile_video['fourcc'] = $thisfile_riff_raw_strh_current['fccHandler']; - if (!$thisfile_riff_video_current['codec'] && isset($thisfile_riff_raw_strf_strhfccType_streamindex['fourcc']) && self::fourccLookup($thisfile_riff_raw_strf_strhfccType_streamindex['fourcc'])) { - $thisfile_riff_video_current['codec'] = self::fourccLookup($thisfile_riff_raw_strf_strhfccType_streamindex['fourcc']); - $thisfile_video['fourcc'] = $thisfile_riff_raw_strf_strhfccType_streamindex['fourcc']; - } - $thisfile_video['codec'] = $thisfile_riff_video_current['codec']; - $thisfile_video['pixel_aspect_ratio'] = (float) 1; - switch ($thisfile_riff_raw_strh_current['fccHandler']) { - case 'HFYU': // Huffman Lossless Codec - case 'IRAW': // Intel YUV Uncompressed - case 'YUY2': // Uncompressed YUV 4:2:2 - $thisfile_video['lossless'] = true; - break; - - default: - $thisfile_video['lossless'] = false; - break; - } - - switch ($strhfccType) { - case 'vids': - $thisfile_riff_raw_strf_strhfccType_streamindex = self::ParseBITMAPINFOHEADER(substr($strfData, 0, 40), ($info['fileformat'] == 'riff')); - $thisfile_video['bits_per_sample'] = $thisfile_riff_raw_strf_strhfccType_streamindex['biBitCount']; - - if ($thisfile_riff_video_current['codec'] == 'DV') { - $thisfile_riff_video_current['dv_type'] = 2; - } - break; - - case 'iavs': - $thisfile_riff_video_current['dv_type'] = 1; - break; - } - break; - - default: - $info['warning'][] = 'Unhandled fccType for stream ('.$i.'): "'.$strhfccType.'"'; - break; - - } - } - } - - if (isset($thisfile_riff_raw_strf_strhfccType_streamindex['fourcc'])) { - - $thisfile_video['fourcc'] = $thisfile_riff_raw_strf_strhfccType_streamindex['fourcc']; - if (self::fourccLookup($thisfile_video['fourcc'])) { - $thisfile_riff_video_current['codec'] = self::fourccLookup($thisfile_video['fourcc']); - $thisfile_video['codec'] = $thisfile_riff_video_current['codec']; - } - - switch ($thisfile_riff_raw_strf_strhfccType_streamindex['fourcc']) { - case 'HFYU': // Huffman Lossless Codec - case 'IRAW': // Intel YUV Uncompressed - case 'YUY2': // Uncompressed YUV 4:2:2 - $thisfile_video['lossless'] = true; - //$thisfile_video['bits_per_sample'] = 24; - break; - - default: - $thisfile_video['lossless'] = false; - //$thisfile_video['bits_per_sample'] = 24; - break; - } - - } - } - } - } - break; - - case 'CDDA': - $thisfile_audio['bitrate_mode'] = 'cbr'; - $thisfile_audio_dataformat = 'cda'; - $thisfile_audio['lossless'] = true; - unset($info['mime_type']); - - $info['avdataoffset'] = 44; - - if (isset($thisfile_riff['CDDA']['fmt '][0]['data'])) { - // shortcut - $thisfile_riff_CDDA_fmt_0 = &$thisfile_riff['CDDA']['fmt '][0]; - - $thisfile_riff_CDDA_fmt_0['unknown1'] = $this->EitherEndian2Int(substr($thisfile_riff_CDDA_fmt_0['data'], 0, 2)); - $thisfile_riff_CDDA_fmt_0['track_num'] = $this->EitherEndian2Int(substr($thisfile_riff_CDDA_fmt_0['data'], 2, 2)); - $thisfile_riff_CDDA_fmt_0['disc_id'] = $this->EitherEndian2Int(substr($thisfile_riff_CDDA_fmt_0['data'], 4, 4)); - $thisfile_riff_CDDA_fmt_0['start_offset_frame'] = $this->EitherEndian2Int(substr($thisfile_riff_CDDA_fmt_0['data'], 8, 4)); - $thisfile_riff_CDDA_fmt_0['playtime_frames'] = $this->EitherEndian2Int(substr($thisfile_riff_CDDA_fmt_0['data'], 12, 4)); - $thisfile_riff_CDDA_fmt_0['unknown6'] = $this->EitherEndian2Int(substr($thisfile_riff_CDDA_fmt_0['data'], 16, 4)); - $thisfile_riff_CDDA_fmt_0['unknown7'] = $this->EitherEndian2Int(substr($thisfile_riff_CDDA_fmt_0['data'], 20, 4)); - - $thisfile_riff_CDDA_fmt_0['start_offset_seconds'] = (float) $thisfile_riff_CDDA_fmt_0['start_offset_frame'] / 75; - $thisfile_riff_CDDA_fmt_0['playtime_seconds'] = (float) $thisfile_riff_CDDA_fmt_0['playtime_frames'] / 75; - $info['comments']['track'] = $thisfile_riff_CDDA_fmt_0['track_num']; - $info['playtime_seconds'] = $thisfile_riff_CDDA_fmt_0['playtime_seconds']; - - // hardcoded data for CD-audio - $thisfile_audio['sample_rate'] = 44100; - $thisfile_audio['channels'] = 2; - $thisfile_audio['bits_per_sample'] = 16; - $thisfile_audio['bitrate'] = $thisfile_audio['sample_rate'] * $thisfile_audio['channels'] * $thisfile_audio['bits_per_sample']; - $thisfile_audio['bitrate_mode'] = 'cbr'; - } - break; - - - case 'AIFF': - case 'AIFC': - $thisfile_audio['bitrate_mode'] = 'cbr'; - $thisfile_audio_dataformat = 'aiff'; - $thisfile_audio['lossless'] = true; - $info['mime_type'] = 'audio/x-aiff'; - - if (isset($thisfile_riff[$RIFFsubtype]['SSND'][0]['offset'])) { - $info['avdataoffset'] = $thisfile_riff[$RIFFsubtype]['SSND'][0]['offset'] + 8; - $info['avdataend'] = $info['avdataoffset'] + $thisfile_riff[$RIFFsubtype]['SSND'][0]['size']; - if ($info['avdataend'] > $info['filesize']) { - if (($info['avdataend'] == ($info['filesize'] + 1)) && (($info['filesize'] % 2) == 1)) { - // structures rounded to 2-byte boundary, but dumb encoders - // forget to pad end of file to make this actually work - } else { - $info['warning'][] = 'Probable truncated AIFF file: expecting '.$thisfile_riff[$RIFFsubtype]['SSND'][0]['size'].' bytes of audio data, only '.($info['filesize'] - $info['avdataoffset']).' bytes found'; - } - $info['avdataend'] = $info['filesize']; - } - } - - if (isset($thisfile_riff[$RIFFsubtype]['COMM'][0]['data'])) { - - // shortcut - $thisfile_riff_RIFFsubtype_COMM_0_data = &$thisfile_riff[$RIFFsubtype]['COMM'][0]['data']; - - $thisfile_riff_audio['channels'] = getid3_lib::BigEndian2Int(substr($thisfile_riff_RIFFsubtype_COMM_0_data, 0, 2), true); - $thisfile_riff_audio['total_samples'] = getid3_lib::BigEndian2Int(substr($thisfile_riff_RIFFsubtype_COMM_0_data, 2, 4), false); - $thisfile_riff_audio['bits_per_sample'] = getid3_lib::BigEndian2Int(substr($thisfile_riff_RIFFsubtype_COMM_0_data, 6, 2), true); - $thisfile_riff_audio['sample_rate'] = (int) getid3_lib::BigEndian2Float(substr($thisfile_riff_RIFFsubtype_COMM_0_data, 8, 10)); - - if ($thisfile_riff[$RIFFsubtype]['COMM'][0]['size'] > 18) { - $thisfile_riff_audio['codec_fourcc'] = substr($thisfile_riff_RIFFsubtype_COMM_0_data, 18, 4); - $CodecNameSize = getid3_lib::BigEndian2Int(substr($thisfile_riff_RIFFsubtype_COMM_0_data, 22, 1), false); - $thisfile_riff_audio['codec_name'] = substr($thisfile_riff_RIFFsubtype_COMM_0_data, 23, $CodecNameSize); - switch ($thisfile_riff_audio['codec_name']) { - case 'NONE': - $thisfile_audio['codec'] = 'Pulse Code Modulation (PCM)'; - $thisfile_audio['lossless'] = true; - break; - - case '': - switch ($thisfile_riff_audio['codec_fourcc']) { - // http://developer.apple.com/qa/snd/snd07.html - case 'sowt': - $thisfile_riff_audio['codec_name'] = 'Two\'s Compliment Little-Endian PCM'; - $thisfile_audio['lossless'] = true; - break; - - case 'twos': - $thisfile_riff_audio['codec_name'] = 'Two\'s Compliment Big-Endian PCM'; - $thisfile_audio['lossless'] = true; - break; - - default: - break; - } - break; - - default: - $thisfile_audio['codec'] = $thisfile_riff_audio['codec_name']; - $thisfile_audio['lossless'] = false; - break; - } - } - - $thisfile_audio['channels'] = $thisfile_riff_audio['channels']; - if ($thisfile_riff_audio['bits_per_sample'] > 0) { - $thisfile_audio['bits_per_sample'] = $thisfile_riff_audio['bits_per_sample']; - } - $thisfile_audio['sample_rate'] = $thisfile_riff_audio['sample_rate']; - if ($thisfile_audio['sample_rate'] == 0) { - $info['error'][] = 'Corrupted AIFF file: sample_rate == zero'; - return false; - } - $info['playtime_seconds'] = $thisfile_riff_audio['total_samples'] / $thisfile_audio['sample_rate']; - } - - if (isset($thisfile_riff[$RIFFsubtype]['COMT'])) { - $offset = 0; - $CommentCount = getid3_lib::BigEndian2Int(substr($thisfile_riff[$RIFFsubtype]['COMT'][0]['data'], $offset, 2), false); - $offset += 2; - for ($i = 0; $i < $CommentCount; $i++) { - $info['comments_raw'][$i]['timestamp'] = getid3_lib::BigEndian2Int(substr($thisfile_riff[$RIFFsubtype]['COMT'][0]['data'], $offset, 4), false); - $offset += 4; - $info['comments_raw'][$i]['marker_id'] = getid3_lib::BigEndian2Int(substr($thisfile_riff[$RIFFsubtype]['COMT'][0]['data'], $offset, 2), true); - $offset += 2; - $CommentLength = getid3_lib::BigEndian2Int(substr($thisfile_riff[$RIFFsubtype]['COMT'][0]['data'], $offset, 2), false); - $offset += 2; - $info['comments_raw'][$i]['comment'] = substr($thisfile_riff[$RIFFsubtype]['COMT'][0]['data'], $offset, $CommentLength); - $offset += $CommentLength; - - $info['comments_raw'][$i]['timestamp_unix'] = getid3_lib::DateMac2Unix($info['comments_raw'][$i]['timestamp']); - $thisfile_riff['comments']['comment'][] = $info['comments_raw'][$i]['comment']; - } - } - - $CommentsChunkNames = array('NAME'=>'title', 'author'=>'artist', '(c) '=>'copyright', 'ANNO'=>'comment'); - foreach ($CommentsChunkNames as $key => $value) { - if (isset($thisfile_riff[$RIFFsubtype][$key][0]['data'])) { - $thisfile_riff['comments'][$value][] = $thisfile_riff[$RIFFsubtype][$key][0]['data']; - } - } -/* - if (isset($thisfile_riff[$RIFFsubtype]['ID3 '])) { - getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v2.php', __FILE__, true); - $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); - $getid3_id3v2 = new getid3_id3v2($getid3_temp); - $getid3_id3v2->StartingOffset = $thisfile_riff[$RIFFsubtype]['ID3 '][0]['offset'] + 8; - if ($thisfile_riff[$RIFFsubtype]['ID3 '][0]['valid'] = $getid3_id3v2->Analyze()) { - $info['id3v2'] = $getid3_temp->info['id3v2']; - } - unset($getid3_temp, $getid3_id3v2); - } -*/ - break; - - case '8SVX': - $thisfile_audio['bitrate_mode'] = 'cbr'; - $thisfile_audio_dataformat = '8svx'; - $thisfile_audio['bits_per_sample'] = 8; - $thisfile_audio['channels'] = 1; // overridden below, if need be - $info['mime_type'] = 'audio/x-aiff'; - - if (isset($thisfile_riff[$RIFFsubtype]['BODY'][0]['offset'])) { - $info['avdataoffset'] = $thisfile_riff[$RIFFsubtype]['BODY'][0]['offset'] + 8; - $info['avdataend'] = $info['avdataoffset'] + $thisfile_riff[$RIFFsubtype]['BODY'][0]['size']; - if ($info['avdataend'] > $info['filesize']) { - $info['warning'][] = 'Probable truncated AIFF file: expecting '.$thisfile_riff[$RIFFsubtype]['BODY'][0]['size'].' bytes of audio data, only '.($info['filesize'] - $info['avdataoffset']).' bytes found'; - } - } - - if (isset($thisfile_riff[$RIFFsubtype]['VHDR'][0]['offset'])) { - // shortcut - $thisfile_riff_RIFFsubtype_VHDR_0 = &$thisfile_riff[$RIFFsubtype]['VHDR'][0]; - - $thisfile_riff_RIFFsubtype_VHDR_0['oneShotHiSamples'] = getid3_lib::BigEndian2Int(substr($thisfile_riff_RIFFsubtype_VHDR_0['data'], 0, 4)); - $thisfile_riff_RIFFsubtype_VHDR_0['repeatHiSamples'] = getid3_lib::BigEndian2Int(substr($thisfile_riff_RIFFsubtype_VHDR_0['data'], 4, 4)); - $thisfile_riff_RIFFsubtype_VHDR_0['samplesPerHiCycle'] = getid3_lib::BigEndian2Int(substr($thisfile_riff_RIFFsubtype_VHDR_0['data'], 8, 4)); - $thisfile_riff_RIFFsubtype_VHDR_0['samplesPerSec'] = getid3_lib::BigEndian2Int(substr($thisfile_riff_RIFFsubtype_VHDR_0['data'], 12, 2)); - $thisfile_riff_RIFFsubtype_VHDR_0['ctOctave'] = getid3_lib::BigEndian2Int(substr($thisfile_riff_RIFFsubtype_VHDR_0['data'], 14, 1)); - $thisfile_riff_RIFFsubtype_VHDR_0['sCompression'] = getid3_lib::BigEndian2Int(substr($thisfile_riff_RIFFsubtype_VHDR_0['data'], 15, 1)); - $thisfile_riff_RIFFsubtype_VHDR_0['Volume'] = getid3_lib::FixedPoint16_16(substr($thisfile_riff_RIFFsubtype_VHDR_0['data'], 16, 4)); - - $thisfile_audio['sample_rate'] = $thisfile_riff_RIFFsubtype_VHDR_0['samplesPerSec']; - - switch ($thisfile_riff_RIFFsubtype_VHDR_0['sCompression']) { - case 0: - $thisfile_audio['codec'] = 'Pulse Code Modulation (PCM)'; - $thisfile_audio['lossless'] = true; - $ActualBitsPerSample = 8; - break; - - case 1: - $thisfile_audio['codec'] = 'Fibonacci-delta encoding'; - $thisfile_audio['lossless'] = false; - $ActualBitsPerSample = 4; - break; - - default: - $info['warning'][] = 'Unexpected sCompression value in 8SVX.VHDR chunk - expecting 0 or 1, found "'.sCompression.'"'; - break; - } - } - - if (isset($thisfile_riff[$RIFFsubtype]['CHAN'][0]['data'])) { - $ChannelsIndex = getid3_lib::BigEndian2Int(substr($thisfile_riff[$RIFFsubtype]['CHAN'][0]['data'], 0, 4)); - switch ($ChannelsIndex) { - case 6: // Stereo - $thisfile_audio['channels'] = 2; - break; - - case 2: // Left channel only - case 4: // Right channel only - $thisfile_audio['channels'] = 1; - break; - - default: - $info['warning'][] = 'Unexpected value in 8SVX.CHAN chunk - expecting 2 or 4 or 6, found "'.$ChannelsIndex.'"'; - break; - } - - } - - $CommentsChunkNames = array('NAME'=>'title', 'author'=>'artist', '(c) '=>'copyright', 'ANNO'=>'comment'); - foreach ($CommentsChunkNames as $key => $value) { - if (isset($thisfile_riff[$RIFFsubtype][$key][0]['data'])) { - $thisfile_riff['comments'][$value][] = $thisfile_riff[$RIFFsubtype][$key][0]['data']; - } - } - - $thisfile_audio['bitrate'] = $thisfile_audio['sample_rate'] * $ActualBitsPerSample * $thisfile_audio['channels']; - if (!empty($thisfile_audio['bitrate'])) { - $info['playtime_seconds'] = ($info['avdataend'] - $info['avdataoffset']) / ($thisfile_audio['bitrate'] / 8); - } - break; - - - case 'CDXA': - $info['mime_type'] = 'video/mpeg'; - if (!empty($thisfile_riff['CDXA']['data'][0]['size'])) { - if (getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.mpeg.php', __FILE__, false)) { - $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); - $getid3_mpeg = new getid3_mpeg($getid3_temp); - $getid3_mpeg->Analyze(); - if (empty($getid3_temp->info['error'])) { - $info['audio'] = $getid3_temp->info['audio']; - $info['video'] = $getid3_temp->info['video']; - $info['mpeg'] = $getid3_temp->info['mpeg']; - $info['warning'] = $getid3_temp->info['warning']; - } - unset($getid3_temp, $getid3_mpeg); - } - } - break; - - - default: - $info['error'][] = 'Unknown RIFF type: expecting one of (WAVE|RMP3|AVI |CDDA|AIFF|AIFC|8SVX|CDXA), found "'.$RIFFsubtype.'" instead'; - unset($info['fileformat']); - break; - } - - switch ($RIFFsubtype) { - case 'WAVE': - case 'AIFF': - case 'AIFC': - $ID3v2_key_good = 'id3 '; - $ID3v2_keys_bad = array('ID3 ', 'tag '); - foreach ($ID3v2_keys_bad as $ID3v2_key_bad) { - if (isset($thisfile_riff[$RIFFsubtype][$ID3v2_key_bad]) && !array_key_exists($ID3v2_key_good, $thisfile_riff[$RIFFsubtype])) { - $thisfile_riff[$RIFFsubtype][$ID3v2_key_good] = $thisfile_riff[$RIFFsubtype][$ID3v2_key_bad]; - $info['warning'][] = 'mapping "'.$ID3v2_key_bad.'" chunk to "'.$ID3v2_key_good.'"'; - } - } - - if (isset($thisfile_riff[$RIFFsubtype]['id3 '])) { - getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v2.php', __FILE__, true); - $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); - $getid3_id3v2 = new getid3_id3v2($getid3_temp); - $getid3_id3v2->StartingOffset = $thisfile_riff[$RIFFsubtype]['id3 '][0]['offset'] + 8; - if ($thisfile_riff[$RIFFsubtype]['id3 '][0]['valid'] = $getid3_id3v2->Analyze()) { - $info['id3v2'] = $getid3_temp->info['id3v2']; - } - unset($getid3_temp, $getid3_id3v2); - } - break; - } - - if (isset($thisfile_riff_WAVE['DISP']) && is_array($thisfile_riff_WAVE['DISP'])) { - $thisfile_riff['comments']['title'][] = trim(substr($thisfile_riff_WAVE['DISP'][count($thisfile_riff_WAVE['DISP']) - 1]['data'], 4)); - } - if (isset($thisfile_riff_WAVE['INFO']) && is_array($thisfile_riff_WAVE['INFO'])) { - self::parseComments($thisfile_riff_WAVE['INFO'], $thisfile_riff['comments']); - } - if (isset($thisfile_riff['AVI ']['INFO']) && is_array($thisfile_riff['AVI ']['INFO'])) { - self::parseComments($thisfile_riff['AVI ']['INFO'], $thisfile_riff['comments']); - } - - if (empty($thisfile_audio['encoder']) && !empty($info['mpeg']['audio']['LAME']['short_version'])) { - $thisfile_audio['encoder'] = $info['mpeg']['audio']['LAME']['short_version']; - } - - if (!isset($info['playtime_seconds'])) { - $info['playtime_seconds'] = 0; - } - if (isset($thisfile_riff_raw['strh'][0]['dwLength']) && isset($thisfile_riff_raw['avih']['dwMicroSecPerFrame'])) { - // needed for >2GB AVIs where 'avih' chunk only lists number of frames in that chunk, not entire movie - $info['playtime_seconds'] = $thisfile_riff_raw['strh'][0]['dwLength'] * ($thisfile_riff_raw['avih']['dwMicroSecPerFrame'] / 1000000); - } elseif (isset($thisfile_riff_raw['avih']['dwTotalFrames']) && isset($thisfile_riff_raw['avih']['dwMicroSecPerFrame'])) { - $info['playtime_seconds'] = $thisfile_riff_raw['avih']['dwTotalFrames'] * ($thisfile_riff_raw['avih']['dwMicroSecPerFrame'] / 1000000); - } - - if ($info['playtime_seconds'] > 0) { - if (isset($thisfile_riff_audio) && isset($thisfile_riff_video)) { - - if (!isset($info['bitrate'])) { - $info['bitrate'] = ((($info['avdataend'] - $info['avdataoffset']) / $info['playtime_seconds']) * 8); - } - - } elseif (isset($thisfile_riff_audio) && !isset($thisfile_riff_video)) { - - if (!isset($thisfile_audio['bitrate'])) { - $thisfile_audio['bitrate'] = ((($info['avdataend'] - $info['avdataoffset']) / $info['playtime_seconds']) * 8); - } - - } elseif (!isset($thisfile_riff_audio) && isset($thisfile_riff_video)) { - - if (!isset($thisfile_video['bitrate'])) { - $thisfile_video['bitrate'] = ((($info['avdataend'] - $info['avdataoffset']) / $info['playtime_seconds']) * 8); - } - - } - } - - - if (isset($thisfile_riff_video) && isset($thisfile_audio['bitrate']) && ($thisfile_audio['bitrate'] > 0) && ($info['playtime_seconds'] > 0)) { - - $info['bitrate'] = ((($info['avdataend'] - $info['avdataoffset']) / $info['playtime_seconds']) * 8); - $thisfile_audio['bitrate'] = 0; - $thisfile_video['bitrate'] = $info['bitrate']; - foreach ($thisfile_riff_audio as $channelnumber => $audioinfoarray) { - $thisfile_video['bitrate'] -= $audioinfoarray['bitrate']; - $thisfile_audio['bitrate'] += $audioinfoarray['bitrate']; - } - if ($thisfile_video['bitrate'] <= 0) { - unset($thisfile_video['bitrate']); - } - if ($thisfile_audio['bitrate'] <= 0) { - unset($thisfile_audio['bitrate']); - } - } - - if (isset($info['mpeg']['audio'])) { - $thisfile_audio_dataformat = 'mp'.$info['mpeg']['audio']['layer']; - $thisfile_audio['sample_rate'] = $info['mpeg']['audio']['sample_rate']; - $thisfile_audio['channels'] = $info['mpeg']['audio']['channels']; - $thisfile_audio['bitrate'] = $info['mpeg']['audio']['bitrate']; - $thisfile_audio['bitrate_mode'] = strtolower($info['mpeg']['audio']['bitrate_mode']); - if (!empty($info['mpeg']['audio']['codec'])) { - $thisfile_audio['codec'] = $info['mpeg']['audio']['codec'].' '.$thisfile_audio['codec']; - } - if (!empty($thisfile_audio['streams'])) { - foreach ($thisfile_audio['streams'] as $streamnumber => $streamdata) { - if ($streamdata['dataformat'] == $thisfile_audio_dataformat) { - $thisfile_audio['streams'][$streamnumber]['sample_rate'] = $thisfile_audio['sample_rate']; - $thisfile_audio['streams'][$streamnumber]['channels'] = $thisfile_audio['channels']; - $thisfile_audio['streams'][$streamnumber]['bitrate'] = $thisfile_audio['bitrate']; - $thisfile_audio['streams'][$streamnumber]['bitrate_mode'] = $thisfile_audio['bitrate_mode']; - $thisfile_audio['streams'][$streamnumber]['codec'] = $thisfile_audio['codec']; - } - } - } - $getid3_mp3 = new getid3_mp3($this->getid3); - $thisfile_audio['encoder_options'] = $getid3_mp3->GuessEncoderOptions(); - unset($getid3_mp3); - } - - - if (!empty($thisfile_riff_raw['fmt ']['wBitsPerSample']) && ($thisfile_riff_raw['fmt ']['wBitsPerSample'] > 0)) { - switch ($thisfile_audio_dataformat) { - case 'ac3': - // ignore bits_per_sample - break; - - default: - $thisfile_audio['bits_per_sample'] = $thisfile_riff_raw['fmt ']['wBitsPerSample']; - break; - } - } - - - if (empty($thisfile_riff_raw)) { - unset($thisfile_riff['raw']); - } - if (empty($thisfile_riff_audio)) { - unset($thisfile_riff['audio']); - } - if (empty($thisfile_riff_video)) { - unset($thisfile_riff['video']); - } - - return true; - } - - public function ParseRIFF($startoffset, $maxoffset) { - $info = &$this->getid3->info; - - $RIFFchunk = false; - $FoundAllChunksWeNeed = false; - - try { - $this->fseek($startoffset); - $maxoffset = min($maxoffset, $info['avdataend']); - while ($this->ftell() < $maxoffset) { - $chunknamesize = $this->fread(8); - //$chunkname = substr($chunknamesize, 0, 4); - $chunkname = str_replace("\x00", '_', substr($chunknamesize, 0, 4)); // note: chunk names of 4 null bytes do appear to be legal (has been observed inside INFO and PRMI chunks, for example), but makes traversing array keys more difficult - $chunksize = $this->EitherEndian2Int(substr($chunknamesize, 4, 4)); - //if (strlen(trim($chunkname, "\x00")) < 4) { - if (strlen($chunkname) < 4) { - $this->error('Expecting chunk name at offset '.($this->ftell() - 8).' but found nothing. Aborting RIFF parsing.'); - break; - } - if (($chunksize == 0) && ($chunkname != 'JUNK')) { - $this->warning('Chunk ('.$chunkname.') size at offset '.($this->ftell() - 4).' is zero. Aborting RIFF parsing.'); - break; - } - if (($chunksize % 2) != 0) { - // all structures are packed on word boundaries - $chunksize++; - } - - switch ($chunkname) { - case 'LIST': - $listname = $this->fread(4); - if (preg_match('#^(movi|rec )$#i', $listname)) { - $RIFFchunk[$listname]['offset'] = $this->ftell() - 4; - $RIFFchunk[$listname]['size'] = $chunksize; - - if (!$FoundAllChunksWeNeed) { - $WhereWeWere = $this->ftell(); - $AudioChunkHeader = $this->fread(12); - $AudioChunkStreamNum = substr($AudioChunkHeader, 0, 2); - $AudioChunkStreamType = substr($AudioChunkHeader, 2, 2); - $AudioChunkSize = getid3_lib::LittleEndian2Int(substr($AudioChunkHeader, 4, 4)); - - if ($AudioChunkStreamType == 'wb') { - $FirstFourBytes = substr($AudioChunkHeader, 8, 4); - if (preg_match('/^\xFF[\xE2-\xE7\xF2-\xF7\xFA-\xFF][\x00-\xEB]/s', $FirstFourBytes)) { - // MP3 - if (getid3_mp3::MPEGaudioHeaderBytesValid($FirstFourBytes)) { - $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); - $getid3_temp->info['avdataoffset'] = $this->ftell() - 4; - $getid3_temp->info['avdataend'] = $this->ftell() + $AudioChunkSize; - $getid3_mp3 = new getid3_mp3($getid3_temp); - $getid3_mp3->getOnlyMPEGaudioInfo($getid3_temp->info['avdataoffset'], false); - if (isset($getid3_temp->info['mpeg']['audio'])) { - $info['mpeg']['audio'] = $getid3_temp->info['mpeg']['audio']; - $info['audio'] = $getid3_temp->info['audio']; - $info['audio']['dataformat'] = 'mp'.$info['mpeg']['audio']['layer']; - $info['audio']['sample_rate'] = $info['mpeg']['audio']['sample_rate']; - $info['audio']['channels'] = $info['mpeg']['audio']['channels']; - $info['audio']['bitrate'] = $info['mpeg']['audio']['bitrate']; - $info['audio']['bitrate_mode'] = strtolower($info['mpeg']['audio']['bitrate_mode']); - //$info['bitrate'] = $info['audio']['bitrate']; - } - unset($getid3_temp, $getid3_mp3); - } - - } elseif (strpos($FirstFourBytes, getid3_ac3::syncword) === 0) { - - // AC3 - $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); - $getid3_temp->info['avdataoffset'] = $this->ftell() - 4; - $getid3_temp->info['avdataend'] = $this->ftell() + $AudioChunkSize; - $getid3_ac3 = new getid3_ac3($getid3_temp); - $getid3_ac3->Analyze(); - if (empty($getid3_temp->info['error'])) { - $info['audio'] = $getid3_temp->info['audio']; - $info['ac3'] = $getid3_temp->info['ac3']; - if (!empty($getid3_temp->info['warning'])) { - foreach ($getid3_temp->info['warning'] as $key => $value) { - $info['warning'][] = $value; - } - } - } - unset($getid3_temp, $getid3_ac3); - } - } - $FoundAllChunksWeNeed = true; - $this->fseek($WhereWeWere); - } - $this->fseek($chunksize - 4, SEEK_CUR); - - } else { - - if (!isset($RIFFchunk[$listname])) { - $RIFFchunk[$listname] = array(); - } - $LISTchunkParent = $listname; - $LISTchunkMaxOffset = $this->ftell() - 4 + $chunksize; - if ($parsedChunk = $this->ParseRIFF($this->ftell(), $LISTchunkMaxOffset)) { - $RIFFchunk[$listname] = array_merge_recursive($RIFFchunk[$listname], $parsedChunk); - } - - } - break; - - default: - if (preg_match('#^[0-9]{2}(wb|pc|dc|db)$#', $chunkname)) { - $this->fseek($chunksize, SEEK_CUR); - break; - } - $thisindex = 0; - if (isset($RIFFchunk[$chunkname]) && is_array($RIFFchunk[$chunkname])) { - $thisindex = count($RIFFchunk[$chunkname]); - } - $RIFFchunk[$chunkname][$thisindex]['offset'] = $this->ftell() - 8; - $RIFFchunk[$chunkname][$thisindex]['size'] = $chunksize; - switch ($chunkname) { - case 'data': - $info['avdataoffset'] = $this->ftell(); - $info['avdataend'] = $info['avdataoffset'] + $chunksize; - - $testData = $this->fread(36); - if ($testData === '') { - break; - } - if (preg_match('/^\xFF[\xE2-\xE7\xF2-\xF7\xFA-\xFF][\x00-\xEB]/s', substr($testData, 0, 4))) { - - // Probably is MP3 data - if (getid3_mp3::MPEGaudioHeaderBytesValid(substr($testData, 0, 4))) { - $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); - $getid3_temp->info['avdataoffset'] = $info['avdataoffset']; - $getid3_temp->info['avdataend'] = $info['avdataend']; - $getid3_mp3 = new getid3_mp3($getid3_temp); - $getid3_mp3->getOnlyMPEGaudioInfo($info['avdataoffset'], false); - if (empty($getid3_temp->info['error'])) { - $info['audio'] = $getid3_temp->info['audio']; - $info['mpeg'] = $getid3_temp->info['mpeg']; - } - unset($getid3_temp, $getid3_mp3); - } - - } elseif (($isRegularAC3 = (substr($testData, 0, 2) == getid3_ac3::syncword)) || substr($testData, 8, 2) == strrev(getid3_ac3::syncword)) { - - // This is probably AC-3 data - $getid3_temp = new getID3(); - if ($isRegularAC3) { - $getid3_temp->openfile($this->getid3->filename); - $getid3_temp->info['avdataoffset'] = $info['avdataoffset']; - $getid3_temp->info['avdataend'] = $info['avdataend']; - } - $getid3_ac3 = new getid3_ac3($getid3_temp); - if ($isRegularAC3) { - $getid3_ac3->Analyze(); - } else { - // Dolby Digital WAV - // AC-3 content, but not encoded in same format as normal AC-3 file - // For one thing, byte order is swapped - $ac3_data = ''; - for ($i = 0; $i < 28; $i += 2) { - $ac3_data .= substr($testData, 8 + $i + 1, 1); - $ac3_data .= substr($testData, 8 + $i + 0, 1); - } - $getid3_ac3->AnalyzeString($ac3_data); - } - - if (empty($getid3_temp->info['error'])) { - $info['audio'] = $getid3_temp->info['audio']; - $info['ac3'] = $getid3_temp->info['ac3']; - if (!empty($getid3_temp->info['warning'])) { - foreach ($getid3_temp->info['warning'] as $newerror) { - $this->warning('getid3_ac3() says: ['.$newerror.']'); - } - } - } - unset($getid3_temp, $getid3_ac3); - - } elseif (preg_match('/^('.implode('|', array_map('preg_quote', getid3_dts::$syncwords)).')/', $testData)) { - - // This is probably DTS data - $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); - $getid3_temp->info['avdataoffset'] = $info['avdataoffset']; - $getid3_dts = new getid3_dts($getid3_temp); - $getid3_dts->Analyze(); - if (empty($getid3_temp->info['error'])) { - $info['audio'] = $getid3_temp->info['audio']; - $info['dts'] = $getid3_temp->info['dts']; - $info['playtime_seconds'] = $getid3_temp->info['playtime_seconds']; // may not match RIFF calculations since DTS-WAV often used 14/16 bit-word packing - if (!empty($getid3_temp->info['warning'])) { - foreach ($getid3_temp->info['warning'] as $newerror) { - $this->warning('getid3_dts() says: ['.$newerror.']'); - } - } - } - - unset($getid3_temp, $getid3_dts); - - } elseif (substr($testData, 0, 4) == 'wvpk') { - - // This is WavPack data - $info['wavpack']['offset'] = $info['avdataoffset']; - $info['wavpack']['size'] = getid3_lib::LittleEndian2Int(substr($testData, 4, 4)); - $this->parseWavPackHeader(substr($testData, 8, 28)); - - } else { - // This is some other kind of data (quite possibly just PCM) - // do nothing special, just skip it - } - $nextoffset = $info['avdataend']; - $this->fseek($nextoffset); - break; - - case 'iXML': - case 'bext': - case 'cart': - case 'fmt ': - case 'strh': - case 'strf': - case 'indx': - case 'MEXT': - case 'DISP': - // always read data in - case 'JUNK': - // should be: never read data in - // but some programs write their version strings in a JUNK chunk (e.g. VirtualDub, AVIdemux, etc) - if ($chunksize < 1048576) { - if ($chunksize > 0) { - $RIFFchunk[$chunkname][$thisindex]['data'] = $this->fread($chunksize); - if ($chunkname == 'JUNK') { - if (preg_match('#^([\\x20-\\x7F]+)#', $RIFFchunk[$chunkname][$thisindex]['data'], $matches)) { - // only keep text characters [chr(32)-chr(127)] - $info['riff']['comments']['junk'][] = trim($matches[1]); - } - // but if nothing there, ignore - // remove the key in either case - unset($RIFFchunk[$chunkname][$thisindex]['data']); - } - } - } else { - $this->warning('Chunk "'.$chunkname.'" at offset '.$this->ftell().' is unexpectedly larger than 1MB (claims to be '.number_format($chunksize).' bytes), skipping data'); - $this->fseek($chunksize, SEEK_CUR); - } - break; - - //case 'IDVX': - // $info['divxtag']['comments'] = self::ParseDIVXTAG($this->fread($chunksize)); - // break; - - default: - if (!empty($LISTchunkParent) && (($RIFFchunk[$chunkname][$thisindex]['offset'] + $RIFFchunk[$chunkname][$thisindex]['size']) <= $LISTchunkMaxOffset)) { - $RIFFchunk[$LISTchunkParent][$chunkname][$thisindex]['offset'] = $RIFFchunk[$chunkname][$thisindex]['offset']; - $RIFFchunk[$LISTchunkParent][$chunkname][$thisindex]['size'] = $RIFFchunk[$chunkname][$thisindex]['size']; - unset($RIFFchunk[$chunkname][$thisindex]['offset']); - unset($RIFFchunk[$chunkname][$thisindex]['size']); - if (isset($RIFFchunk[$chunkname][$thisindex]) && empty($RIFFchunk[$chunkname][$thisindex])) { - unset($RIFFchunk[$chunkname][$thisindex]); - } - if (isset($RIFFchunk[$chunkname]) && empty($RIFFchunk[$chunkname])) { - unset($RIFFchunk[$chunkname]); - } - $RIFFchunk[$LISTchunkParent][$chunkname][$thisindex]['data'] = $this->fread($chunksize); - } elseif ($chunksize < 2048) { - // only read data in if smaller than 2kB - $RIFFchunk[$chunkname][$thisindex]['data'] = $this->fread($chunksize); - } else { - $this->fseek($chunksize, SEEK_CUR); - } - break; - } - break; - } - } - - } catch (getid3_exception $e) { - if ($e->getCode() == 10) { - $this->warning('RIFF parser: '.$e->getMessage()); - } else { - throw $e; - } - } - - return $RIFFchunk; - } - - public function ParseRIFFdata(&$RIFFdata) { - $info = &$this->getid3->info; - if ($RIFFdata) { - $tempfile = tempnam(GETID3_TEMP_DIR, 'getID3'); - $fp_temp = fopen($tempfile, 'wb'); - $RIFFdataLength = strlen($RIFFdata); - $NewLengthString = getid3_lib::LittleEndian2String($RIFFdataLength, 4); - for ($i = 0; $i < 4; $i++) { - $RIFFdata[($i + 4)] = $NewLengthString[$i]; - } - fwrite($fp_temp, $RIFFdata); - fclose($fp_temp); - - $getid3_temp = new getID3(); - $getid3_temp->openfile($tempfile); - $getid3_temp->info['filesize'] = $RIFFdataLength; - $getid3_temp->info['filenamepath'] = $info['filenamepath']; - $getid3_temp->info['tags'] = $info['tags']; - $getid3_temp->info['warning'] = $info['warning']; - $getid3_temp->info['error'] = $info['error']; - $getid3_temp->info['comments'] = $info['comments']; - $getid3_temp->info['audio'] = (isset($info['audio']) ? $info['audio'] : array()); - $getid3_temp->info['video'] = (isset($info['video']) ? $info['video'] : array()); - $getid3_riff = new getid3_riff($getid3_temp); - $getid3_riff->Analyze(); - - $info['riff'] = $getid3_temp->info['riff']; - $info['warning'] = $getid3_temp->info['warning']; - $info['error'] = $getid3_temp->info['error']; - $info['tags'] = $getid3_temp->info['tags']; - $info['comments'] = $getid3_temp->info['comments']; - unset($getid3_riff, $getid3_temp); - unlink($tempfile); - } - return false; - } - - public static function parseComments(&$RIFFinfoArray, &$CommentsTargetArray) { - $RIFFinfoKeyLookup = array( - 'IARL'=>'archivallocation', - 'IART'=>'artist', - 'ICDS'=>'costumedesigner', - 'ICMS'=>'commissionedby', - 'ICMT'=>'comment', - 'ICNT'=>'country', - 'ICOP'=>'copyright', - 'ICRD'=>'creationdate', - 'IDIM'=>'dimensions', - 'IDIT'=>'digitizationdate', - 'IDPI'=>'resolution', - 'IDST'=>'distributor', - 'IEDT'=>'editor', - 'IENG'=>'engineers', - 'IFRM'=>'accountofparts', - 'IGNR'=>'genre', - 'IKEY'=>'keywords', - 'ILGT'=>'lightness', - 'ILNG'=>'language', - 'IMED'=>'orignalmedium', - 'IMUS'=>'composer', - 'INAM'=>'title', - 'IPDS'=>'productiondesigner', - 'IPLT'=>'palette', - 'IPRD'=>'product', - 'IPRO'=>'producer', - 'IPRT'=>'part', - 'IRTD'=>'rating', - 'ISBJ'=>'subject', - 'ISFT'=>'software', - 'ISGN'=>'secondarygenre', - 'ISHP'=>'sharpness', - 'ISRC'=>'sourcesupplier', - 'ISRF'=>'digitizationsource', - 'ISTD'=>'productionstudio', - 'ISTR'=>'starring', - 'ITCH'=>'encoded_by', - 'IWEB'=>'url', - 'IWRI'=>'writer', - '____'=>'comment', - ); - foreach ($RIFFinfoKeyLookup as $key => $value) { - if (isset($RIFFinfoArray[$key])) { - foreach ($RIFFinfoArray[$key] as $commentid => $commentdata) { - if (trim($commentdata['data']) != '') { - if (isset($CommentsTargetArray[$value])) { - $CommentsTargetArray[$value][] = trim($commentdata['data']); - } else { - $CommentsTargetArray[$value] = array(trim($commentdata['data'])); - } - } - } - } - } - return true; - } - - public static function parseWAVEFORMATex($WaveFormatExData) { - // shortcut - $WaveFormatEx['raw'] = array(); - $WaveFormatEx_raw = &$WaveFormatEx['raw']; - - $WaveFormatEx_raw['wFormatTag'] = substr($WaveFormatExData, 0, 2); - $WaveFormatEx_raw['nChannels'] = substr($WaveFormatExData, 2, 2); - $WaveFormatEx_raw['nSamplesPerSec'] = substr($WaveFormatExData, 4, 4); - $WaveFormatEx_raw['nAvgBytesPerSec'] = substr($WaveFormatExData, 8, 4); - $WaveFormatEx_raw['nBlockAlign'] = substr($WaveFormatExData, 12, 2); - $WaveFormatEx_raw['wBitsPerSample'] = substr($WaveFormatExData, 14, 2); - if (strlen($WaveFormatExData) > 16) { - $WaveFormatEx_raw['cbSize'] = substr($WaveFormatExData, 16, 2); - } - $WaveFormatEx_raw = array_map('getid3_lib::LittleEndian2Int', $WaveFormatEx_raw); - - $WaveFormatEx['codec'] = self::wFormatTagLookup($WaveFormatEx_raw['wFormatTag']); - $WaveFormatEx['channels'] = $WaveFormatEx_raw['nChannels']; - $WaveFormatEx['sample_rate'] = $WaveFormatEx_raw['nSamplesPerSec']; - $WaveFormatEx['bitrate'] = $WaveFormatEx_raw['nAvgBytesPerSec'] * 8; - $WaveFormatEx['bits_per_sample'] = $WaveFormatEx_raw['wBitsPerSample']; - - return $WaveFormatEx; - } - - public function parseWavPackHeader($WavPackChunkData) { - // typedef struct { - // char ckID [4]; - // long ckSize; - // short version; - // short bits; // added for version 2.00 - // short flags, shift; // added for version 3.00 - // long total_samples, crc, crc2; - // char extension [4], extra_bc, extras [3]; - // } WavpackHeader; - - // shortcut - $info = &$this->getid3->info; - $info['wavpack'] = array(); - $thisfile_wavpack = &$info['wavpack']; - - $thisfile_wavpack['version'] = getid3_lib::LittleEndian2Int(substr($WavPackChunkData, 0, 2)); - if ($thisfile_wavpack['version'] >= 2) { - $thisfile_wavpack['bits'] = getid3_lib::LittleEndian2Int(substr($WavPackChunkData, 2, 2)); - } - if ($thisfile_wavpack['version'] >= 3) { - $thisfile_wavpack['flags_raw'] = getid3_lib::LittleEndian2Int(substr($WavPackChunkData, 4, 2)); - $thisfile_wavpack['shift'] = getid3_lib::LittleEndian2Int(substr($WavPackChunkData, 6, 2)); - $thisfile_wavpack['total_samples'] = getid3_lib::LittleEndian2Int(substr($WavPackChunkData, 8, 4)); - $thisfile_wavpack['crc1'] = getid3_lib::LittleEndian2Int(substr($WavPackChunkData, 12, 4)); - $thisfile_wavpack['crc2'] = getid3_lib::LittleEndian2Int(substr($WavPackChunkData, 16, 4)); - $thisfile_wavpack['extension'] = substr($WavPackChunkData, 20, 4); - $thisfile_wavpack['extra_bc'] = getid3_lib::LittleEndian2Int(substr($WavPackChunkData, 24, 1)); - for ($i = 0; $i <= 2; $i++) { - $thisfile_wavpack['extras'][] = getid3_lib::LittleEndian2Int(substr($WavPackChunkData, 25 + $i, 1)); - } - - // shortcut - $thisfile_wavpack['flags'] = array(); - $thisfile_wavpack_flags = &$thisfile_wavpack['flags']; - - $thisfile_wavpack_flags['mono'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x000001); - $thisfile_wavpack_flags['fast_mode'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x000002); - $thisfile_wavpack_flags['raw_mode'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x000004); - $thisfile_wavpack_flags['calc_noise'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x000008); - $thisfile_wavpack_flags['high_quality'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x000010); - $thisfile_wavpack_flags['3_byte_samples'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x000020); - $thisfile_wavpack_flags['over_20_bits'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x000040); - $thisfile_wavpack_flags['use_wvc'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x000080); - $thisfile_wavpack_flags['noiseshaping'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x000100); - $thisfile_wavpack_flags['very_fast_mode'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x000200); - $thisfile_wavpack_flags['new_high_quality'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x000400); - $thisfile_wavpack_flags['cancel_extreme'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x000800); - $thisfile_wavpack_flags['cross_decorrelation'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x001000); - $thisfile_wavpack_flags['new_decorrelation'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x002000); - $thisfile_wavpack_flags['joint_stereo'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x004000); - $thisfile_wavpack_flags['extra_decorrelation'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x008000); - $thisfile_wavpack_flags['override_noiseshape'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x010000); - $thisfile_wavpack_flags['override_jointstereo'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x020000); - $thisfile_wavpack_flags['copy_source_filetime'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x040000); - $thisfile_wavpack_flags['create_exe'] = (bool) ($thisfile_wavpack['flags_raw'] & 0x080000); - } - - return true; - } - - public static function ParseBITMAPINFOHEADER($BITMAPINFOHEADER, $littleEndian=true) { - - $parsed['biSize'] = substr($BITMAPINFOHEADER, 0, 4); // number of bytes required by the BITMAPINFOHEADER structure - $parsed['biWidth'] = substr($BITMAPINFOHEADER, 4, 4); // width of the bitmap in pixels - $parsed['biHeight'] = substr($BITMAPINFOHEADER, 8, 4); // height of the bitmap in pixels. If biHeight is positive, the bitmap is a 'bottom-up' DIB and its origin is the lower left corner. If biHeight is negative, the bitmap is a 'top-down' DIB and its origin is the upper left corner - $parsed['biPlanes'] = substr($BITMAPINFOHEADER, 12, 2); // number of color planes on the target device. In most cases this value must be set to 1 - $parsed['biBitCount'] = substr($BITMAPINFOHEADER, 14, 2); // Specifies the number of bits per pixels - $parsed['biSizeImage'] = substr($BITMAPINFOHEADER, 20, 4); // size of the bitmap data section of the image (the actual pixel data, excluding BITMAPINFOHEADER and RGBQUAD structures) - $parsed['biXPelsPerMeter'] = substr($BITMAPINFOHEADER, 24, 4); // horizontal resolution, in pixels per metre, of the target device - $parsed['biYPelsPerMeter'] = substr($BITMAPINFOHEADER, 28, 4); // vertical resolution, in pixels per metre, of the target device - $parsed['biClrUsed'] = substr($BITMAPINFOHEADER, 32, 4); // actual number of color indices in the color table used by the bitmap. If this value is zero, the bitmap uses the maximum number of colors corresponding to the value of the biBitCount member for the compression mode specified by biCompression - $parsed['biClrImportant'] = substr($BITMAPINFOHEADER, 36, 4); // number of color indices that are considered important for displaying the bitmap. If this value is zero, all colors are important - $parsed = array_map('getid3_lib::'.($littleEndian ? 'Little' : 'Big').'Endian2Int', $parsed); - - $parsed['fourcc'] = substr($BITMAPINFOHEADER, 16, 4); // compression identifier - - return $parsed; - } - - public static function ParseDIVXTAG($DIVXTAG, $raw=false) { - // structure from "IDivX" source, Form1.frm, by "Greg Frazier of Daemonic Software Group", email: gfrazier@icestorm.net, web: http://dsg.cjb.net/ - // source available at http://files.divx-digest.com/download/c663efe7ef8ad2e90bf4af4d3ea6188a/on0SWN2r/edit/IDivX.zip - // 'Byte Layout: '1111111111111111 - // '32 for Movie - 1 '1111111111111111 - // '28 for Author - 6 '6666666666666666 - // '4 for year - 2 '6666666666662222 - // '3 for genre - 3 '7777777777777777 - // '48 for Comments - 7 '7777777777777777 - // '1 for Rating - 4 '7777777777777777 - // '5 for Future Additions - 0 '333400000DIVXTAG - // '128 bytes total - - static $DIVXTAGgenre = array( - 0 => 'Action', - 1 => 'Action/Adventure', - 2 => 'Adventure', - 3 => 'Adult', - 4 => 'Anime', - 5 => 'Cartoon', - 6 => 'Claymation', - 7 => 'Comedy', - 8 => 'Commercial', - 9 => 'Documentary', - 10 => 'Drama', - 11 => 'Home Video', - 12 => 'Horror', - 13 => 'Infomercial', - 14 => 'Interactive', - 15 => 'Mystery', - 16 => 'Music Video', - 17 => 'Other', - 18 => 'Religion', - 19 => 'Sci Fi', - 20 => 'Thriller', - 21 => 'Western', - ), - $DIVXTAGrating = array( - 0 => 'Unrated', - 1 => 'G', - 2 => 'PG', - 3 => 'PG-13', - 4 => 'R', - 5 => 'NC-17', - ); - - $parsed['title'] = trim(substr($DIVXTAG, 0, 32)); - $parsed['artist'] = trim(substr($DIVXTAG, 32, 28)); - $parsed['year'] = intval(trim(substr($DIVXTAG, 60, 4))); - $parsed['comment'] = trim(substr($DIVXTAG, 64, 48)); - $parsed['genre_id'] = intval(trim(substr($DIVXTAG, 112, 3))); - $parsed['rating_id'] = ord(substr($DIVXTAG, 115, 1)); - //$parsed['padding'] = substr($DIVXTAG, 116, 5); // 5-byte null - //$parsed['magic'] = substr($DIVXTAG, 121, 7); // "DIVXTAG" - - $parsed['genre'] = (isset($DIVXTAGgenre[$parsed['genre_id']]) ? $DIVXTAGgenre[$parsed['genre_id']] : $parsed['genre_id']); - $parsed['rating'] = (isset($DIVXTAGrating[$parsed['rating_id']]) ? $DIVXTAGrating[$parsed['rating_id']] : $parsed['rating_id']); - - if (!$raw) { - unset($parsed['genre_id'], $parsed['rating_id']); - foreach ($parsed as $key => $value) { - if (!$value === '') { - unset($parsed['key']); - } - } - } - - foreach ($parsed as $tag => $value) { - $parsed[$tag] = array($value); - } - - return $parsed; - } - - public static function waveSNDMtagLookup($tagshortname) { - $begin = __LINE__; - - /** This is not a comment! - - ©kwd keywords - ©BPM bpm - ©trt tracktitle - ©des description - ©gen category - ©fin featuredinstrument - ©LID longid - ©bex bwdescription - ©pub publisher - ©cdt cdtitle - ©alb library - ©com composer - - */ - - return getid3_lib::EmbeddedLookup($tagshortname, $begin, __LINE__, __FILE__, 'riff-sndm'); - } - - public static function wFormatTagLookup($wFormatTag) { - - $begin = __LINE__; - - /** This is not a comment! - - 0x0000 Microsoft Unknown Wave Format - 0x0001 Pulse Code Modulation (PCM) - 0x0002 Microsoft ADPCM - 0x0003 IEEE Float - 0x0004 Compaq Computer VSELP - 0x0005 IBM CVSD - 0x0006 Microsoft A-Law - 0x0007 Microsoft mu-Law - 0x0008 Microsoft DTS - 0x0010 OKI ADPCM - 0x0011 Intel DVI/IMA ADPCM - 0x0012 Videologic MediaSpace ADPCM - 0x0013 Sierra Semiconductor ADPCM - 0x0014 Antex Electronics G.723 ADPCM - 0x0015 DSP Solutions DigiSTD - 0x0016 DSP Solutions DigiFIX - 0x0017 Dialogic OKI ADPCM - 0x0018 MediaVision ADPCM - 0x0019 Hewlett-Packard CU - 0x0020 Yamaha ADPCM - 0x0021 Speech Compression Sonarc - 0x0022 DSP Group TrueSpeech - 0x0023 Echo Speech EchoSC1 - 0x0024 Audiofile AF36 - 0x0025 Audio Processing Technology APTX - 0x0026 AudioFile AF10 - 0x0027 Prosody 1612 - 0x0028 LRC - 0x0030 Dolby AC2 - 0x0031 Microsoft GSM 6.10 - 0x0032 MSNAudio - 0x0033 Antex Electronics ADPCME - 0x0034 Control Resources VQLPC - 0x0035 DSP Solutions DigiREAL - 0x0036 DSP Solutions DigiADPCM - 0x0037 Control Resources CR10 - 0x0038 Natural MicroSystems VBXADPCM - 0x0039 Crystal Semiconductor IMA ADPCM - 0x003A EchoSC3 - 0x003B Rockwell ADPCM - 0x003C Rockwell Digit LK - 0x003D Xebec - 0x0040 Antex Electronics G.721 ADPCM - 0x0041 G.728 CELP - 0x0042 MSG723 - 0x0050 MPEG Layer-2 or Layer-1 - 0x0052 RT24 - 0x0053 PAC - 0x0055 MPEG Layer-3 - 0x0059 Lucent G.723 - 0x0060 Cirrus - 0x0061 ESPCM - 0x0062 Voxware - 0x0063 Canopus Atrac - 0x0064 G.726 ADPCM - 0x0065 G.722 ADPCM - 0x0066 DSAT - 0x0067 DSAT Display - 0x0069 Voxware Byte Aligned - 0x0070 Voxware AC8 - 0x0071 Voxware AC10 - 0x0072 Voxware AC16 - 0x0073 Voxware AC20 - 0x0074 Voxware MetaVoice - 0x0075 Voxware MetaSound - 0x0076 Voxware RT29HW - 0x0077 Voxware VR12 - 0x0078 Voxware VR18 - 0x0079 Voxware TQ40 - 0x0080 Softsound - 0x0081 Voxware TQ60 - 0x0082 MSRT24 - 0x0083 G.729A - 0x0084 MVI MV12 - 0x0085 DF G.726 - 0x0086 DF GSM610 - 0x0088 ISIAudio - 0x0089 Onlive - 0x0091 SBC24 - 0x0092 Dolby AC3 SPDIF - 0x0093 MediaSonic G.723 - 0x0094 Aculab PLC Prosody 8kbps - 0x0097 ZyXEL ADPCM - 0x0098 Philips LPCBB - 0x0099 Packed - 0x00FF AAC - 0x0100 Rhetorex ADPCM - 0x0101 IBM mu-law - 0x0102 IBM A-law - 0x0103 IBM AVC Adaptive Differential Pulse Code Modulation (ADPCM) - 0x0111 Vivo G.723 - 0x0112 Vivo Siren - 0x0123 Digital G.723 - 0x0125 Sanyo LD ADPCM - 0x0130 Sipro Lab Telecom ACELP NET - 0x0131 Sipro Lab Telecom ACELP 4800 - 0x0132 Sipro Lab Telecom ACELP 8V3 - 0x0133 Sipro Lab Telecom G.729 - 0x0134 Sipro Lab Telecom G.729A - 0x0135 Sipro Lab Telecom Kelvin - 0x0140 Windows Media Video V8 - 0x0150 Qualcomm PureVoice - 0x0151 Qualcomm HalfRate - 0x0155 Ring Zero Systems TUB GSM - 0x0160 Microsoft Audio 1 - 0x0161 Windows Media Audio V7 / V8 / V9 - 0x0162 Windows Media Audio Professional V9 - 0x0163 Windows Media Audio Lossless V9 - 0x0200 Creative Labs ADPCM - 0x0202 Creative Labs Fastspeech8 - 0x0203 Creative Labs Fastspeech10 - 0x0210 UHER Informatic GmbH ADPCM - 0x0220 Quarterdeck - 0x0230 I-link Worldwide VC - 0x0240 Aureal RAW Sport - 0x0250 Interactive Products HSX - 0x0251 Interactive Products RPELP - 0x0260 Consistent Software CS2 - 0x0270 Sony SCX - 0x0300 Fujitsu FM Towns Snd - 0x0400 BTV Digital - 0x0401 Intel Music Coder - 0x0450 QDesign Music - 0x0680 VME VMPCM - 0x0681 AT&T Labs TPC - 0x08AE ClearJump LiteWave - 0x1000 Olivetti GSM - 0x1001 Olivetti ADPCM - 0x1002 Olivetti CELP - 0x1003 Olivetti SBC - 0x1004 Olivetti OPR - 0x1100 Lernout & Hauspie Codec (0x1100) - 0x1101 Lernout & Hauspie CELP Codec (0x1101) - 0x1102 Lernout & Hauspie SBC Codec (0x1102) - 0x1103 Lernout & Hauspie SBC Codec (0x1103) - 0x1104 Lernout & Hauspie SBC Codec (0x1104) - 0x1400 Norris - 0x1401 AT&T ISIAudio - 0x1500 Soundspace Music Compression - 0x181C VoxWare RT24 Speech - 0x1FC4 NCT Soft ALF2CD (www.nctsoft.com) - 0x2000 Dolby AC3 - 0x2001 Dolby DTS - 0x2002 WAVE_FORMAT_14_4 - 0x2003 WAVE_FORMAT_28_8 - 0x2004 WAVE_FORMAT_COOK - 0x2005 WAVE_FORMAT_DNET - 0x674F Ogg Vorbis 1 - 0x6750 Ogg Vorbis 2 - 0x6751 Ogg Vorbis 3 - 0x676F Ogg Vorbis 1+ - 0x6770 Ogg Vorbis 2+ - 0x6771 Ogg Vorbis 3+ - 0x7A21 GSM-AMR (CBR, no SID) - 0x7A22 GSM-AMR (VBR, including SID) - 0xFFFE WAVE_FORMAT_EXTENSIBLE - 0xFFFF WAVE_FORMAT_DEVELOPMENT - - */ - - return getid3_lib::EmbeddedLookup('0x'.str_pad(strtoupper(dechex($wFormatTag)), 4, '0', STR_PAD_LEFT), $begin, __LINE__, __FILE__, 'riff-wFormatTag'); - } - - public static function fourccLookup($fourcc) { - - $begin = __LINE__; - - /** This is not a comment! - - swot http://developer.apple.com/qa/snd/snd07.html - ____ No Codec (____) - _BIT BI_BITFIELDS (Raw RGB) - _JPG JPEG compressed - _PNG PNG compressed W3C/ISO/IEC (RFC-2083) - _RAW Full Frames (Uncompressed) - _RGB Raw RGB Bitmap - _RL4 RLE 4bpp RGB - _RL8 RLE 8bpp RGB - 3IV1 3ivx MPEG-4 v1 - 3IV2 3ivx MPEG-4 v2 - 3IVX 3ivx MPEG-4 - AASC Autodesk Animator - ABYR Kensington ?ABYR? - AEMI Array Microsystems VideoONE MPEG1-I Capture - AFLC Autodesk Animator FLC - AFLI Autodesk Animator FLI - AMPG Array Microsystems VideoONE MPEG - ANIM Intel RDX (ANIM) - AP41 AngelPotion Definitive - ASV1 Asus Video v1 - ASV2 Asus Video v2 - ASVX Asus Video 2.0 (audio) - AUR2 AuraVision Aura 2 Codec - YUV 4:2:2 - AURA AuraVision Aura 1 Codec - YUV 4:1:1 - AVDJ Independent JPEG Group\'s codec (AVDJ) - AVRN Independent JPEG Group\'s codec (AVRN) - AYUV 4:4:4 YUV (AYUV) - AZPR Quicktime Apple Video (AZPR) - BGR Raw RGB32 - BLZ0 Blizzard DivX MPEG-4 - BTVC Conexant Composite Video - BINK RAD Game Tools Bink Video - BT20 Conexant Prosumer Video - BTCV Conexant Composite Video Codec - BW10 Data Translation Broadway MPEG Capture - CC12 Intel YUV12 - CDVC Canopus DV - CFCC Digital Processing Systems DPS Perception - CGDI Microsoft Office 97 Camcorder Video - CHAM Winnov Caviara Champagne - CJPG Creative WebCam JPEG - CLJR Cirrus Logic YUV 4:1:1 - CMYK Common Data Format in Printing (Colorgraph) - CPLA Weitek 4:2:0 YUV Planar - CRAM Microsoft Video 1 (CRAM) - cvid Radius Cinepak - CVID Radius Cinepak - CWLT Microsoft Color WLT DIB - CYUV Creative Labs YUV - CYUY ATI YUV - D261 H.261 - D263 H.263 - DIB Device Independent Bitmap - DIV1 FFmpeg OpenDivX - DIV2 Microsoft MPEG-4 v1/v2 - DIV3 DivX ;-) MPEG-4 v3.x Low-Motion - DIV4 DivX ;-) MPEG-4 v3.x Fast-Motion - DIV5 DivX MPEG-4 v5.x - DIV6 DivX ;-) (MS MPEG-4 v3.x) - DIVX DivX MPEG-4 v4 (OpenDivX / Project Mayo) - divx DivX MPEG-4 - DMB1 Matrox Rainbow Runner hardware MJPEG - DMB2 Paradigm MJPEG - DSVD ?DSVD? - DUCK Duck TrueMotion 1.0 - DPS0 DPS/Leitch Reality Motion JPEG - DPSC DPS/Leitch PAR Motion JPEG - DV25 Matrox DVCPRO codec - DV50 Matrox DVCPRO50 codec - DVC IEC 61834 and SMPTE 314M (DVC/DV Video) - DVCP IEC 61834 and SMPTE 314M (DVC/DV Video) - DVHD IEC Standard DV 1125 lines @ 30fps / 1250 lines @ 25fps - DVMA Darim Vision DVMPEG (dummy for MPEG compressor) (www.darvision.com) - DVSL IEC Standard DV compressed in SD (SDL) - DVAN ?DVAN? - DVE2 InSoft DVE-2 Videoconferencing - dvsd IEC 61834 and SMPTE 314M DVC/DV Video - DVSD IEC 61834 and SMPTE 314M DVC/DV Video - DVX1 Lucent DVX1000SP Video Decoder - DVX2 Lucent DVX2000S Video Decoder - DVX3 Lucent DVX3000S Video Decoder - DX50 DivX v5 - DXT1 Microsoft DirectX Compressed Texture (DXT1) - DXT2 Microsoft DirectX Compressed Texture (DXT2) - DXT3 Microsoft DirectX Compressed Texture (DXT3) - DXT4 Microsoft DirectX Compressed Texture (DXT4) - DXT5 Microsoft DirectX Compressed Texture (DXT5) - DXTC Microsoft DirectX Compressed Texture (DXTC) - DXTn Microsoft DirectX Compressed Texture (DXTn) - EM2V Etymonix MPEG-2 I-frame (www.etymonix.com) - EKQ0 Elsa ?EKQ0? - ELK0 Elsa ?ELK0? - ESCP Eidos Escape - ETV1 eTreppid Video ETV1 - ETV2 eTreppid Video ETV2 - ETVC eTreppid Video ETVC - FLIC Autodesk FLI/FLC Animation - FLV1 Sorenson Spark - FLV4 On2 TrueMotion VP6 - FRWT Darim Vision Forward Motion JPEG (www.darvision.com) - FRWU Darim Vision Forward Uncompressed (www.darvision.com) - FLJP D-Vision Field Encoded Motion JPEG - FPS1 FRAPS v1 - FRWA SoftLab-Nsk Forward Motion JPEG w/ alpha channel - FRWD SoftLab-Nsk Forward Motion JPEG - FVF1 Iterated Systems Fractal Video Frame - GLZW Motion LZW (gabest@freemail.hu) - GPEG Motion JPEG (gabest@freemail.hu) - GWLT Microsoft Greyscale WLT DIB - H260 Intel ITU H.260 Videoconferencing - H261 Intel ITU H.261 Videoconferencing - H262 Intel ITU H.262 Videoconferencing - H263 Intel ITU H.263 Videoconferencing - H264 Intel ITU H.264 Videoconferencing - H265 Intel ITU H.265 Videoconferencing - H266 Intel ITU H.266 Videoconferencing - H267 Intel ITU H.267 Videoconferencing - H268 Intel ITU H.268 Videoconferencing - H269 Intel ITU H.269 Videoconferencing - HFYU Huffman Lossless Codec - HMCR Rendition Motion Compensation Format (HMCR) - HMRR Rendition Motion Compensation Format (HMRR) - I263 FFmpeg I263 decoder - IF09 Indeo YVU9 ("YVU9 with additional delta-frame info after the U plane") - IUYV Interlaced version of UYVY (www.leadtools.com) - IY41 Interlaced version of Y41P (www.leadtools.com) - IYU1 12 bit format used in mode 2 of the IEEE 1394 Digital Camera 1.04 spec IEEE standard - IYU2 24 bit format used in mode 2 of the IEEE 1394 Digital Camera 1.04 spec IEEE standard - IYUV Planar YUV format (8-bpp Y plane, followed by 8-bpp 2×2 U and V planes) - i263 Intel ITU H.263 Videoconferencing (i263) - I420 Intel Indeo 4 - IAN Intel Indeo 4 (RDX) - ICLB InSoft CellB Videoconferencing - IGOR Power DVD - IJPG Intergraph JPEG - ILVC Intel Layered Video - ILVR ITU-T H.263+ - IPDV I-O Data Device Giga AVI DV Codec - IR21 Intel Indeo 2.1 - IRAW Intel YUV Uncompressed - IV30 Intel Indeo 3.0 - IV31 Intel Indeo 3.1 - IV32 Ligos Indeo 3.2 - IV33 Ligos Indeo 3.3 - IV34 Ligos Indeo 3.4 - IV35 Ligos Indeo 3.5 - IV36 Ligos Indeo 3.6 - IV37 Ligos Indeo 3.7 - IV38 Ligos Indeo 3.8 - IV39 Ligos Indeo 3.9 - IV40 Ligos Indeo Interactive 4.0 - IV41 Ligos Indeo Interactive 4.1 - IV42 Ligos Indeo Interactive 4.2 - IV43 Ligos Indeo Interactive 4.3 - IV44 Ligos Indeo Interactive 4.4 - IV45 Ligos Indeo Interactive 4.5 - IV46 Ligos Indeo Interactive 4.6 - IV47 Ligos Indeo Interactive 4.7 - IV48 Ligos Indeo Interactive 4.8 - IV49 Ligos Indeo Interactive 4.9 - IV50 Ligos Indeo Interactive 5.0 - JBYR Kensington ?JBYR? - JPEG Still Image JPEG DIB - JPGL Pegasus Lossless Motion JPEG - KMVC Team17 Software Karl Morton\'s Video Codec - LSVM Vianet Lighting Strike Vmail (Streaming) (www.vianet.com) - LEAD LEAD Video Codec - Ljpg LEAD MJPEG Codec - MDVD Alex MicroDVD Video (hacked MS MPEG-4) (www.tiasoft.de) - MJPA Morgan Motion JPEG (MJPA) (www.morgan-multimedia.com) - MJPB Morgan Motion JPEG (MJPB) (www.morgan-multimedia.com) - MMES Matrox MPEG-2 I-frame - MP2v Microsoft S-Mpeg 4 version 1 (MP2v) - MP42 Microsoft S-Mpeg 4 version 2 (MP42) - MP43 Microsoft S-Mpeg 4 version 3 (MP43) - MP4S Microsoft S-Mpeg 4 version 3 (MP4S) - MP4V FFmpeg MPEG-4 - MPG1 FFmpeg MPEG 1/2 - MPG2 FFmpeg MPEG 1/2 - MPG3 FFmpeg DivX ;-) (MS MPEG-4 v3) - MPG4 Microsoft MPEG-4 - MPGI Sigma Designs MPEG - MPNG PNG images decoder - MSS1 Microsoft Windows Screen Video - MSZH LCL (Lossless Codec Library) (www.geocities.co.jp/Playtown-Denei/2837/LRC.htm) - M261 Microsoft H.261 - M263 Microsoft H.263 - M4S2 Microsoft Fully Compliant MPEG-4 v2 simple profile (M4S2) - m4s2 Microsoft Fully Compliant MPEG-4 v2 simple profile (m4s2) - MC12 ATI Motion Compensation Format (MC12) - MCAM ATI Motion Compensation Format (MCAM) - MJ2C Morgan Multimedia Motion JPEG2000 - mJPG IBM Motion JPEG w/ Huffman Tables - MJPG Microsoft Motion JPEG DIB - MP42 Microsoft MPEG-4 (low-motion) - MP43 Microsoft MPEG-4 (fast-motion) - MP4S Microsoft MPEG-4 (MP4S) - mp4s Microsoft MPEG-4 (mp4s) - MPEG Chromatic Research MPEG-1 Video I-Frame - MPG4 Microsoft MPEG-4 Video High Speed Compressor - MPGI Sigma Designs MPEG - MRCA FAST Multimedia Martin Regen Codec - MRLE Microsoft Run Length Encoding - MSVC Microsoft Video 1 - MTX1 Matrox ?MTX1? - MTX2 Matrox ?MTX2? - MTX3 Matrox ?MTX3? - MTX4 Matrox ?MTX4? - MTX5 Matrox ?MTX5? - MTX6 Matrox ?MTX6? - MTX7 Matrox ?MTX7? - MTX8 Matrox ?MTX8? - MTX9 Matrox ?MTX9? - MV12 Motion Pixels Codec (old) - MWV1 Aware Motion Wavelets - nAVI SMR Codec (hack of Microsoft MPEG-4) (IRC #shadowrealm) - NT00 NewTek LightWave HDTV YUV w/ Alpha (www.newtek.com) - NUV1 NuppelVideo - NTN1 Nogatech Video Compression 1 - NVS0 nVidia GeForce Texture (NVS0) - NVS1 nVidia GeForce Texture (NVS1) - NVS2 nVidia GeForce Texture (NVS2) - NVS3 nVidia GeForce Texture (NVS3) - NVS4 nVidia GeForce Texture (NVS4) - NVS5 nVidia GeForce Texture (NVS5) - NVT0 nVidia GeForce Texture (NVT0) - NVT1 nVidia GeForce Texture (NVT1) - NVT2 nVidia GeForce Texture (NVT2) - NVT3 nVidia GeForce Texture (NVT3) - NVT4 nVidia GeForce Texture (NVT4) - NVT5 nVidia GeForce Texture (NVT5) - PIXL MiroXL, Pinnacle PCTV - PDVC I-O Data Device Digital Video Capture DV codec - PGVV Radius Video Vision - PHMO IBM Photomotion - PIM1 MPEG Realtime (Pinnacle Cards) - PIM2 Pegasus Imaging ?PIM2? - PIMJ Pegasus Imaging Lossless JPEG - PVEZ Horizons Technology PowerEZ - PVMM PacketVideo Corporation MPEG-4 - PVW2 Pegasus Imaging Wavelet Compression - Q1.0 Q-Team\'s QPEG 1.0 (www.q-team.de) - Q1.1 Q-Team\'s QPEG 1.1 (www.q-team.de) - QPEG Q-Team QPEG 1.0 - qpeq Q-Team QPEG 1.1 - RGB Raw BGR32 - RGBA Raw RGB w/ Alpha - RMP4 REALmagic MPEG-4 (unauthorized XVID copy) (www.sigmadesigns.com) - ROQV Id RoQ File Video Decoder - RPZA Quicktime Apple Video (RPZA) - RUD0 Rududu video codec (http://rududu.ifrance.com/rududu/) - RV10 RealVideo 1.0 (aka RealVideo 5.0) - RV13 RealVideo 1.0 (RV13) - RV20 RealVideo G2 - RV30 RealVideo 8 - RV40 RealVideo 9 - RGBT Raw RGB w/ Transparency - RLE Microsoft Run Length Encoder - RLE4 Run Length Encoded (4bpp, 16-color) - RLE8 Run Length Encoded (8bpp, 256-color) - RT21 Intel Indeo RealTime Video 2.1 - rv20 RealVideo G2 - rv30 RealVideo 8 - RVX Intel RDX (RVX ) - SMC Apple Graphics (SMC ) - SP54 Logitech Sunplus Sp54 Codec for Mustek GSmart Mini 2 - SPIG Radius Spigot - SVQ3 Sorenson Video 3 (Apple Quicktime 5) - s422 Tekram VideoCap C210 YUV 4:2:2 - SDCC Sun Communication Digital Camera Codec - SFMC CrystalNet Surface Fitting Method - SMSC Radius SMSC - SMSD Radius SMSD - smsv WorldConnect Wavelet Video - SPIG Radius Spigot - SPLC Splash Studios ACM Audio Codec (www.splashstudios.net) - SQZ2 Microsoft VXTreme Video Codec V2 - STVA ST Microelectronics CMOS Imager Data (Bayer) - STVB ST Microelectronics CMOS Imager Data (Nudged Bayer) - STVC ST Microelectronics CMOS Imager Data (Bunched) - STVX ST Microelectronics CMOS Imager Data (Extended CODEC Data Format) - STVY ST Microelectronics CMOS Imager Data (Extended CODEC Data Format with Correction Data) - SV10 Sorenson Video R1 - SVQ1 Sorenson Video - T420 Toshiba YUV 4:2:0 - TM2A Duck TrueMotion Archiver 2.0 (www.duck.com) - TVJP Pinnacle/Truevision Targa 2000 board (TVJP) - TVMJ Pinnacle/Truevision Targa 2000 board (TVMJ) - TY0N Tecomac Low-Bit Rate Codec (www.tecomac.com) - TY2C Trident Decompression Driver - TLMS TeraLogic Motion Intraframe Codec (TLMS) - TLST TeraLogic Motion Intraframe Codec (TLST) - TM20 Duck TrueMotion 2.0 - TM2X Duck TrueMotion 2X - TMIC TeraLogic Motion Intraframe Codec (TMIC) - TMOT Horizons Technology TrueMotion S - tmot Horizons TrueMotion Video Compression - TR20 Duck TrueMotion RealTime 2.0 - TSCC TechSmith Screen Capture Codec - TV10 Tecomac Low-Bit Rate Codec - TY2N Trident ?TY2N? - U263 UB Video H.263/H.263+/H.263++ Decoder - UMP4 UB Video MPEG 4 (www.ubvideo.com) - UYNV Nvidia UYVY packed 4:2:2 - UYVP Evans & Sutherland YCbCr 4:2:2 extended precision - UCOD eMajix.com ClearVideo - ULTI IBM Ultimotion - UYVY UYVY packed 4:2:2 - V261 Lucent VX2000S - VIFP VFAPI Reader Codec (www.yks.ne.jp/~hori/) - VIV1 FFmpeg H263+ decoder - VIV2 Vivo H.263 - VQC2 Vector-quantised codec 2 (research) http://eprints.ecs.soton.ac.uk/archive/00001310/01/VTC97-js.pdf) - VTLP Alaris VideoGramPiX - VYU9 ATI YUV (VYU9) - VYUY ATI YUV (VYUY) - V261 Lucent VX2000S - V422 Vitec Multimedia 24-bit YUV 4:2:2 Format - V655 Vitec Multimedia 16-bit YUV 4:2:2 Format - VCR1 ATI Video Codec 1 - VCR2 ATI Video Codec 2 - VCR3 ATI VCR 3.0 - VCR4 ATI VCR 4.0 - VCR5 ATI VCR 5.0 - VCR6 ATI VCR 6.0 - VCR7 ATI VCR 7.0 - VCR8 ATI VCR 8.0 - VCR9 ATI VCR 9.0 - VDCT Vitec Multimedia Video Maker Pro DIB - VDOM VDOnet VDOWave - VDOW VDOnet VDOLive (H.263) - VDTZ Darim Vison VideoTizer YUV - VGPX Alaris VideoGramPiX - VIDS Vitec Multimedia YUV 4:2:2 CCIR 601 for V422 - VIVO Vivo H.263 v2.00 - vivo Vivo H.263 - VIXL Miro/Pinnacle Video XL - VLV1 VideoLogic/PURE Digital Videologic Capture - VP30 On2 VP3.0 - VP31 On2 VP3.1 - VP6F On2 TrueMotion VP6 - VX1K Lucent VX1000S Video Codec - VX2K Lucent VX2000S Video Codec - VXSP Lucent VX1000SP Video Codec - WBVC Winbond W9960 - WHAM Microsoft Video 1 (WHAM) - WINX Winnov Software Compression - WJPG AverMedia Winbond JPEG - WMV1 Windows Media Video V7 - WMV2 Windows Media Video V8 - WMV3 Windows Media Video V9 - WNV1 Winnov Hardware Compression - XYZP Extended PAL format XYZ palette (www.riff.org) - x263 Xirlink H.263 - XLV0 NetXL Video Decoder - XMPG Xing MPEG (I-Frame only) - XVID XviD MPEG-4 (www.xvid.org) - XXAN ?XXAN? - YU92 Intel YUV (YU92) - YUNV Nvidia Uncompressed YUV 4:2:2 - YUVP Extended PAL format YUV palette (www.riff.org) - Y211 YUV 2:1:1 Packed - Y411 YUV 4:1:1 Packed - Y41B Weitek YUV 4:1:1 Planar - Y41P Brooktree PC1 YUV 4:1:1 Packed - Y41T Brooktree PC1 YUV 4:1:1 with transparency - Y42B Weitek YUV 4:2:2 Planar - Y42T Brooktree UYUV 4:2:2 with transparency - Y422 ADS Technologies Copy of UYVY used in Pyro WebCam firewire camera - Y800 Simple, single Y plane for monochrome images - Y8 Grayscale video - YC12 Intel YUV 12 codec - YUV8 Winnov Caviar YUV8 - YUV9 Intel YUV9 - YUY2 Uncompressed YUV 4:2:2 - YUYV Canopus YUV - YV12 YVU12 Planar - YVU9 Intel YVU9 Planar (8-bpp Y plane, followed by 8-bpp 4x4 U and V planes) - YVYU YVYU 4:2:2 Packed - ZLIB Lossless Codec Library zlib compression (www.geocities.co.jp/Playtown-Denei/2837/LRC.htm) - ZPEG Metheus Video Zipper - - */ - - return getid3_lib::EmbeddedLookup($fourcc, $begin, __LINE__, __FILE__, 'riff-fourcc'); - } - - private function EitherEndian2Int($byteword, $signed=false) { - if ($this->getid3->info['fileformat'] == 'riff') { - return getid3_lib::LittleEndian2Int($byteword, $signed); - } - return getid3_lib::BigEndian2Int($byteword, false, $signed); - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio-video.swf.php b/src/Classes/Vendor/getid3/module.audio-video.swf.php deleted file mode 100755 index 48491cbfa..000000000 --- a/src/Classes/Vendor/getid3/module.audio-video.swf.php +++ /dev/null @@ -1,139 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio-video.swf.php // -// module for analyzing Shockwave Flash files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_swf extends getid3_handler -{ - public $ReturnAllTagData = false; - - public function Analyze() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'swf'; - $info['video']['dataformat'] = 'swf'; - - // http://www.openswf.org/spec/SWFfileformat.html - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - - $SWFfileData = fread($this->getid3->fp, $info['avdataend'] - $info['avdataoffset']); // 8 + 2 + 2 + max(9) bytes NOT including Frame_Size RECT data - - $info['swf']['header']['signature'] = substr($SWFfileData, 0, 3); - switch ($info['swf']['header']['signature']) { - case 'FWS': - $info['swf']['header']['compressed'] = false; - break; - - case 'CWS': - $info['swf']['header']['compressed'] = true; - break; - - default: - $info['error'][] = 'Expecting "FWS" or "CWS" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($info['swf']['header']['signature']).'"'; - unset($info['swf']); - unset($info['fileformat']); - return false; - break; - } - $info['swf']['header']['version'] = getid3_lib::LittleEndian2Int(substr($SWFfileData, 3, 1)); - $info['swf']['header']['length'] = getid3_lib::LittleEndian2Int(substr($SWFfileData, 4, 4)); - - if ($info['swf']['header']['compressed']) { - $SWFHead = substr($SWFfileData, 0, 8); - $SWFfileData = substr($SWFfileData, 8); - if ($decompressed = @gzuncompress($SWFfileData)) { - $SWFfileData = $SWFHead.$decompressed; - } else { - $info['error'][] = 'Error decompressing compressed SWF data ('.strlen($SWFfileData).' bytes compressed, should be '.($info['swf']['header']['length'] - 8).' bytes uncompressed)'; - return false; - } - } - - $FrameSizeBitsPerValue = (ord(substr($SWFfileData, 8, 1)) & 0xF8) >> 3; - $FrameSizeDataLength = ceil((5 + (4 * $FrameSizeBitsPerValue)) / 8); - $FrameSizeDataString = str_pad(decbin(ord(substr($SWFfileData, 8, 1)) & 0x07), 3, '0', STR_PAD_LEFT); - for ($i = 1; $i < $FrameSizeDataLength; $i++) { - $FrameSizeDataString .= str_pad(decbin(ord(substr($SWFfileData, 8 + $i, 1))), 8, '0', STR_PAD_LEFT); - } - list($X1, $X2, $Y1, $Y2) = explode("\n", wordwrap($FrameSizeDataString, $FrameSizeBitsPerValue, "\n", 1)); - $info['swf']['header']['frame_width'] = getid3_lib::Bin2Dec($X2); - $info['swf']['header']['frame_height'] = getid3_lib::Bin2Dec($Y2); - - // http://www-lehre.informatik.uni-osnabrueck.de/~fbstark/diplom/docs/swf/Flash_Uncovered.htm - // Next in the header is the frame rate, which is kind of weird. - // It is supposed to be stored as a 16bit integer, but the first byte - // (or last depending on how you look at it) is completely ignored. - // Example: 0x000C -> 0x0C -> 12 So the frame rate is 12 fps. - - // Byte at (8 + $FrameSizeDataLength) is always zero and ignored - $info['swf']['header']['frame_rate'] = getid3_lib::LittleEndian2Int(substr($SWFfileData, 9 + $FrameSizeDataLength, 1)); - $info['swf']['header']['frame_count'] = getid3_lib::LittleEndian2Int(substr($SWFfileData, 10 + $FrameSizeDataLength, 2)); - - $info['video']['frame_rate'] = $info['swf']['header']['frame_rate']; - $info['video']['resolution_x'] = intval(round($info['swf']['header']['frame_width'] / 20)); - $info['video']['resolution_y'] = intval(round($info['swf']['header']['frame_height'] / 20)); - $info['video']['pixel_aspect_ratio'] = (float) 1; - - if (($info['swf']['header']['frame_count'] > 0) && ($info['swf']['header']['frame_rate'] > 0)) { - $info['playtime_seconds'] = $info['swf']['header']['frame_count'] / $info['swf']['header']['frame_rate']; - } -//echo __LINE__.'='.number_format(microtime(true) - $start_time, 3).'
    '; - - - // SWF tags - - $CurrentOffset = 12 + $FrameSizeDataLength; - $SWFdataLength = strlen($SWFfileData); - - while ($CurrentOffset < $SWFdataLength) { -//echo __LINE__.'='.number_format(microtime(true) - $start_time, 3).'
    '; - - $TagIDTagLength = getid3_lib::LittleEndian2Int(substr($SWFfileData, $CurrentOffset, 2)); - $TagID = ($TagIDTagLength & 0xFFFC) >> 6; - $TagLength = ($TagIDTagLength & 0x003F); - $CurrentOffset += 2; - if ($TagLength == 0x3F) { - $TagLength = getid3_lib::LittleEndian2Int(substr($SWFfileData, $CurrentOffset, 4)); - $CurrentOffset += 4; - } - - unset($TagData); - $TagData['offset'] = $CurrentOffset; - $TagData['size'] = $TagLength; - $TagData['id'] = $TagID; - $TagData['data'] = substr($SWFfileData, $CurrentOffset, $TagLength); - switch ($TagID) { - case 0: // end of movie - break 2; - - case 9: // Set background color - //$info['swf']['tags'][] = $TagData; - $info['swf']['bgcolor'] = strtoupper(str_pad(dechex(getid3_lib::BigEndian2Int($TagData['data'])), 6, '0', STR_PAD_LEFT)); - break; - - default: - if ($this->ReturnAllTagData) { - $info['swf']['tags'][] = $TagData; - } - break; - } - - $CurrentOffset += $TagLength; - } - - return true; - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio-video.ts.php b/src/Classes/Vendor/getid3/module.audio-video.ts.php deleted file mode 100755 index 3fcf71ea3..000000000 --- a/src/Classes/Vendor/getid3/module.audio-video.ts.php +++ /dev/null @@ -1,78 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio-video.ts.php // -// module for analyzing MPEG Transport Stream (.ts) files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_ts extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $TSheader = fread($this->getid3->fp, 19); - $magic = "\x47"; - if (substr($TSheader, 0, 1) != $magic) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" at '.$info['avdataoffset'].', found '.getid3_lib::PrintHexBytes(substr($TSheader, 0, 1)).' instead.'; - return false; - } - $info['fileformat'] = 'ts'; - - // http://en.wikipedia.org/wiki/.ts - - $offset = 0; - $info['ts']['packet']['sync'] = getid3_lib::BigEndian2Int(substr($TSheader, $offset, 1)); $offset += 1; - $pid_flags_raw = getid3_lib::BigEndian2Int(substr($TSheader, $offset, 2)); $offset += 2; - $SAC_raw = getid3_lib::BigEndian2Int(substr($TSheader, $offset, 1)); $offset += 1; - $info['ts']['packet']['flags']['transport_error_indicator'] = (bool) ($pid_flags_raw & 0x8000); // Set by demodulator if can't correct errors in the stream, to tell the demultiplexer that the packet has an uncorrectable error - $info['ts']['packet']['flags']['payload_unit_start_indicator'] = (bool) ($pid_flags_raw & 0x4000); // 1 means start of PES data or PSI otherwise zero only. - $info['ts']['packet']['flags']['transport_high_priority'] = (bool) ($pid_flags_raw & 0x2000); // 1 means higher priority than other packets with the same PID. - $info['ts']['packet']['packet_id'] = ($pid_flags_raw & 0x1FFF) >> 0; - - $info['ts']['packet']['raw']['scrambling_control'] = ($SAC_raw & 0xC0) >> 6; - $info['ts']['packet']['flags']['adaption_field_exists'] = (bool) ($SAC_raw & 0x20); - $info['ts']['packet']['flags']['payload_exists'] = (bool) ($SAC_raw & 0x10); - $info['ts']['packet']['continuity_counter'] = ($SAC_raw & 0x0F) >> 0; // Incremented only when a payload is present - $info['ts']['packet']['scrambling_control'] = $this->TSscramblingControlLookup($info['ts']['packet']['raw']['scrambling_control']); - - if ($info['ts']['packet']['flags']['adaption_field_exists']) { - $AdaptionField_raw = getid3_lib::BigEndian2Int(substr($TSheader, $offset, 2)); $offset += 2; - $info['ts']['packet']['adaption']['field_length'] = ($AdaptionField_raw & 0xFF00) >> 8; // Number of bytes in the adaptation field immediately following this byte - $info['ts']['packet']['adaption']['flags']['discontinuity'] = (bool) ($AdaptionField_raw & 0x0080); // Set to 1 if current TS packet is in a discontinuity state with respect to either the continuity counter or the program clock reference - $info['ts']['packet']['adaption']['flags']['random_access'] = (bool) ($AdaptionField_raw & 0x0040); // Set to 1 if the PES packet in this TS packet starts a video/audio sequence - $info['ts']['packet']['adaption']['flags']['high_priority'] = (bool) ($AdaptionField_raw & 0x0020); // 1 = higher priority - $info['ts']['packet']['adaption']['flags']['pcr'] = (bool) ($AdaptionField_raw & 0x0010); // 1 means adaptation field does contain a PCR field - $info['ts']['packet']['adaption']['flags']['opcr'] = (bool) ($AdaptionField_raw & 0x0008); // 1 means adaptation field does contain an OPCR field - $info['ts']['packet']['adaption']['flags']['splice_point'] = (bool) ($AdaptionField_raw & 0x0004); // 1 means presence of splice countdown field in adaptation field - $info['ts']['packet']['adaption']['flags']['private_data'] = (bool) ($AdaptionField_raw & 0x0002); // 1 means presence of private data bytes in adaptation field - $info['ts']['packet']['adaption']['flags']['extension'] = (bool) ($AdaptionField_raw & 0x0001); // 1 means presence of adaptation field extension - if ($info['ts']['packet']['adaption']['flags']['pcr']) { - $info['ts']['packet']['adaption']['raw']['pcr'] = getid3_lib::BigEndian2Int(substr($TSheader, $offset, 6)); $offset += 6; - } - if ($info['ts']['packet']['adaption']['flags']['opcr']) { - $info['ts']['packet']['adaption']['raw']['opcr'] = getid3_lib::BigEndian2Int(substr($TSheader, $offset, 6)); $offset += 6; - } - } - -$info['error'][] = 'MPEG Transport Stream (.ts) parsing not enabled in this version of getID3() ['.$this->getid3->version().']'; -return false; - - } - - - public function TSscramblingControlLookup($raw) { - $TSscramblingControlLookup = array(0x00=>'not scrambled', 0x01=>'reserved', 0x02=>'scrambled, even key', 0x03=>'scrambled, odd key'); - return (isset($TSscramblingControlLookup[$raw]) ? $TSscramblingControlLookup[$raw] : 'invalid'); - } -} diff --git a/src/Classes/Vendor/getid3/module.audio.aa.php b/src/Classes/Vendor/getid3/module.audio.aa.php deleted file mode 100755 index dbf1b7c8c..000000000 --- a/src/Classes/Vendor/getid3/module.audio.aa.php +++ /dev/null @@ -1,58 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.aa.php // -// module for analyzing Audible Audiobook files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_aa extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $AAheader = fread($this->getid3->fp, 8); - - $magic = "\x57\x90\x75\x36"; - if (substr($AAheader, 4, 4) != $magic) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($AAheader, 4, 4)).'"'; - return false; - } - - // shortcut - $info['aa'] = array(); - $thisfile_au = &$info['aa']; - - $info['fileformat'] = 'aa'; - $info['audio']['dataformat'] = 'aa'; -$info['error'][] = 'Audible Audiobook (.aa) parsing not enabled in this version of getID3() ['.$this->getid3->version().']'; -return false; - $info['audio']['bitrate_mode'] = 'cbr'; // is it? - $thisfile_au['encoding'] = 'ISO-8859-1'; - - $thisfile_au['filesize'] = getid3_lib::BigEndian2Int(substr($AUheader, 0, 4)); - if ($thisfile_au['filesize'] > ($info['avdataend'] - $info['avdataoffset'])) { - $info['warning'][] = 'Possible truncated file - expecting "'.$thisfile_au['filesize'].'" bytes of data, only found '.($info['avdataend'] - $info['avdataoffset']).' bytes"'; - } - - $info['audio']['bits_per_sample'] = 16; // is it? - $info['audio']['sample_rate'] = $thisfile_au['sample_rate']; - $info['audio']['channels'] = $thisfile_au['channels']; - - //$info['playtime_seconds'] = 0; - //$info['audio']['bitrate'] = 0; - - return true; - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.aac.php b/src/Classes/Vendor/getid3/module.audio.aac.php deleted file mode 100755 index 537ce567c..000000000 --- a/src/Classes/Vendor/getid3/module.audio.aac.php +++ /dev/null @@ -1,512 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.aac.php // -// module for analyzing AAC Audio files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_aac extends getid3_handler -{ - public function Analyze() { - $info = &$this->getid3->info; - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - if (fread($this->getid3->fp, 4) == 'ADIF') { - $this->getAACADIFheaderFilepointer(); - } else { - $this->getAACADTSheaderFilepointer(); - } - return true; - } - - - - public function getAACADIFheaderFilepointer() { - $info = &$this->getid3->info; - $info['fileformat'] = 'aac'; - $info['audio']['dataformat'] = 'aac'; - $info['audio']['lossless'] = false; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $AACheader = fread($this->getid3->fp, 1024); - $offset = 0; - - if (substr($AACheader, 0, 4) == 'ADIF') { - - // http://faac.sourceforge.net/wiki/index.php?page=ADIF - - // http://libmpeg.org/mpeg4/doc/w2203tfs.pdf - // adif_header() { - // adif_id 32 - // copyright_id_present 1 - // if( copyright_id_present ) - // copyright_id 72 - // original_copy 1 - // home 1 - // bitstream_type 1 - // bitrate 23 - // num_program_config_elements 4 - // for (i = 0; i < num_program_config_elements + 1; i++ ) { - // if( bitstream_type == '0' ) - // adif_buffer_fullness 20 - // program_config_element() - // } - // } - - $AACheaderBitstream = getid3_lib::BigEndian2Bin($AACheader); - $bitoffset = 0; - - $info['aac']['header_type'] = 'ADIF'; - $bitoffset += 32; - $info['aac']['header']['mpeg_version'] = 4; - - $info['aac']['header']['copyright'] = (bool) (substr($AACheaderBitstream, $bitoffset, 1) == '1'); - $bitoffset += 1; - if ($info['aac']['header']['copyright']) { - $info['aac']['header']['copyright_id'] = getid3_lib::Bin2String(substr($AACheaderBitstream, $bitoffset, 72)); - $bitoffset += 72; - } - $info['aac']['header']['original_copy'] = (bool) (substr($AACheaderBitstream, $bitoffset, 1) == '1'); - $bitoffset += 1; - $info['aac']['header']['home'] = (bool) (substr($AACheaderBitstream, $bitoffset, 1) == '1'); - $bitoffset += 1; - $info['aac']['header']['is_vbr'] = (bool) (substr($AACheaderBitstream, $bitoffset, 1) == '1'); - $bitoffset += 1; - if ($info['aac']['header']['is_vbr']) { - $info['audio']['bitrate_mode'] = 'vbr'; - $info['aac']['header']['bitrate_max'] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 23)); - $bitoffset += 23; - } else { - $info['audio']['bitrate_mode'] = 'cbr'; - $info['aac']['header']['bitrate'] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 23)); - $bitoffset += 23; - $info['audio']['bitrate'] = $info['aac']['header']['bitrate']; - } - if ($info['audio']['bitrate'] == 0) { - $info['error'][] = 'Corrupt AAC file: bitrate_audio == zero'; - return false; - } - $info['aac']['header']['num_program_configs'] = 1 + getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 4)); - $bitoffset += 4; - - for ($i = 0; $i < $info['aac']['header']['num_program_configs']; $i++) { - // http://www.audiocoding.com/wiki/index.php?page=program_config_element - - // buffer_fullness 20 - - // element_instance_tag 4 - // object_type 2 - // sampling_frequency_index 4 - // num_front_channel_elements 4 - // num_side_channel_elements 4 - // num_back_channel_elements 4 - // num_lfe_channel_elements 2 - // num_assoc_data_elements 3 - // num_valid_cc_elements 4 - // mono_mixdown_present 1 - // mono_mixdown_element_number 4 if mono_mixdown_present == 1 - // stereo_mixdown_present 1 - // stereo_mixdown_element_number 4 if stereo_mixdown_present == 1 - // matrix_mixdown_idx_present 1 - // matrix_mixdown_idx 2 if matrix_mixdown_idx_present == 1 - // pseudo_surround_enable 1 if matrix_mixdown_idx_present == 1 - // for (i = 0; i < num_front_channel_elements; i++) { - // front_element_is_cpe[i] 1 - // front_element_tag_select[i] 4 - // } - // for (i = 0; i < num_side_channel_elements; i++) { - // side_element_is_cpe[i] 1 - // side_element_tag_select[i] 4 - // } - // for (i = 0; i < num_back_channel_elements; i++) { - // back_element_is_cpe[i] 1 - // back_element_tag_select[i] 4 - // } - // for (i = 0; i < num_lfe_channel_elements; i++) { - // lfe_element_tag_select[i] 4 - // } - // for (i = 0; i < num_assoc_data_elements; i++) { - // assoc_data_element_tag_select[i] 4 - // } - // for (i = 0; i < num_valid_cc_elements; i++) { - // cc_element_is_ind_sw[i] 1 - // valid_cc_element_tag_select[i] 4 - // } - // byte_alignment() VAR - // comment_field_bytes 8 - // for (i = 0; i < comment_field_bytes; i++) { - // comment_field_data[i] 8 - // } - - if (!$info['aac']['header']['is_vbr']) { - $info['aac']['program_configs'][$i]['buffer_fullness'] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 20)); - $bitoffset += 20; - } - $info['aac']['program_configs'][$i]['element_instance_tag'] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 4)); - $bitoffset += 4; - $info['aac']['program_configs'][$i]['object_type'] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 2)); - $bitoffset += 2; - $info['aac']['program_configs'][$i]['sampling_frequency_index'] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 4)); - $bitoffset += 4; - $info['aac']['program_configs'][$i]['num_front_channel_elements'] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 4)); - $bitoffset += 4; - $info['aac']['program_configs'][$i]['num_side_channel_elements'] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 4)); - $bitoffset += 4; - $info['aac']['program_configs'][$i]['num_back_channel_elements'] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 4)); - $bitoffset += 4; - $info['aac']['program_configs'][$i]['num_lfe_channel_elements'] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 2)); - $bitoffset += 2; - $info['aac']['program_configs'][$i]['num_assoc_data_elements'] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 3)); - $bitoffset += 3; - $info['aac']['program_configs'][$i]['num_valid_cc_elements'] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 4)); - $bitoffset += 4; - $info['aac']['program_configs'][$i]['mono_mixdown_present'] = (bool) getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 1)); - $bitoffset += 1; - if ($info['aac']['program_configs'][$i]['mono_mixdown_present']) { - $info['aac']['program_configs'][$i]['mono_mixdown_element_number'] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 4)); - $bitoffset += 4; - } - $info['aac']['program_configs'][$i]['stereo_mixdown_present'] = (bool) getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 1)); - $bitoffset += 1; - if ($info['aac']['program_configs'][$i]['stereo_mixdown_present']) { - $info['aac']['program_configs'][$i]['stereo_mixdown_element_number'] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 4)); - $bitoffset += 4; - } - $info['aac']['program_configs'][$i]['matrix_mixdown_idx_present'] = (bool) getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 1)); - $bitoffset += 1; - if ($info['aac']['program_configs'][$i]['matrix_mixdown_idx_present']) { - $info['aac']['program_configs'][$i]['matrix_mixdown_idx'] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 2)); - $bitoffset += 2; - $info['aac']['program_configs'][$i]['pseudo_surround_enable'] = (bool) getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 1)); - $bitoffset += 1; - } - for ($j = 0; $j < $info['aac']['program_configs'][$i]['num_front_channel_elements']; $j++) { - $info['aac']['program_configs'][$i]['front_element_is_cpe'][$j] = (bool) getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 1)); - $bitoffset += 1; - $info['aac']['program_configs'][$i]['front_element_tag_select'][$j] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 4)); - $bitoffset += 4; - } - for ($j = 0; $j < $info['aac']['program_configs'][$i]['num_side_channel_elements']; $j++) { - $info['aac']['program_configs'][$i]['side_element_is_cpe'][$j] = (bool) getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 1)); - $bitoffset += 1; - $info['aac']['program_configs'][$i]['side_element_tag_select'][$j] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 4)); - $bitoffset += 4; - } - for ($j = 0; $j < $info['aac']['program_configs'][$i]['num_back_channel_elements']; $j++) { - $info['aac']['program_configs'][$i]['back_element_is_cpe'][$j] = (bool) getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 1)); - $bitoffset += 1; - $info['aac']['program_configs'][$i]['back_element_tag_select'][$j] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 4)); - $bitoffset += 4; - } - for ($j = 0; $j < $info['aac']['program_configs'][$i]['num_lfe_channel_elements']; $j++) { - $info['aac']['program_configs'][$i]['lfe_element_tag_select'][$j] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 4)); - $bitoffset += 4; - } - for ($j = 0; $j < $info['aac']['program_configs'][$i]['num_assoc_data_elements']; $j++) { - $info['aac']['program_configs'][$i]['assoc_data_element_tag_select'][$j] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 4)); - $bitoffset += 4; - } - for ($j = 0; $j < $info['aac']['program_configs'][$i]['num_valid_cc_elements']; $j++) { - $info['aac']['program_configs'][$i]['cc_element_is_ind_sw'][$j] = (bool) getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 1)); - $bitoffset += 1; - $info['aac']['program_configs'][$i]['valid_cc_element_tag_select'][$j] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 4)); - $bitoffset += 4; - } - - $bitoffset = ceil($bitoffset / 8) * 8; - - $info['aac']['program_configs'][$i]['comment_field_bytes'] = getid3_lib::Bin2Dec(substr($AACheaderBitstream, $bitoffset, 8)); - $bitoffset += 8; - $info['aac']['program_configs'][$i]['comment_field'] = getid3_lib::Bin2String(substr($AACheaderBitstream, $bitoffset, 8 * $info['aac']['program_configs'][$i]['comment_field_bytes'])); - $bitoffset += 8 * $info['aac']['program_configs'][$i]['comment_field_bytes']; - - - $info['aac']['header']['profile'] = self::AACprofileLookup($info['aac']['program_configs'][$i]['object_type'], $info['aac']['header']['mpeg_version']); - $info['aac']['program_configs'][$i]['sampling_frequency'] = self::AACsampleRateLookup($info['aac']['program_configs'][$i]['sampling_frequency_index']); - $info['audio']['sample_rate'] = $info['aac']['program_configs'][$i]['sampling_frequency']; - $info['audio']['channels'] = self::AACchannelCountCalculate($info['aac']['program_configs'][$i]); - if ($info['aac']['program_configs'][$i]['comment_field']) { - $info['aac']['comments'][] = $info['aac']['program_configs'][$i]['comment_field']; - } - } - $info['playtime_seconds'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['audio']['bitrate']; - - $info['audio']['encoder_options'] = $info['aac']['header_type'].' '.$info['aac']['header']['profile']; - - - - return true; - - } else { - - unset($info['fileformat']); - unset($info['aac']); - $info['error'][] = 'AAC-ADIF synch not found at offset '.$info['avdataoffset'].' (expected "ADIF", found "'.substr($AACheader, 0, 4).'" instead)'; - return false; - - } - - } - - - public function getAACADTSheaderFilepointer($MaxFramesToScan=1000000, $ReturnExtendedInfo=false) { - $info = &$this->getid3->info; - - // based loosely on code from AACfile by Jurgen Faul - // http://jfaul.de/atl or http://j-faul.virtualave.net/atl/atl.html - - - // http://faac.sourceforge.net/wiki/index.php?page=ADTS // dead link - // http://wiki.multimedia.cx/index.php?title=ADTS - - // * ADTS Fixed Header: these don't change from frame to frame - // syncword 12 always: '111111111111' - // ID 1 0: MPEG-4, 1: MPEG-2 - // MPEG layer 2 If you send AAC in MPEG-TS, set to 0 - // protection_absent 1 0: CRC present; 1: no CRC - // profile 2 0: AAC Main; 1: AAC LC (Low Complexity); 2: AAC SSR (Scalable Sample Rate); 3: AAC LTP (Long Term Prediction) - // sampling_frequency_index 4 15 not allowed - // private_bit 1 usually 0 - // channel_configuration 3 - // original/copy 1 0: original; 1: copy - // home 1 usually 0 - // emphasis 2 only if ID == 0 (ie MPEG-4) // not present in some documentation? - - // * ADTS Variable Header: these can change from frame to frame - // copyright_identification_bit 1 - // copyright_identification_start 1 - // aac_frame_length 13 length of the frame including header (in bytes) - // adts_buffer_fullness 11 0x7FF indicates VBR - // no_raw_data_blocks_in_frame 2 - - // * ADTS Error check - // crc_check 16 only if protection_absent == 0 - - $byteoffset = $info['avdataoffset']; - $framenumber = 0; - - // Init bit pattern array - static $decbin = array(); - - // Populate $bindec - for ($i = 0; $i < 256; $i++) { - $decbin[chr($i)] = str_pad(decbin($i), 8, '0', STR_PAD_LEFT); - } - - // used to calculate bitrate below - $BitrateCache = array(); - - - while (true) { - // breaks out when end-of-file encountered, or invalid data found, - // or MaxFramesToScan frames have been scanned - - if (!getid3_lib::intValueSupported($byteoffset)) { - $info['warning'][] = 'Unable to parse AAC file beyond '.ftell($this->getid3->fp).' (PHP does not support file operations beyond '.round(PHP_INT_MAX / 1073741824).'GB)'; - return false; - } - fseek($this->getid3->fp, $byteoffset, SEEK_SET); - - // First get substring - $substring = fread($this->getid3->fp, 9); // header is 7 bytes (or 9 if CRC is present) - $substringlength = strlen($substring); - if ($substringlength != 9) { - $info['error'][] = 'Failed to read 7 bytes at offset '.(ftell($this->getid3->fp) - $substringlength).' (only read '.$substringlength.' bytes)'; - return false; - } - // this would be easier with 64-bit math, but split it up to allow for 32-bit: - $header1 = getid3_lib::BigEndian2Int(substr($substring, 0, 2)); - $header2 = getid3_lib::BigEndian2Int(substr($substring, 2, 4)); - $header3 = getid3_lib::BigEndian2Int(substr($substring, 6, 1)); - - $info['aac']['header']['raw']['syncword'] = ($header1 & 0xFFF0) >> 4; - if ($info['aac']['header']['raw']['syncword'] != 0x0FFF) { - $info['error'][] = 'Synch pattern (0x0FFF) not found at offset '.(ftell($this->getid3->fp) - $substringlength).' (found 0x0'.strtoupper(dechex($info['aac']['header']['raw']['syncword'])).' instead)'; - //if ($info['fileformat'] == 'aac') { - // return true; - //} - unset($info['aac']); - return false; - } - - // Gather info for first frame only - this takes time to do 1000 times! - if ($framenumber == 0) { - $info['aac']['header_type'] = 'ADTS'; - $info['fileformat'] = 'aac'; - $info['audio']['dataformat'] = 'aac'; - - $info['aac']['header']['raw']['mpeg_version'] = ($header1 & 0x0008) >> 3; - $info['aac']['header']['raw']['mpeg_layer'] = ($header1 & 0x0006) >> 1; - $info['aac']['header']['raw']['protection_absent'] = ($header1 & 0x0001) >> 0; - - $info['aac']['header']['raw']['profile_code'] = ($header2 & 0xC0000000) >> 30; - $info['aac']['header']['raw']['sample_rate_code'] = ($header2 & 0x3C000000) >> 26; - $info['aac']['header']['raw']['private_stream'] = ($header2 & 0x02000000) >> 25; - $info['aac']['header']['raw']['channels_code'] = ($header2 & 0x01C00000) >> 22; - $info['aac']['header']['raw']['original'] = ($header2 & 0x00200000) >> 21; - $info['aac']['header']['raw']['home'] = ($header2 & 0x00100000) >> 20; - $info['aac']['header']['raw']['copyright_stream'] = ($header2 & 0x00080000) >> 19; - $info['aac']['header']['raw']['copyright_start'] = ($header2 & 0x00040000) >> 18; - $info['aac']['header']['raw']['frame_length'] = ($header2 & 0x0003FFE0) >> 5; - - $info['aac']['header']['mpeg_version'] = ($info['aac']['header']['raw']['mpeg_version'] ? 2 : 4); - $info['aac']['header']['crc_present'] = ($info['aac']['header']['raw']['protection_absent'] ? false: true); - $info['aac']['header']['profile'] = self::AACprofileLookup($info['aac']['header']['raw']['profile_code'], $info['aac']['header']['mpeg_version']); - $info['aac']['header']['sample_frequency'] = self::AACsampleRateLookup($info['aac']['header']['raw']['sample_rate_code']); - $info['aac']['header']['private'] = (bool) $info['aac']['header']['raw']['private_stream']; - $info['aac']['header']['original'] = (bool) $info['aac']['header']['raw']['original']; - $info['aac']['header']['home'] = (bool) $info['aac']['header']['raw']['home']; - $info['aac']['header']['channels'] = (($info['aac']['header']['raw']['channels_code'] == 7) ? 8 : $info['aac']['header']['raw']['channels_code']); - if ($ReturnExtendedInfo) { - $info['aac'][$framenumber]['copyright_id_bit'] = (bool) $info['aac']['header']['raw']['copyright_stream']; - $info['aac'][$framenumber]['copyright_id_start'] = (bool) $info['aac']['header']['raw']['copyright_start']; - } - - if ($info['aac']['header']['raw']['mpeg_layer'] != 0) { - $info['warning'][] = 'Layer error - expected "0", found "'.$info['aac']['header']['raw']['mpeg_layer'].'" instead'; - } - if ($info['aac']['header']['sample_frequency'] == 0) { - $info['error'][] = 'Corrupt AAC file: sample_frequency == zero'; - return false; - } - - $info['audio']['sample_rate'] = $info['aac']['header']['sample_frequency']; - $info['audio']['channels'] = $info['aac']['header']['channels']; - } - - $FrameLength = ($header2 & 0x0003FFE0) >> 5; - - if (!isset($BitrateCache[$FrameLength])) { - $BitrateCache[$FrameLength] = ($info['aac']['header']['sample_frequency'] / 1024) * $FrameLength * 8; - } - getid3_lib::safe_inc($info['aac']['bitrate_distribution'][$BitrateCache[$FrameLength]], 1); - - $info['aac'][$framenumber]['aac_frame_length'] = $FrameLength; - - $info['aac'][$framenumber]['adts_buffer_fullness'] = (($header2 & 0x0000001F) << 6) & (($header3 & 0xFC) >> 2); - if ($info['aac'][$framenumber]['adts_buffer_fullness'] == 0x07FF) { - $info['audio']['bitrate_mode'] = 'vbr'; - } else { - $info['audio']['bitrate_mode'] = 'cbr'; - } - $info['aac'][$framenumber]['num_raw_data_blocks'] = (($header3 & 0x03) >> 0); - - if ($info['aac']['header']['crc_present']) { - //$info['aac'][$framenumber]['crc'] = getid3_lib::BigEndian2Int(substr($substring, 7, 2); - } - - if (!$ReturnExtendedInfo) { - unset($info['aac'][$framenumber]); - } - - /* - $rounded_precision = 5000; - $info['aac']['bitrate_distribution_rounded'] = array(); - foreach ($info['aac']['bitrate_distribution'] as $bitrate => $count) { - $rounded_bitrate = round($bitrate / $rounded_precision) * $rounded_precision; - getid3_lib::safe_inc($info['aac']['bitrate_distribution_rounded'][$rounded_bitrate], $count); - } - ksort($info['aac']['bitrate_distribution_rounded']); - */ - - $byteoffset += $FrameLength; - if ((++$framenumber < $MaxFramesToScan) && (($byteoffset + 10) < $info['avdataend'])) { - - // keep scanning - - } else { - - $info['aac']['frames'] = $framenumber; - $info['playtime_seconds'] = ($info['avdataend'] / $byteoffset) * (($framenumber * 1024) / $info['aac']['header']['sample_frequency']); // (1 / % of file scanned) * (samples / (samples/sec)) = seconds - if ($info['playtime_seconds'] == 0) { - $info['error'][] = 'Corrupt AAC file: playtime_seconds == zero'; - return false; - } - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; - ksort($info['aac']['bitrate_distribution']); - - $info['audio']['encoder_options'] = $info['aac']['header_type'].' '.$info['aac']['header']['profile']; - - return true; - - } - } - // should never get here. - } - - public static function AACsampleRateLookup($samplerateid) { - static $AACsampleRateLookup = array(); - if (empty($AACsampleRateLookup)) { - $AACsampleRateLookup[0] = 96000; - $AACsampleRateLookup[1] = 88200; - $AACsampleRateLookup[2] = 64000; - $AACsampleRateLookup[3] = 48000; - $AACsampleRateLookup[4] = 44100; - $AACsampleRateLookup[5] = 32000; - $AACsampleRateLookup[6] = 24000; - $AACsampleRateLookup[7] = 22050; - $AACsampleRateLookup[8] = 16000; - $AACsampleRateLookup[9] = 12000; - $AACsampleRateLookup[10] = 11025; - $AACsampleRateLookup[11] = 8000; - $AACsampleRateLookup[12] = 0; - $AACsampleRateLookup[13] = 0; - $AACsampleRateLookup[14] = 0; - $AACsampleRateLookup[15] = 0; - } - return (isset($AACsampleRateLookup[$samplerateid]) ? $AACsampleRateLookup[$samplerateid] : 'invalid'); - } - - public static function AACprofileLookup($profileid, $mpegversion) { - static $AACprofileLookup = array(); - if (empty($AACprofileLookup)) { - $AACprofileLookup[2][0] = 'Main profile'; - $AACprofileLookup[2][1] = 'Low Complexity profile (LC)'; - $AACprofileLookup[2][2] = 'Scalable Sample Rate profile (SSR)'; - $AACprofileLookup[2][3] = '(reserved)'; - $AACprofileLookup[4][0] = 'AAC_MAIN'; - $AACprofileLookup[4][1] = 'AAC_LC'; - $AACprofileLookup[4][2] = 'AAC_SSR'; - $AACprofileLookup[4][3] = 'AAC_LTP'; - } - return (isset($AACprofileLookup[$mpegversion][$profileid]) ? $AACprofileLookup[$mpegversion][$profileid] : 'invalid'); - } - - public static function AACchannelCountCalculate($program_configs) { - $channels = 0; - for ($i = 0; $i < $program_configs['num_front_channel_elements']; $i++) { - $channels++; - if ($program_configs['front_element_is_cpe'][$i]) { - // each front element is channel pair (CPE = Channel Pair Element) - $channels++; - } - } - for ($i = 0; $i < $program_configs['num_side_channel_elements']; $i++) { - $channels++; - if ($program_configs['side_element_is_cpe'][$i]) { - // each side element is channel pair (CPE = Channel Pair Element) - $channels++; - } - } - for ($i = 0; $i < $program_configs['num_back_channel_elements']; $i++) { - $channels++; - if ($program_configs['back_element_is_cpe'][$i]) { - // each back element is channel pair (CPE = Channel Pair Element) - $channels++; - } - } - for ($i = 0; $i < $program_configs['num_lfe_channel_elements']; $i++) { - $channels++; - } - return $channels; - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.ac3.php b/src/Classes/Vendor/getid3/module.audio.ac3.php deleted file mode 100755 index 9834feb5b..000000000 --- a/src/Classes/Vendor/getid3/module.audio.ac3.php +++ /dev/null @@ -1,473 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.ac3.php // -// module for analyzing AC-3 (aka Dolby Digital) audio files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_ac3 extends getid3_handler -{ - private $AC3header = array(); - private $BSIoffset = 0; - - const syncword = "\x0B\x77"; - - public function Analyze() { - $info = &$this->getid3->info; - - ///AH - $info['ac3']['raw']['bsi'] = array(); - $thisfile_ac3 = &$info['ac3']; - $thisfile_ac3_raw = &$thisfile_ac3['raw']; - $thisfile_ac3_raw_bsi = &$thisfile_ac3_raw['bsi']; - - - // http://www.atsc.org/standards/a_52a.pdf - - $info['fileformat'] = 'ac3'; - - // An AC-3 serial coded audio bit stream is made up of a sequence of synchronization frames - // Each synchronization frame contains 6 coded audio blocks (AB), each of which represent 256 - // new audio samples per channel. A synchronization information (SI) header at the beginning - // of each frame contains information needed to acquire and maintain synchronization. A - // bit stream information (BSI) header follows SI, and contains parameters describing the coded - // audio service. The coded audio blocks may be followed by an auxiliary data (Aux) field. At the - // end of each frame is an error check field that includes a CRC word for error detection. An - // additional CRC word is located in the SI header, the use of which, by a decoder, is optional. - // - // syncinfo() | bsi() | AB0 | AB1 | AB2 | AB3 | AB4 | AB5 | Aux | CRC - - // syncinfo() { - // syncword 16 - // crc1 16 - // fscod 2 - // frmsizecod 6 - // } /* end of syncinfo */ - - $this->fseek($info['avdataoffset']); - $this->AC3header['syncinfo'] = $this->fread(5); - - if (strpos($this->AC3header['syncinfo'], self::syncword) === 0) { - $thisfile_ac3_raw['synchinfo']['synchword'] = self::syncword; - $offset = 2; - } else { - if (!$this->isDependencyFor('matroska')) { - unset($info['fileformat'], $info['ac3']); - return $this->error('Expecting "'.getid3_lib::PrintHexBytes(self::syncword).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($this->AC3header['syncinfo'], 0, 2)).'"'); - } - $offset = 0; - $this->fseek(-2, SEEK_CUR); - } - - $info['audio']['dataformat'] = 'ac3'; - $info['audio']['bitrate_mode'] = 'cbr'; - $info['audio']['lossless'] = false; - - $thisfile_ac3_raw['synchinfo']['crc1'] = getid3_lib::LittleEndian2Int(substr($this->AC3header['syncinfo'], $offset, 2)); - $ac3_synchinfo_fscod_frmsizecod = getid3_lib::LittleEndian2Int(substr($this->AC3header['syncinfo'], ($offset + 2), 1)); - $thisfile_ac3_raw['synchinfo']['fscod'] = ($ac3_synchinfo_fscod_frmsizecod & 0xC0) >> 6; - $thisfile_ac3_raw['synchinfo']['frmsizecod'] = ($ac3_synchinfo_fscod_frmsizecod & 0x3F); - - $thisfile_ac3['sample_rate'] = self::sampleRateCodeLookup($thisfile_ac3_raw['synchinfo']['fscod']); - if ($thisfile_ac3_raw['synchinfo']['fscod'] <= 3) { - $info['audio']['sample_rate'] = $thisfile_ac3['sample_rate']; - } - - $thisfile_ac3['frame_length'] = self::frameSizeLookup($thisfile_ac3_raw['synchinfo']['frmsizecod'], $thisfile_ac3_raw['synchinfo']['fscod']); - $thisfile_ac3['bitrate'] = self::bitrateLookup($thisfile_ac3_raw['synchinfo']['frmsizecod']); - $info['audio']['bitrate'] = $thisfile_ac3['bitrate']; - - $this->AC3header['bsi'] = getid3_lib::BigEndian2Bin($this->fread(15)); - $ac3_bsi_offset = 0; - - $thisfile_ac3_raw_bsi['bsid'] = $this->readHeaderBSI(5); - if ($thisfile_ac3_raw_bsi['bsid'] > 8) { - // Decoders which can decode version 8 will thus be able to decode version numbers less than 8. - // If this standard is extended by the addition of additional elements or features, a value of bsid greater than 8 will be used. - // Decoders built to this version of the standard will not be able to decode versions with bsid greater than 8. - $this->error('Bit stream identification is version '.$thisfile_ac3_raw_bsi['bsid'].', but getID3() only understands up to version 8'); - unset($info['ac3']); - return false; - } - - $thisfile_ac3_raw_bsi['bsmod'] = $this->readHeaderBSI(3); - $thisfile_ac3_raw_bsi['acmod'] = $this->readHeaderBSI(3); - - $thisfile_ac3['service_type'] = self::serviceTypeLookup($thisfile_ac3_raw_bsi['bsmod'], $thisfile_ac3_raw_bsi['acmod']); - $ac3_coding_mode = self::audioCodingModeLookup($thisfile_ac3_raw_bsi['acmod']); - foreach($ac3_coding_mode as $key => $value) { - $thisfile_ac3[$key] = $value; - } - switch ($thisfile_ac3_raw_bsi['acmod']) { - case 0: - case 1: - $info['audio']['channelmode'] = 'mono'; - break; - case 3: - case 4: - $info['audio']['channelmode'] = 'stereo'; - break; - default: - $info['audio']['channelmode'] = 'surround'; - break; - } - $info['audio']['channels'] = $thisfile_ac3['num_channels']; - - if ($thisfile_ac3_raw_bsi['acmod'] & 0x01) { - // If the lsb of acmod is a 1, center channel is in use and cmixlev follows in the bit stream. - $thisfile_ac3_raw_bsi['cmixlev'] = $this->readHeaderBSI(2); - $thisfile_ac3['center_mix_level'] = self::centerMixLevelLookup($thisfile_ac3_raw_bsi['cmixlev']); - } - - if ($thisfile_ac3_raw_bsi['acmod'] & 0x04) { - // If the msb of acmod is a 1, surround channels are in use and surmixlev follows in the bit stream. - $thisfile_ac3_raw_bsi['surmixlev'] = $this->readHeaderBSI(2); - $thisfile_ac3['surround_mix_level'] = self::surroundMixLevelLookup($thisfile_ac3_raw_bsi['surmixlev']); - } - - if ($thisfile_ac3_raw_bsi['acmod'] == 0x02) { - // When operating in the two channel mode, this 2-bit code indicates whether or not the program has been encoded in Dolby Surround. - $thisfile_ac3_raw_bsi['dsurmod'] = $this->readHeaderBSI(2); - $thisfile_ac3['dolby_surround_mode'] = self::dolbySurroundModeLookup($thisfile_ac3_raw_bsi['dsurmod']); - } - - $thisfile_ac3_raw_bsi['lfeon'] = (bool) $this->readHeaderBSI(1); - $thisfile_ac3['lfe_enabled'] = $thisfile_ac3_raw_bsi['lfeon']; - if ($thisfile_ac3_raw_bsi['lfeon']) { - //$info['audio']['channels']++; - $info['audio']['channels'] .= '.1'; - } - - $thisfile_ac3['channels_enabled'] = self::channelsEnabledLookup($thisfile_ac3_raw_bsi['acmod'], $thisfile_ac3_raw_bsi['lfeon']); - - // This indicates how far the average dialogue level is below digital 100 percent. Valid values are 1-31. - // The value of 0 is reserved. The values of 1 to 31 are interpreted as -1 dB to -31 dB with respect to digital 100 percent. - $thisfile_ac3_raw_bsi['dialnorm'] = $this->readHeaderBSI(5); - $thisfile_ac3['dialogue_normalization'] = '-'.$thisfile_ac3_raw_bsi['dialnorm'].'dB'; - - $thisfile_ac3_raw_bsi['compre_flag'] = (bool) $this->readHeaderBSI(1); - if ($thisfile_ac3_raw_bsi['compre_flag']) { - $thisfile_ac3_raw_bsi['compr'] = $this->readHeaderBSI(8); - $thisfile_ac3['heavy_compression'] = self::heavyCompression($thisfile_ac3_raw_bsi['compr']); - } - - $thisfile_ac3_raw_bsi['langcode_flag'] = (bool) $this->readHeaderBSI(1); - if ($thisfile_ac3_raw_bsi['langcode_flag']) { - $thisfile_ac3_raw_bsi['langcod'] = $this->readHeaderBSI(8); - } - - $thisfile_ac3_raw_bsi['audprodie'] = (bool) $this->readHeaderBSI(1); - if ($thisfile_ac3_raw_bsi['audprodie']) { - $thisfile_ac3_raw_bsi['mixlevel'] = $this->readHeaderBSI(5); - $thisfile_ac3_raw_bsi['roomtyp'] = $this->readHeaderBSI(2); - - $thisfile_ac3['mixing_level'] = (80 + $thisfile_ac3_raw_bsi['mixlevel']).'dB'; - $thisfile_ac3['room_type'] = self::roomTypeLookup($thisfile_ac3_raw_bsi['roomtyp']); - } - - if ($thisfile_ac3_raw_bsi['acmod'] == 0x00) { - // If acmod is 0, then two completely independent program channels (dual mono) - // are encoded into the bit stream, and are referenced as Ch1, Ch2. In this case, - // a number of additional items are present in BSI or audblk to fully describe Ch2. - - // This indicates how far the average dialogue level is below digital 100 percent. Valid values are 1-31. - // The value of 0 is reserved. The values of 1 to 31 are interpreted as -1 dB to -31 dB with respect to digital 100 percent. - $thisfile_ac3_raw_bsi['dialnorm2'] = $this->readHeaderBSI(5); - $thisfile_ac3['dialogue_normalization2'] = '-'.$thisfile_ac3_raw_bsi['dialnorm2'].'dB'; - - $thisfile_ac3_raw_bsi['compre_flag2'] = (bool) $this->readHeaderBSI(1); - if ($thisfile_ac3_raw_bsi['compre_flag2']) { - $thisfile_ac3_raw_bsi['compr2'] = $this->readHeaderBSI(8); - $thisfile_ac3['heavy_compression2'] = self::heavyCompression($thisfile_ac3_raw_bsi['compr2']); - } - - $thisfile_ac3_raw_bsi['langcode_flag2'] = (bool) $this->readHeaderBSI(1); - if ($thisfile_ac3_raw_bsi['langcode_flag2']) { - $thisfile_ac3_raw_bsi['langcod2'] = $this->readHeaderBSI(8); - } - - $thisfile_ac3_raw_bsi['audprodie2'] = (bool) $this->readHeaderBSI(1); - if ($thisfile_ac3_raw_bsi['audprodie2']) { - $thisfile_ac3_raw_bsi['mixlevel2'] = $this->readHeaderBSI(5); - $thisfile_ac3_raw_bsi['roomtyp2'] = $this->readHeaderBSI(2); - - $thisfile_ac3['mixing_level2'] = (80 + $thisfile_ac3_raw_bsi['mixlevel2']).'dB'; - $thisfile_ac3['room_type2'] = self::roomTypeLookup($thisfile_ac3_raw_bsi['roomtyp2']); - } - - } - - $thisfile_ac3_raw_bsi['copyright'] = (bool) $this->readHeaderBSI(1); - - $thisfile_ac3_raw_bsi['original'] = (bool) $this->readHeaderBSI(1); - - $thisfile_ac3_raw_bsi['timecode1_flag'] = (bool) $this->readHeaderBSI(1); - if ($thisfile_ac3_raw_bsi['timecode1_flag']) { - $thisfile_ac3_raw_bsi['timecode1'] = $this->readHeaderBSI(14); - } - - $thisfile_ac3_raw_bsi['timecode2_flag'] = (bool) $this->readHeaderBSI(1); - if ($thisfile_ac3_raw_bsi['timecode2_flag']) { - $thisfile_ac3_raw_bsi['timecode2'] = $this->readHeaderBSI(14); - } - - $thisfile_ac3_raw_bsi['addbsi_flag'] = (bool) $this->readHeaderBSI(1); - if ($thisfile_ac3_raw_bsi['addbsi_flag']) { - $thisfile_ac3_raw_bsi['addbsi_length'] = $this->readHeaderBSI(6); - - $this->AC3header['bsi'] .= getid3_lib::BigEndian2Bin($this->fread($thisfile_ac3_raw_bsi['addbsi_length'])); - - $thisfile_ac3_raw_bsi['addbsi_data'] = substr($this->AC3header['bsi'], $this->BSIoffset, $thisfile_ac3_raw_bsi['addbsi_length'] * 8); - $this->BSIoffset += $thisfile_ac3_raw_bsi['addbsi_length'] * 8; - } - - return true; - } - - private function readHeaderBSI($length) { - $data = substr($this->AC3header['bsi'], $this->BSIoffset, $length); - $this->BSIoffset += $length; - - return bindec($data); - } - - public static function sampleRateCodeLookup($fscod) { - static $sampleRateCodeLookup = array( - 0 => 48000, - 1 => 44100, - 2 => 32000, - 3 => 'reserved' // If the reserved code is indicated, the decoder should not attempt to decode audio and should mute. - ); - return (isset($sampleRateCodeLookup[$fscod]) ? $sampleRateCodeLookup[$fscod] : false); - } - - public static function serviceTypeLookup($bsmod, $acmod) { - static $serviceTypeLookup = array(); - if (empty($serviceTypeLookup)) { - for ($i = 0; $i <= 7; $i++) { - $serviceTypeLookup[0][$i] = 'main audio service: complete main (CM)'; - $serviceTypeLookup[1][$i] = 'main audio service: music and effects (ME)'; - $serviceTypeLookup[2][$i] = 'associated service: visually impaired (VI)'; - $serviceTypeLookup[3][$i] = 'associated service: hearing impaired (HI)'; - $serviceTypeLookup[4][$i] = 'associated service: dialogue (D)'; - $serviceTypeLookup[5][$i] = 'associated service: commentary (C)'; - $serviceTypeLookup[6][$i] = 'associated service: emergency (E)'; - } - - $serviceTypeLookup[7][1] = 'associated service: voice over (VO)'; - for ($i = 2; $i <= 7; $i++) { - $serviceTypeLookup[7][$i] = 'main audio service: karaoke'; - } - } - return (isset($serviceTypeLookup[$bsmod][$acmod]) ? $serviceTypeLookup[$bsmod][$acmod] : false); - } - - public static function audioCodingModeLookup($acmod) { - // array(channel configuration, # channels (not incl LFE), channel order) - static $audioCodingModeLookup = array ( - 0 => array('channel_config'=>'1+1', 'num_channels'=>2, 'channel_order'=>'Ch1,Ch2'), - 1 => array('channel_config'=>'1/0', 'num_channels'=>1, 'channel_order'=>'C'), - 2 => array('channel_config'=>'2/0', 'num_channels'=>2, 'channel_order'=>'L,R'), - 3 => array('channel_config'=>'3/0', 'num_channels'=>3, 'channel_order'=>'L,C,R'), - 4 => array('channel_config'=>'2/1', 'num_channels'=>3, 'channel_order'=>'L,R,S'), - 5 => array('channel_config'=>'3/1', 'num_channels'=>4, 'channel_order'=>'L,C,R,S'), - 6 => array('channel_config'=>'2/2', 'num_channels'=>4, 'channel_order'=>'L,R,SL,SR'), - 7 => array('channel_config'=>'3/2', 'num_channels'=>5, 'channel_order'=>'L,C,R,SL,SR'), - ); - return (isset($audioCodingModeLookup[$acmod]) ? $audioCodingModeLookup[$acmod] : false); - } - - public static function centerMixLevelLookup($cmixlev) { - static $centerMixLevelLookup; - if (empty($centerMixLevelLookup)) { - $centerMixLevelLookup = array( - 0 => pow(2, -3.0 / 6), // 0.707 (-3.0 dB) - 1 => pow(2, -4.5 / 6), // 0.595 (-4.5 dB) - 2 => pow(2, -6.0 / 6), // 0.500 (-6.0 dB) - 3 => 'reserved' - ); - } - return (isset($centerMixLevelLookup[$cmixlev]) ? $centerMixLevelLookup[$cmixlev] : false); - } - - public static function surroundMixLevelLookup($surmixlev) { - static $surroundMixLevelLookup; - if (empty($surroundMixLevelLookup)) { - $surroundMixLevelLookup = array( - 0 => pow(2, -3.0 / 6), - 1 => pow(2, -6.0 / 6), - 2 => 0, - 3 => 'reserved' - ); - } - return (isset($surroundMixLevelLookup[$surmixlev]) ? $surroundMixLevelLookup[$surmixlev] : false); - } - - public static function dolbySurroundModeLookup($dsurmod) { - static $dolbySurroundModeLookup = array( - 0 => 'not indicated', - 1 => 'Not Dolby Surround encoded', - 2 => 'Dolby Surround encoded', - 3 => 'reserved' - ); - return (isset($dolbySurroundModeLookup[$dsurmod]) ? $dolbySurroundModeLookup[$dsurmod] : false); - } - - public static function channelsEnabledLookup($acmod, $lfeon) { - $lookup = array( - 'ch1'=>(bool) ($acmod == 0), - 'ch2'=>(bool) ($acmod == 0), - 'left'=>(bool) ($acmod > 1), - 'right'=>(bool) ($acmod > 1), - 'center'=>(bool) ($acmod & 0x01), - 'surround_mono'=>false, - 'surround_left'=>false, - 'surround_right'=>false, - 'lfe'=>$lfeon); - switch ($acmod) { - case 4: - case 5: - $lookup['surround_mono'] = true; - break; - case 6: - case 7: - $lookup['surround_left'] = true; - $lookup['surround_right'] = true; - break; - } - return $lookup; - } - - public static function heavyCompression($compre) { - // The first four bits indicate gain changes in 6.02dB increments which can be - // implemented with an arithmetic shift operation. The following four bits - // indicate linear gain changes, and require a 5-bit multiply. - // We will represent the two 4-bit fields of compr as follows: - // X0 X1 X2 X3 . Y4 Y5 Y6 Y7 - // The meaning of the X values is most simply described by considering X to represent a 4-bit - // signed integer with values from -8 to +7. The gain indicated by X is then (X + 1) * 6.02 dB. The - // following table shows this in detail. - - // Meaning of 4 msb of compr - // 7 +48.16 dB - // 6 +42.14 dB - // 5 +36.12 dB - // 4 +30.10 dB - // 3 +24.08 dB - // 2 +18.06 dB - // 1 +12.04 dB - // 0 +6.02 dB - // -1 0 dB - // -2 -6.02 dB - // -3 -12.04 dB - // -4 -18.06 dB - // -5 -24.08 dB - // -6 -30.10 dB - // -7 -36.12 dB - // -8 -42.14 dB - - $fourbit = str_pad(decbin(($compre & 0xF0) >> 4), 4, '0', STR_PAD_LEFT); - if ($fourbit{0} == '1') { - $log_gain = -8 + bindec(substr($fourbit, 1)); - } else { - $log_gain = bindec(substr($fourbit, 1)); - } - $log_gain = ($log_gain + 1) * getid3_lib::RGADamplitude2dB(2); - - // The value of Y is a linear representation of a gain change of up to -6 dB. Y is considered to - // be an unsigned fractional integer, with a leading value of 1, or: 0.1 Y4 Y5 Y6 Y7 (base 2). Y can - // represent values between 0.111112 (or 31/32) and 0.100002 (or 1/2). Thus, Y can represent gain - // changes from -0.28 dB to -6.02 dB. - - $lin_gain = (16 + ($compre & 0x0F)) / 32; - - // The combination of X and Y values allows compr to indicate gain changes from - // 48.16 - 0.28 = +47.89 dB, to - // -42.14 - 6.02 = -48.16 dB. - - return $log_gain - $lin_gain; - } - - public static function roomTypeLookup($roomtyp) { - static $roomTypeLookup = array( - 0 => 'not indicated', - 1 => 'large room, X curve monitor', - 2 => 'small room, flat monitor', - 3 => 'reserved' - ); - return (isset($roomTypeLookup[$roomtyp]) ? $roomTypeLookup[$roomtyp] : false); - } - - public static function frameSizeLookup($frmsizecod, $fscod) { - $padding = (bool) ($frmsizecod % 2); - $framesizeid = floor($frmsizecod / 2); - - static $frameSizeLookup = array(); - if (empty($frameSizeLookup)) { - $frameSizeLookup = array ( - 0 => array(128, 138, 192), - 1 => array(40, 160, 174, 240), - 2 => array(48, 192, 208, 288), - 3 => array(56, 224, 242, 336), - 4 => array(64, 256, 278, 384), - 5 => array(80, 320, 348, 480), - 6 => array(96, 384, 416, 576), - 7 => array(112, 448, 486, 672), - 8 => array(128, 512, 556, 768), - 9 => array(160, 640, 696, 960), - 10 => array(192, 768, 834, 1152), - 11 => array(224, 896, 974, 1344), - 12 => array(256, 1024, 1114, 1536), - 13 => array(320, 1280, 1392, 1920), - 14 => array(384, 1536, 1670, 2304), - 15 => array(448, 1792, 1950, 2688), - 16 => array(512, 2048, 2228, 3072), - 17 => array(576, 2304, 2506, 3456), - 18 => array(640, 2560, 2786, 3840) - ); - } - if (($fscod == 1) && $padding) { - // frame lengths are padded by 1 word (16 bits) at 44100 - $frameSizeLookup[$frmsizecod] += 2; - } - return (isset($frameSizeLookup[$framesizeid][$fscod]) ? $frameSizeLookup[$framesizeid][$fscod] : false); - } - - public static function bitrateLookup($frmsizecod) { - $framesizeid = floor($frmsizecod / 2); - - static $bitrateLookup = array( - 0 => 32000, - 1 => 40000, - 2 => 48000, - 3 => 56000, - 4 => 64000, - 5 => 80000, - 6 => 96000, - 7 => 112000, - 8 => 128000, - 9 => 160000, - 10 => 192000, - 11 => 224000, - 12 => 256000, - 13 => 320000, - 14 => 384000, - 15 => 448000, - 16 => 512000, - 17 => 576000, - 18 => 640000 - ); - return (isset($bitrateLookup[$framesizeid]) ? $bitrateLookup[$framesizeid] : false); - } - - -} diff --git a/src/Classes/Vendor/getid3/module.audio.au.php b/src/Classes/Vendor/getid3/module.audio.au.php deleted file mode 100755 index 5951684ae..000000000 --- a/src/Classes/Vendor/getid3/module.audio.au.php +++ /dev/null @@ -1,162 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.au.php // -// module for analyzing AU files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_au extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $AUheader = fread($this->getid3->fp, 8); - - $magic = '.snd'; - if (substr($AUheader, 0, 4) != $magic) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" (".snd") at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($AUheader, 0, 4)).'"'; - return false; - } - - // shortcut - $info['au'] = array(); - $thisfile_au = &$info['au']; - - $info['fileformat'] = 'au'; - $info['audio']['dataformat'] = 'au'; - $info['audio']['bitrate_mode'] = 'cbr'; - $thisfile_au['encoding'] = 'ISO-8859-1'; - - $thisfile_au['header_length'] = getid3_lib::BigEndian2Int(substr($AUheader, 4, 4)); - $AUheader .= fread($this->getid3->fp, $thisfile_au['header_length'] - 8); - $info['avdataoffset'] += $thisfile_au['header_length']; - - $thisfile_au['data_size'] = getid3_lib::BigEndian2Int(substr($AUheader, 8, 4)); - $thisfile_au['data_format_id'] = getid3_lib::BigEndian2Int(substr($AUheader, 12, 4)); - $thisfile_au['sample_rate'] = getid3_lib::BigEndian2Int(substr($AUheader, 16, 4)); - $thisfile_au['channels'] = getid3_lib::BigEndian2Int(substr($AUheader, 20, 4)); - $thisfile_au['comments']['comment'][] = trim(substr($AUheader, 24)); - - $thisfile_au['data_format'] = $this->AUdataFormatNameLookup($thisfile_au['data_format_id']); - $thisfile_au['used_bits_per_sample'] = $this->AUdataFormatUsedBitsPerSampleLookup($thisfile_au['data_format_id']); - if ($thisfile_au['bits_per_sample'] = $this->AUdataFormatBitsPerSampleLookup($thisfile_au['data_format_id'])) { - $info['audio']['bits_per_sample'] = $thisfile_au['bits_per_sample']; - } else { - unset($thisfile_au['bits_per_sample']); - } - - $info['audio']['sample_rate'] = $thisfile_au['sample_rate']; - $info['audio']['channels'] = $thisfile_au['channels']; - - if (($info['avdataoffset'] + $thisfile_au['data_size']) > $info['avdataend']) { - $info['warning'][] = 'Possible truncated file - expecting "'.$thisfile_au['data_size'].'" bytes of audio data, only found '.($info['avdataend'] - $info['avdataoffset']).' bytes"'; - } - - $info['playtime_seconds'] = $thisfile_au['data_size'] / ($thisfile_au['sample_rate'] * $thisfile_au['channels'] * ($thisfile_au['used_bits_per_sample'] / 8)); - $info['audio']['bitrate'] = ($thisfile_au['data_size'] * 8) / $info['playtime_seconds']; - - return true; - } - - public function AUdataFormatNameLookup($id) { - static $AUdataFormatNameLookup = array( - 0 => 'unspecified format', - 1 => '8-bit mu-law', - 2 => '8-bit linear', - 3 => '16-bit linear', - 4 => '24-bit linear', - 5 => '32-bit linear', - 6 => 'floating-point', - 7 => 'double-precision float', - 8 => 'fragmented sampled data', - 9 => 'SUN_FORMAT_NESTED', - 10 => 'DSP program', - 11 => '8-bit fixed-point', - 12 => '16-bit fixed-point', - 13 => '24-bit fixed-point', - 14 => '32-bit fixed-point', - - 16 => 'non-audio display data', - 17 => 'SND_FORMAT_MULAW_SQUELCH', - 18 => '16-bit linear with emphasis', - 19 => '16-bit linear with compression', - 20 => '16-bit linear with emphasis + compression', - 21 => 'Music Kit DSP commands', - 22 => 'SND_FORMAT_DSP_COMMANDS_SAMPLES', - 23 => 'CCITT g.721 4-bit ADPCM', - 24 => 'CCITT g.722 ADPCM', - 25 => 'CCITT g.723 3-bit ADPCM', - 26 => 'CCITT g.723 5-bit ADPCM', - 27 => 'A-Law 8-bit' - ); - return (isset($AUdataFormatNameLookup[$id]) ? $AUdataFormatNameLookup[$id] : false); - } - - public function AUdataFormatBitsPerSampleLookup($id) { - static $AUdataFormatBitsPerSampleLookup = array( - 1 => 8, - 2 => 8, - 3 => 16, - 4 => 24, - 5 => 32, - 6 => 32, - 7 => 64, - - 11 => 8, - 12 => 16, - 13 => 24, - 14 => 32, - - 18 => 16, - 19 => 16, - 20 => 16, - - 23 => 16, - - 25 => 16, - 26 => 16, - 27 => 8 - ); - return (isset($AUdataFormatBitsPerSampleLookup[$id]) ? $AUdataFormatBitsPerSampleLookup[$id] : false); - } - - public function AUdataFormatUsedBitsPerSampleLookup($id) { - static $AUdataFormatUsedBitsPerSampleLookup = array( - 1 => 8, - 2 => 8, - 3 => 16, - 4 => 24, - 5 => 32, - 6 => 32, - 7 => 64, - - 11 => 8, - 12 => 16, - 13 => 24, - 14 => 32, - - 18 => 16, - 19 => 16, - 20 => 16, - - 23 => 4, - - 25 => 3, - 26 => 5, - 27 => 8, - ); - return (isset($AUdataFormatUsedBitsPerSampleLookup[$id]) ? $AUdataFormatUsedBitsPerSampleLookup[$id] : false); - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.avr.php b/src/Classes/Vendor/getid3/module.audio.avr.php deleted file mode 100755 index 77107eaea..000000000 --- a/src/Classes/Vendor/getid3/module.audio.avr.php +++ /dev/null @@ -1,124 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.avr.php // -// module for analyzing AVR Audio files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_avr extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - // http://cui.unige.ch/OSG/info/AudioFormats/ap11.html - // http://www.btinternet.com/~AnthonyJ/Atari/programming/avr_format.html - // offset type length name comments - // --------------------------------------------------------------------- - // 0 char 4 ID format ID == "2BIT" - // 4 char 8 name sample name (unused space filled with 0) - // 12 short 1 mono/stereo 0=mono, -1 (0xFFFF)=stereo - // With stereo, samples are alternated, - // the first voice is the left : - // (LRLRLRLRLRLRLRLRLR...) - // 14 short 1 resolution 8, 12 or 16 (bits) - // 16 short 1 signed or not 0=unsigned, -1 (0xFFFF)=signed - // 18 short 1 loop or not 0=no loop, -1 (0xFFFF)=loop on - // 20 short 1 MIDI note 0xFFnn, where 0 <= nn <= 127 - // 0xFFFF means "no MIDI note defined" - // 22 byte 1 Replay speed Frequence in the Replay software - // 0=5.485 Khz, 1=8.084 Khz, 2=10.971 Khz, - // 3=16.168 Khz, 4=21.942 Khz, 5=32.336 Khz - // 6=43.885 Khz, 7=47.261 Khz - // -1 (0xFF)=no defined Frequence - // 23 byte 3 sample rate in Hertz - // 26 long 1 size in bytes (2 * bytes in stereo) - // 30 long 1 loop begin 0 for no loop - // 34 long 1 loop size equal to 'size' for no loop - // 38 short 2 Reserved, MIDI keyboard split */ - // 40 short 2 Reserved, sample compression */ - // 42 short 2 Reserved */ - // 44 char 20; Additional filename space, used if (name[7] != 0) - // 64 byte 64 user data - // 128 bytes ? sample data (12 bits samples are coded on 16 bits: - // 0000 xxxx xxxx xxxx) - // --------------------------------------------------------------------- - - // Note that all values are in motorola (big-endian) format, and that long is - // assumed to be 4 bytes, and short 2 bytes. - // When reading the samples, you should handle both signed and unsigned data, - // and be prepared to convert 16->8 bit, or mono->stereo if needed. To convert - // 8-bit data between signed/unsigned just add 127 to the sample values. - // Simularly for 16-bit data you should add 32769 - - $info['fileformat'] = 'avr'; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $AVRheader = fread($this->getid3->fp, 128); - - $info['avr']['raw']['magic'] = substr($AVRheader, 0, 4); - $magic = '2BIT'; - if ($info['avr']['raw']['magic'] != $magic) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($info['avr']['raw']['magic']).'"'; - unset($info['fileformat']); - unset($info['avr']); - return false; - } - $info['avdataoffset'] += 128; - - $info['avr']['sample_name'] = rtrim(substr($AVRheader, 4, 8)); - $info['avr']['raw']['mono'] = getid3_lib::BigEndian2Int(substr($AVRheader, 12, 2)); - $info['avr']['bits_per_sample'] = getid3_lib::BigEndian2Int(substr($AVRheader, 14, 2)); - $info['avr']['raw']['signed'] = getid3_lib::BigEndian2Int(substr($AVRheader, 16, 2)); - $info['avr']['raw']['loop'] = getid3_lib::BigEndian2Int(substr($AVRheader, 18, 2)); - $info['avr']['raw']['midi'] = getid3_lib::BigEndian2Int(substr($AVRheader, 20, 2)); - $info['avr']['raw']['replay_freq'] = getid3_lib::BigEndian2Int(substr($AVRheader, 22, 1)); - $info['avr']['sample_rate'] = getid3_lib::BigEndian2Int(substr($AVRheader, 23, 3)); - $info['avr']['sample_length'] = getid3_lib::BigEndian2Int(substr($AVRheader, 26, 4)); - $info['avr']['loop_start'] = getid3_lib::BigEndian2Int(substr($AVRheader, 30, 4)); - $info['avr']['loop_end'] = getid3_lib::BigEndian2Int(substr($AVRheader, 34, 4)); - $info['avr']['midi_split'] = getid3_lib::BigEndian2Int(substr($AVRheader, 38, 2)); - $info['avr']['sample_compression'] = getid3_lib::BigEndian2Int(substr($AVRheader, 40, 2)); - $info['avr']['reserved'] = getid3_lib::BigEndian2Int(substr($AVRheader, 42, 2)); - $info['avr']['sample_name_extra'] = rtrim(substr($AVRheader, 44, 20)); - $info['avr']['comment'] = rtrim(substr($AVRheader, 64, 64)); - - $info['avr']['flags']['stereo'] = (($info['avr']['raw']['mono'] == 0) ? false : true); - $info['avr']['flags']['signed'] = (($info['avr']['raw']['signed'] == 0) ? false : true); - $info['avr']['flags']['loop'] = (($info['avr']['raw']['loop'] == 0) ? false : true); - - $info['avr']['midi_notes'] = array(); - if (($info['avr']['raw']['midi'] & 0xFF00) != 0xFF00) { - $info['avr']['midi_notes'][] = ($info['avr']['raw']['midi'] & 0xFF00) >> 8; - } - if (($info['avr']['raw']['midi'] & 0x00FF) != 0x00FF) { - $info['avr']['midi_notes'][] = ($info['avr']['raw']['midi'] & 0x00FF); - } - - if (($info['avdataend'] - $info['avdataoffset']) != ($info['avr']['sample_length'] * (($info['avr']['bits_per_sample'] == 8) ? 1 : 2))) { - $info['warning'][] = 'Probable truncated file: expecting '.($info['avr']['sample_length'] * (($info['avr']['bits_per_sample'] == 8) ? 1 : 2)).' bytes of audio data, found '.($info['avdataend'] - $info['avdataoffset']); - } - - $info['audio']['dataformat'] = 'avr'; - $info['audio']['lossless'] = true; - $info['audio']['bitrate_mode'] = 'cbr'; - $info['audio']['bits_per_sample'] = $info['avr']['bits_per_sample']; - $info['audio']['sample_rate'] = $info['avr']['sample_rate']; - $info['audio']['channels'] = ($info['avr']['flags']['stereo'] ? 2 : 1); - $info['playtime_seconds'] = ($info['avr']['sample_length'] / $info['audio']['channels']) / $info['avr']['sample_rate']; - $info['audio']['bitrate'] = ($info['avr']['sample_length'] * (($info['avr']['bits_per_sample'] == 8) ? 8 : 16)) / $info['playtime_seconds']; - - - return true; - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.bonk.php b/src/Classes/Vendor/getid3/module.audio.bonk.php deleted file mode 100755 index b16eff7c4..000000000 --- a/src/Classes/Vendor/getid3/module.audio.bonk.php +++ /dev/null @@ -1,227 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.la.php // -// module for analyzing BONK audio files // -// dependencies: module.tag.id3v2.php (optional) // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_bonk extends getid3_handler -{ - public function Analyze() { - $info = &$this->getid3->info; - - // shortcut - $info['bonk'] = array(); - $thisfile_bonk = &$info['bonk']; - - $thisfile_bonk['dataoffset'] = $info['avdataoffset']; - $thisfile_bonk['dataend'] = $info['avdataend']; - - if (!getid3_lib::intValueSupported($thisfile_bonk['dataend'])) { - - $info['warning'][] = 'Unable to parse BONK file from end (v0.6+ preferred method) because PHP filesystem functions only support up to '.round(PHP_INT_MAX / 1073741824).'GB'; - - } else { - - // scan-from-end method, for v0.6 and higher - fseek($this->getid3->fp, $thisfile_bonk['dataend'] - 8, SEEK_SET); - $PossibleBonkTag = fread($this->getid3->fp, 8); - while ($this->BonkIsValidTagName(substr($PossibleBonkTag, 4, 4), true)) { - $BonkTagSize = getid3_lib::LittleEndian2Int(substr($PossibleBonkTag, 0, 4)); - fseek($this->getid3->fp, 0 - $BonkTagSize, SEEK_CUR); - $BonkTagOffset = ftell($this->getid3->fp); - $TagHeaderTest = fread($this->getid3->fp, 5); - if (($TagHeaderTest{0} != "\x00") || (substr($PossibleBonkTag, 4, 4) != strtolower(substr($PossibleBonkTag, 4, 4)))) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes("\x00".strtoupper(substr($PossibleBonkTag, 4, 4))).'" at offset '.$BonkTagOffset.', found "'.getid3_lib::PrintHexBytes($TagHeaderTest).'"'; - return false; - } - $BonkTagName = substr($TagHeaderTest, 1, 4); - - $thisfile_bonk[$BonkTagName]['size'] = $BonkTagSize; - $thisfile_bonk[$BonkTagName]['offset'] = $BonkTagOffset; - $this->HandleBonkTags($BonkTagName); - $NextTagEndOffset = $BonkTagOffset - 8; - if ($NextTagEndOffset < $thisfile_bonk['dataoffset']) { - if (empty($info['audio']['encoder'])) { - $info['audio']['encoder'] = 'Extended BONK v0.9+'; - } - return true; - } - fseek($this->getid3->fp, $NextTagEndOffset, SEEK_SET); - $PossibleBonkTag = fread($this->getid3->fp, 8); - } - - } - - // seek-from-beginning method for v0.4 and v0.5 - if (empty($thisfile_bonk['BONK'])) { - fseek($this->getid3->fp, $thisfile_bonk['dataoffset'], SEEK_SET); - do { - $TagHeaderTest = fread($this->getid3->fp, 5); - switch ($TagHeaderTest) { - case "\x00".'BONK': - if (empty($info['audio']['encoder'])) { - $info['audio']['encoder'] = 'BONK v0.4'; - } - break; - - case "\x00".'INFO': - $info['audio']['encoder'] = 'Extended BONK v0.5'; - break; - - default: - break 2; - } - $BonkTagName = substr($TagHeaderTest, 1, 4); - $thisfile_bonk[$BonkTagName]['size'] = $thisfile_bonk['dataend'] - $thisfile_bonk['dataoffset']; - $thisfile_bonk[$BonkTagName]['offset'] = $thisfile_bonk['dataoffset']; - $this->HandleBonkTags($BonkTagName); - - } while (true); - } - - // parse META block for v0.6 - v0.8 - if (empty($thisfile_bonk['INFO']) && isset($thisfile_bonk['META']['tags']['info'])) { - fseek($this->getid3->fp, $thisfile_bonk['META']['tags']['info'], SEEK_SET); - $TagHeaderTest = fread($this->getid3->fp, 5); - if ($TagHeaderTest == "\x00".'INFO') { - $info['audio']['encoder'] = 'Extended BONK v0.6 - v0.8'; - - $BonkTagName = substr($TagHeaderTest, 1, 4); - $thisfile_bonk[$BonkTagName]['size'] = $thisfile_bonk['dataend'] - $thisfile_bonk['dataoffset']; - $thisfile_bonk[$BonkTagName]['offset'] = $thisfile_bonk['dataoffset']; - $this->HandleBonkTags($BonkTagName); - } - } - - if (empty($info['audio']['encoder'])) { - $info['audio']['encoder'] = 'Extended BONK v0.9+'; - } - if (empty($thisfile_bonk['BONK'])) { - unset($info['bonk']); - } - return true; - - } - - public function HandleBonkTags($BonkTagName) { - $info = &$this->getid3->info; - switch ($BonkTagName) { - case 'BONK': - // shortcut - $thisfile_bonk_BONK = &$info['bonk']['BONK']; - - $BonkData = "\x00".'BONK'.fread($this->getid3->fp, 17); - $thisfile_bonk_BONK['version'] = getid3_lib::LittleEndian2Int(substr($BonkData, 5, 1)); - $thisfile_bonk_BONK['number_samples'] = getid3_lib::LittleEndian2Int(substr($BonkData, 6, 4)); - $thisfile_bonk_BONK['sample_rate'] = getid3_lib::LittleEndian2Int(substr($BonkData, 10, 4)); - - $thisfile_bonk_BONK['channels'] = getid3_lib::LittleEndian2Int(substr($BonkData, 14, 1)); - $thisfile_bonk_BONK['lossless'] = (bool) getid3_lib::LittleEndian2Int(substr($BonkData, 15, 1)); - $thisfile_bonk_BONK['joint_stereo'] = (bool) getid3_lib::LittleEndian2Int(substr($BonkData, 16, 1)); - $thisfile_bonk_BONK['number_taps'] = getid3_lib::LittleEndian2Int(substr($BonkData, 17, 2)); - $thisfile_bonk_BONK['downsampling_ratio'] = getid3_lib::LittleEndian2Int(substr($BonkData, 19, 1)); - $thisfile_bonk_BONK['samples_per_packet'] = getid3_lib::LittleEndian2Int(substr($BonkData, 20, 2)); - - $info['avdataoffset'] = $thisfile_bonk_BONK['offset'] + 5 + 17; - $info['avdataend'] = $thisfile_bonk_BONK['offset'] + $thisfile_bonk_BONK['size']; - - $info['fileformat'] = 'bonk'; - $info['audio']['dataformat'] = 'bonk'; - $info['audio']['bitrate_mode'] = 'vbr'; // assumed - $info['audio']['channels'] = $thisfile_bonk_BONK['channels']; - $info['audio']['sample_rate'] = $thisfile_bonk_BONK['sample_rate']; - $info['audio']['channelmode'] = ($thisfile_bonk_BONK['joint_stereo'] ? 'joint stereo' : 'stereo'); - $info['audio']['lossless'] = $thisfile_bonk_BONK['lossless']; - $info['audio']['codec'] = 'bonk'; - - $info['playtime_seconds'] = $thisfile_bonk_BONK['number_samples'] / ($thisfile_bonk_BONK['sample_rate'] * $thisfile_bonk_BONK['channels']); - if ($info['playtime_seconds'] > 0) { - $info['audio']['bitrate'] = (($info['bonk']['dataend'] - $info['bonk']['dataoffset']) * 8) / $info['playtime_seconds']; - } - break; - - case 'INFO': - // shortcut - $thisfile_bonk_INFO = &$info['bonk']['INFO']; - - $thisfile_bonk_INFO['version'] = getid3_lib::LittleEndian2Int(fread($this->getid3->fp, 1)); - $thisfile_bonk_INFO['entries_count'] = 0; - $NextInfoDataPair = fread($this->getid3->fp, 5); - if (!$this->BonkIsValidTagName(substr($NextInfoDataPair, 1, 4))) { - while (!feof($this->getid3->fp)) { - //$CurrentSeekInfo['offset'] = getid3_lib::LittleEndian2Int(substr($NextInfoDataPair, 0, 4)); - //$CurrentSeekInfo['nextbit'] = getid3_lib::LittleEndian2Int(substr($NextInfoDataPair, 4, 1)); - //$thisfile_bonk_INFO[] = $CurrentSeekInfo; - - $NextInfoDataPair = fread($this->getid3->fp, 5); - if ($this->BonkIsValidTagName(substr($NextInfoDataPair, 1, 4))) { - fseek($this->getid3->fp, -5, SEEK_CUR); - break; - } - $thisfile_bonk_INFO['entries_count']++; - } - } - break; - - case 'META': - $BonkData = "\x00".'META'.fread($this->getid3->fp, $info['bonk']['META']['size'] - 5); - $info['bonk']['META']['version'] = getid3_lib::LittleEndian2Int(substr($BonkData, 5, 1)); - - $MetaTagEntries = floor(((strlen($BonkData) - 8) - 6) / 8); // BonkData - xxxxmeta - ØMETA - $offset = 6; - for ($i = 0; $i < $MetaTagEntries; $i++) { - $MetaEntryTagName = substr($BonkData, $offset, 4); - $offset += 4; - $MetaEntryTagOffset = getid3_lib::LittleEndian2Int(substr($BonkData, $offset, 4)); - $offset += 4; - $info['bonk']['META']['tags'][$MetaEntryTagName] = $MetaEntryTagOffset; - } - break; - - case ' ID3': - $info['audio']['encoder'] = 'Extended BONK v0.9+'; - - // ID3v2 checking is optional - if (class_exists('getid3_id3v2')) { - $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); - $getid3_id3v2 = new getid3_id3v2($getid3_temp); - $getid3_id3v2->StartingOffset = $info['bonk'][' ID3']['offset'] + 2; - $info['bonk'][' ID3']['valid'] = $getid3_id3v2->Analyze(); - if ($info['bonk'][' ID3']['valid']) { - $info['id3v2'] = $getid3_temp->info['id3v2']; - } - unset($getid3_temp, $getid3_id3v2); - } - break; - - default: - $info['warning'][] = 'Unexpected Bonk tag "'.$BonkTagName.'" at offset '.$info['bonk'][$BonkTagName]['offset']; - break; - - } - } - - public static function BonkIsValidTagName($PossibleBonkTag, $ignorecase=false) { - static $BonkIsValidTagName = array('BONK', 'INFO', ' ID3', 'META'); - foreach ($BonkIsValidTagName as $validtagname) { - if ($validtagname == $PossibleBonkTag) { - return true; - } elseif ($ignorecase && (strtolower($validtagname) == strtolower($PossibleBonkTag))) { - return true; - } - } - return false; - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.dss.php b/src/Classes/Vendor/getid3/module.audio.dss.php deleted file mode 100755 index 4719d53b4..000000000 --- a/src/Classes/Vendor/getid3/module.audio.dss.php +++ /dev/null @@ -1,77 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.dss.php // -// module for analyzing Digital Speech Standard (DSS) files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_dss extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $DSSheader = fread($this->getid3->fp, 1256); - - if (!preg_match('#^(\x02|\x03)ds[s2]#', $DSSheader)) { - $info['error'][] = 'Expecting "[02-03] 64 73 [73|32]" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($DSSheader, 0, 4)).'"'; - return false; - } - - // some structure information taken from http://cpansearch.perl.org/src/RGIBSON/Audio-DSS-0.02/lib/Audio/DSS.pm - $info['encoding'] = 'ISO-8859-1'; // not certain, but assumed - $info['dss'] = array(); - - $info['fileformat'] = 'dss'; - $info['mime_type'] = 'audio/x-'.substr($DSSheader, 1, 3); // "audio/x-dss" or "audio/x-ds2" - $info['audio']['dataformat'] = substr($DSSheader, 1, 3); // "dss" or "ds2" - $info['audio']['bitrate_mode'] = 'cbr'; - - $info['dss']['version'] = ord(substr($DSSheader, 0, 1)); - $info['dss']['hardware'] = trim(substr($DSSheader, 12, 16)); // identification string for hardware used to create the file, e.g. "DPM 9600", "DS2400" - $info['dss']['unknown1'] = getid3_lib::LittleEndian2Int(substr($DSSheader, 28, 4)); - // 32-37 = "FE FF FE FF F7 FF" in all the sample files I've seen - $info['dss']['date_create'] = $this->DSSdateStringToUnixDate(substr($DSSheader, 38, 12)); - $info['dss']['date_complete'] = $this->DSSdateStringToUnixDate(substr($DSSheader, 50, 12)); - $info['dss']['playtime_sec'] = intval((substr($DSSheader, 62, 2) * 3600) + (substr($DSSheader, 64, 2) * 60) + substr($DSSheader, 66, 2)); // approximate file playtime in HHMMSS - $info['dss']['playtime_ms'] = getid3_lib::LittleEndian2Int(substr($DSSheader, 512, 4)); // exact file playtime in milliseconds. Has also been observed at offset 530 in one sample file, with something else (unknown) at offset 512 - $info['dss']['priority'] = ord(substr($DSSheader, 793, 1)); - $info['dss']['comments'] = trim(substr($DSSheader, 798, 100)); - - //$info['audio']['bits_per_sample'] = ?; - //$info['audio']['sample_rate'] = ?; - $info['audio']['channels'] = 1; - - $info['playtime_seconds'] = $info['dss']['playtime_ms'] / 1000; - if (floor($info['dss']['playtime_ms'] / 1000) != $info['dss']['playtime_sec']) { - // *should* just be playtime_ms / 1000 but at least one sample file has playtime_ms at offset 530 instead of offset 512, so safety check - $info['playtime_seconds'] = $info['dss']['playtime_sec']; - $this->getid3->warning('playtime_ms ('.number_format($info['dss']['playtime_ms'] / 1000, 3).') does not match playtime_sec ('.number_format($info['dss']['playtime_sec']).') - using playtime_sec value'); - } - $info['audio']['bitrate'] = ($info['filesize'] * 8) / $info['playtime_seconds']; - - return true; - } - - public function DSSdateStringToUnixDate($datestring) { - $y = substr($datestring, 0, 2); - $m = substr($datestring, 2, 2); - $d = substr($datestring, 4, 2); - $h = substr($datestring, 6, 2); - $i = substr($datestring, 8, 2); - $s = substr($datestring, 10, 2); - $y += (($y < 95) ? 2000 : 1900); - return mktime($h, $i, $s, $m, $d, $y); - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.dts.php b/src/Classes/Vendor/getid3/module.audio.dts.php deleted file mode 100755 index 79982cccf..000000000 --- a/src/Classes/Vendor/getid3/module.audio.dts.php +++ /dev/null @@ -1,290 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.dts.php // -// module for analyzing DTS Audio files // -// dependencies: NONE // -// // -///////////////////////////////////////////////////////////////// - - -/** -* @tutorial http://wiki.multimedia.cx/index.php?title=DTS -*/ -class getid3_dts extends getid3_handler -{ - /** - * Default DTS syncword used in native .cpt or .dts formats - */ - const syncword = "\x7F\xFE\x80\x01"; - - private $readBinDataOffset = 0; - - /** - * Possible syncwords indicating bitstream encoding - */ - public static $syncwords = array( - 0 => "\x7F\xFE\x80\x01", // raw big-endian - 1 => "\xFE\x7F\x01\x80", // raw little-endian - 2 => "\x1F\xFF\xE8\x00", // 14-bit big-endian - 3 => "\xFF\x1F\x00\xE8"); // 14-bit little-endian - - public function Analyze() { - $info = &$this->getid3->info; - $info['fileformat'] = 'dts'; - - $this->fseek($info['avdataoffset']); - $DTSheader = $this->fread(20); // we only need 2 words magic + 6 words frame header, but these words may be normal 16-bit words OR 14-bit words with 2 highest bits set to zero, so 8 words can be either 8*16/8 = 16 bytes OR 8*16*(16/14)/8 = 18.3 bytes - - // check syncword - $sync = substr($DTSheader, 0, 4); - if (($encoding = array_search($sync, self::$syncwords)) !== false) { - - $info['dts']['raw']['magic'] = $sync; - $this->readBinDataOffset = 32; - - } elseif ($this->isDependencyFor('matroska')) { - - // Matroska contains DTS without syncword encoded as raw big-endian format - $encoding = 0; - $this->readBinDataOffset = 0; - - } else { - - unset($info['fileformat']); - return $this->error('Expecting "'.implode('| ', array_map('getid3_lib::PrintHexBytes', self::$syncwords)).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($sync).'"'); - - } - - // decode header - $fhBS = ''; - for ($word_offset = 0; $word_offset <= strlen($DTSheader); $word_offset += 2) { - switch ($encoding) { - case 0: // raw big-endian - $fhBS .= getid3_lib::BigEndian2Bin( substr($DTSheader, $word_offset, 2) ); - break; - case 1: // raw little-endian - $fhBS .= getid3_lib::BigEndian2Bin(strrev(substr($DTSheader, $word_offset, 2))); - break; - case 2: // 14-bit big-endian - $fhBS .= substr(getid3_lib::BigEndian2Bin( substr($DTSheader, $word_offset, 2) ), 2, 14); - break; - case 3: // 14-bit little-endian - $fhBS .= substr(getid3_lib::BigEndian2Bin(strrev(substr($DTSheader, $word_offset, 2))), 2, 14); - break; - } - } - - $info['dts']['raw']['frame_type'] = $this->readBinData($fhBS, 1); - $info['dts']['raw']['deficit_samples'] = $this->readBinData($fhBS, 5); - $info['dts']['flags']['crc_present'] = (bool) $this->readBinData($fhBS, 1); - $info['dts']['raw']['pcm_sample_blocks'] = $this->readBinData($fhBS, 7); - $info['dts']['raw']['frame_byte_size'] = $this->readBinData($fhBS, 14); - $info['dts']['raw']['channel_arrangement'] = $this->readBinData($fhBS, 6); - $info['dts']['raw']['sample_frequency'] = $this->readBinData($fhBS, 4); - $info['dts']['raw']['bitrate'] = $this->readBinData($fhBS, 5); - $info['dts']['flags']['embedded_downmix'] = (bool) $this->readBinData($fhBS, 1); - $info['dts']['flags']['dynamicrange'] = (bool) $this->readBinData($fhBS, 1); - $info['dts']['flags']['timestamp'] = (bool) $this->readBinData($fhBS, 1); - $info['dts']['flags']['auxdata'] = (bool) $this->readBinData($fhBS, 1); - $info['dts']['flags']['hdcd'] = (bool) $this->readBinData($fhBS, 1); - $info['dts']['raw']['extension_audio'] = $this->readBinData($fhBS, 3); - $info['dts']['flags']['extended_coding'] = (bool) $this->readBinData($fhBS, 1); - $info['dts']['flags']['audio_sync_insertion'] = (bool) $this->readBinData($fhBS, 1); - $info['dts']['raw']['lfe_effects'] = $this->readBinData($fhBS, 2); - $info['dts']['flags']['predictor_history'] = (bool) $this->readBinData($fhBS, 1); - if ($info['dts']['flags']['crc_present']) { - $info['dts']['raw']['crc16'] = $this->readBinData($fhBS, 16); - } - $info['dts']['flags']['mri_perfect_reconst'] = (bool) $this->readBinData($fhBS, 1); - $info['dts']['raw']['encoder_soft_version'] = $this->readBinData($fhBS, 4); - $info['dts']['raw']['copy_history'] = $this->readBinData($fhBS, 2); - $info['dts']['raw']['bits_per_sample'] = $this->readBinData($fhBS, 2); - $info['dts']['flags']['surround_es'] = (bool) $this->readBinData($fhBS, 1); - $info['dts']['flags']['front_sum_diff'] = (bool) $this->readBinData($fhBS, 1); - $info['dts']['flags']['surround_sum_diff'] = (bool) $this->readBinData($fhBS, 1); - $info['dts']['raw']['dialog_normalization'] = $this->readBinData($fhBS, 4); - - - $info['dts']['bitrate'] = self::bitrateLookup($info['dts']['raw']['bitrate']); - $info['dts']['bits_per_sample'] = self::bitPerSampleLookup($info['dts']['raw']['bits_per_sample']); - $info['dts']['sample_rate'] = self::sampleRateLookup($info['dts']['raw']['sample_frequency']); - $info['dts']['dialog_normalization'] = self::dialogNormalization($info['dts']['raw']['dialog_normalization'], $info['dts']['raw']['encoder_soft_version']); - $info['dts']['flags']['lossless'] = (($info['dts']['raw']['bitrate'] == 31) ? true : false); - $info['dts']['bitrate_mode'] = (($info['dts']['raw']['bitrate'] == 30) ? 'vbr' : 'cbr'); - $info['dts']['channels'] = self::numChannelsLookup($info['dts']['raw']['channel_arrangement']); - $info['dts']['channel_arrangement'] = self::channelArrangementLookup($info['dts']['raw']['channel_arrangement']); - - $info['audio']['dataformat'] = 'dts'; - $info['audio']['lossless'] = $info['dts']['flags']['lossless']; - $info['audio']['bitrate_mode'] = $info['dts']['bitrate_mode']; - $info['audio']['bits_per_sample'] = $info['dts']['bits_per_sample']; - $info['audio']['sample_rate'] = $info['dts']['sample_rate']; - $info['audio']['channels'] = $info['dts']['channels']; - $info['audio']['bitrate'] = $info['dts']['bitrate']; - if (isset($info['avdataend']) && !empty($info['dts']['bitrate']) && is_numeric($info['dts']['bitrate'])) { - $info['playtime_seconds'] = ($info['avdataend'] - $info['avdataoffset']) / ($info['dts']['bitrate'] / 8); - if (($encoding == 2) || ($encoding == 3)) { - // 14-bit data packed into 16-bit words, so the playtime is wrong because only (14/16) of the bytes in the data portion of the file are used at the specified bitrate - $info['playtime_seconds'] *= (14 / 16); - } - } - return true; - } - - private function readBinData($bin, $length) { - $data = substr($bin, $this->readBinDataOffset, $length); - $this->readBinDataOffset += $length; - - return bindec($data); - } - - public static function bitrateLookup($index) { - static $lookup = array( - 0 => 32000, - 1 => 56000, - 2 => 64000, - 3 => 96000, - 4 => 112000, - 5 => 128000, - 6 => 192000, - 7 => 224000, - 8 => 256000, - 9 => 320000, - 10 => 384000, - 11 => 448000, - 12 => 512000, - 13 => 576000, - 14 => 640000, - 15 => 768000, - 16 => 960000, - 17 => 1024000, - 18 => 1152000, - 19 => 1280000, - 20 => 1344000, - 21 => 1408000, - 22 => 1411200, - 23 => 1472000, - 24 => 1536000, - 25 => 1920000, - 26 => 2048000, - 27 => 3072000, - 28 => 3840000, - 29 => 'open', - 30 => 'variable', - 31 => 'lossless', - ); - return (isset($lookup[$index]) ? $lookup[$index] : false); - } - - public static function sampleRateLookup($index) { - static $lookup = array( - 0 => 'invalid', - 1 => 8000, - 2 => 16000, - 3 => 32000, - 4 => 'invalid', - 5 => 'invalid', - 6 => 11025, - 7 => 22050, - 8 => 44100, - 9 => 'invalid', - 10 => 'invalid', - 11 => 12000, - 12 => 24000, - 13 => 48000, - 14 => 'invalid', - 15 => 'invalid', - ); - return (isset($lookup[$index]) ? $lookup[$index] : false); - } - - public static function bitPerSampleLookup($index) { - static $lookup = array( - 0 => 16, - 1 => 20, - 2 => 24, - 3 => 24, - ); - return (isset($lookup[$index]) ? $lookup[$index] : false); - } - - public static function numChannelsLookup($index) { - switch ($index) { - case 0: - return 1; - break; - case 1: - case 2: - case 3: - case 4: - return 2; - break; - case 5: - case 6: - return 3; - break; - case 7: - case 8: - return 4; - break; - case 9: - return 5; - break; - case 10: - case 11: - case 12: - return 6; - break; - case 13: - return 7; - break; - case 14: - case 15: - return 8; - break; - } - return false; - } - - public static function channelArrangementLookup($index) { - static $lookup = array( - 0 => 'A', - 1 => 'A + B (dual mono)', - 2 => 'L + R (stereo)', - 3 => '(L+R) + (L-R) (sum-difference)', - 4 => 'LT + RT (left and right total)', - 5 => 'C + L + R', - 6 => 'L + R + S', - 7 => 'C + L + R + S', - 8 => 'L + R + SL + SR', - 9 => 'C + L + R + SL + SR', - 10 => 'CL + CR + L + R + SL + SR', - 11 => 'C + L + R+ LR + RR + OV', - 12 => 'CF + CR + LF + RF + LR + RR', - 13 => 'CL + C + CR + L + R + SL + SR', - 14 => 'CL + CR + L + R + SL1 + SL2 + SR1 + SR2', - 15 => 'CL + C+ CR + L + R + SL + S + SR', - ); - return (isset($lookup[$index]) ? $lookup[$index] : 'user-defined'); - } - - public static function dialogNormalization($index, $version) { - switch ($version) { - case 7: - return 0 - $index; - break; - case 6: - return 0 - 16 - $index; - break; - } - return false; - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.flac.php b/src/Classes/Vendor/getid3/module.audio.flac.php deleted file mode 100755 index 6b9598c74..000000000 --- a/src/Classes/Vendor/getid3/module.audio.flac.php +++ /dev/null @@ -1,442 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.flac.php // -// module for analyzing FLAC and OggFLAC audio files // -// dependencies: module.audio.ogg.php // -// /// -///////////////////////////////////////////////////////////////// - - -getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.ogg.php', __FILE__, true); - -/** -* @tutorial http://flac.sourceforge.net/format.html -*/ -class getid3_flac extends getid3_handler -{ - const syncword = 'fLaC'; - - public function Analyze() { - $info = &$this->getid3->info; - - $this->fseek($info['avdataoffset']); - $StreamMarker = $this->fread(4); - if ($StreamMarker != self::syncword) { - return $this->error('Expecting "'.getid3_lib::PrintHexBytes(self::syncword).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($StreamMarker).'"'); - } - $info['fileformat'] = 'flac'; - $info['audio']['dataformat'] = 'flac'; - $info['audio']['bitrate_mode'] = 'vbr'; - $info['audio']['lossless'] = true; - - // parse flac container - return $this->parseMETAdata(); - } - - public function parseMETAdata() { - $info = &$this->getid3->info; - do { - $BlockOffset = $this->ftell(); - $BlockHeader = $this->fread(4); - $LBFBT = getid3_lib::BigEndian2Int(substr($BlockHeader, 0, 1)); - $LastBlockFlag = (bool) ($LBFBT & 0x80); - $BlockType = ($LBFBT & 0x7F); - $BlockLength = getid3_lib::BigEndian2Int(substr($BlockHeader, 1, 3)); - $BlockTypeText = self::metaBlockTypeLookup($BlockType); - - if (($BlockOffset + 4 + $BlockLength) > $info['avdataend']) { - $this->error('METADATA_BLOCK_HEADER.BLOCK_TYPE ('.$BlockTypeText.') at offset '.$BlockOffset.' extends beyond end of file'); - break; - } - if ($BlockLength < 1) { - $this->error('METADATA_BLOCK_HEADER.BLOCK_LENGTH ('.$BlockLength.') at offset '.$BlockOffset.' is invalid'); - break; - } - - $info['flac'][$BlockTypeText]['raw'] = array(); - $BlockTypeText_raw = &$info['flac'][$BlockTypeText]['raw']; - - $BlockTypeText_raw['offset'] = $BlockOffset; - $BlockTypeText_raw['last_meta_block'] = $LastBlockFlag; - $BlockTypeText_raw['block_type'] = $BlockType; - $BlockTypeText_raw['block_type_text'] = $BlockTypeText; - $BlockTypeText_raw['block_length'] = $BlockLength; - if ($BlockTypeText_raw['block_type'] != 0x06) { // do not read attachment data automatically - $BlockTypeText_raw['block_data'] = $this->fread($BlockLength); - } - - switch ($BlockTypeText) { - case 'STREAMINFO': // 0x00 - if (!$this->parseSTREAMINFO($BlockTypeText_raw['block_data'])) { - return false; - } - break; - - case 'PADDING': // 0x01 - unset($info['flac']['PADDING']); // ignore - break; - - case 'APPLICATION': // 0x02 - if (!$this->parseAPPLICATION($BlockTypeText_raw['block_data'])) { - return false; - } - break; - - case 'SEEKTABLE': // 0x03 - if (!$this->parseSEEKTABLE($BlockTypeText_raw['block_data'])) { - return false; - } - break; - - case 'VORBIS_COMMENT': // 0x04 - if (!$this->parseVORBIS_COMMENT($BlockTypeText_raw['block_data'])) { - return false; - } - break; - - case 'CUESHEET': // 0x05 - if (!$this->parseCUESHEET($BlockTypeText_raw['block_data'])) { - return false; - } - break; - - case 'PICTURE': // 0x06 - if (!$this->parsePICTURE()) { - return false; - } - break; - - default: - $this->warning('Unhandled METADATA_BLOCK_HEADER.BLOCK_TYPE ('.$BlockType.') at offset '.$BlockOffset); - } - - unset($info['flac'][$BlockTypeText]['raw']); - $info['avdataoffset'] = $this->ftell(); - } - while ($LastBlockFlag === false); - - // handle tags - if (!empty($info['flac']['VORBIS_COMMENT']['comments'])) { - $info['flac']['comments'] = $info['flac']['VORBIS_COMMENT']['comments']; - } - if (!empty($info['flac']['VORBIS_COMMENT']['vendor'])) { - $info['audio']['encoder'] = str_replace('reference ', '', $info['flac']['VORBIS_COMMENT']['vendor']); - } - - // copy attachments to 'comments' array if nesesary - if (isset($info['flac']['PICTURE']) && ($this->getid3->option_save_attachments !== getID3::ATTACHMENTS_NONE)) { - foreach ($info['flac']['PICTURE'] as $entry) { - if (!empty($entry['data'])) { - $info['flac']['comments']['picture'][] = array('image_mime'=>$entry['image_mime'], 'data'=>$entry['data']); - } - } - } - - if (isset($info['flac']['STREAMINFO'])) { - if (!$this->isDependencyFor('matroska')) { - $info['flac']['compressed_audio_bytes'] = $info['avdataend'] - $info['avdataoffset']; - } - $info['flac']['uncompressed_audio_bytes'] = $info['flac']['STREAMINFO']['samples_stream'] * $info['flac']['STREAMINFO']['channels'] * ($info['flac']['STREAMINFO']['bits_per_sample'] / 8); - if ($info['flac']['uncompressed_audio_bytes'] == 0) { - return $this->error('Corrupt FLAC file: uncompressed_audio_bytes == zero'); - } - if (!empty($info['flac']['compressed_audio_bytes'])) { - $info['flac']['compression_ratio'] = $info['flac']['compressed_audio_bytes'] / $info['flac']['uncompressed_audio_bytes']; - } - } - - // set md5_data_source - built into flac 0.5+ - if (isset($info['flac']['STREAMINFO']['audio_signature'])) { - - if ($info['flac']['STREAMINFO']['audio_signature'] === str_repeat("\x00", 16)) { - $this->warning('FLAC STREAMINFO.audio_signature is null (known issue with libOggFLAC)'); - } - else { - $info['md5_data_source'] = ''; - $md5 = $info['flac']['STREAMINFO']['audio_signature']; - for ($i = 0; $i < strlen($md5); $i++) { - $info['md5_data_source'] .= str_pad(dechex(ord($md5[$i])), 2, '00', STR_PAD_LEFT); - } - if (!preg_match('/^[0-9a-f]{32}$/', $info['md5_data_source'])) { - unset($info['md5_data_source']); - } - } - } - - if (isset($info['flac']['STREAMINFO']['bits_per_sample'])) { - $info['audio']['bits_per_sample'] = $info['flac']['STREAMINFO']['bits_per_sample']; - if ($info['audio']['bits_per_sample'] == 8) { - // special case - // must invert sign bit on all data bytes before MD5'ing to match FLAC's calculated value - // MD5sum calculates on unsigned bytes, but FLAC calculated MD5 on 8-bit audio data as signed - $this->warning('FLAC calculates MD5 data strangely on 8-bit audio, so the stored md5_data_source value will not match the decoded WAV file'); - } - } - - return true; - } - - private function parseSTREAMINFO($BlockData) { - $info = &$this->getid3->info; - - $info['flac']['STREAMINFO'] = array(); - $streaminfo = &$info['flac']['STREAMINFO']; - - $streaminfo['min_block_size'] = getid3_lib::BigEndian2Int(substr($BlockData, 0, 2)); - $streaminfo['max_block_size'] = getid3_lib::BigEndian2Int(substr($BlockData, 2, 2)); - $streaminfo['min_frame_size'] = getid3_lib::BigEndian2Int(substr($BlockData, 4, 3)); - $streaminfo['max_frame_size'] = getid3_lib::BigEndian2Int(substr($BlockData, 7, 3)); - - $SRCSBSS = getid3_lib::BigEndian2Bin(substr($BlockData, 10, 8)); - $streaminfo['sample_rate'] = getid3_lib::Bin2Dec(substr($SRCSBSS, 0, 20)); - $streaminfo['channels'] = getid3_lib::Bin2Dec(substr($SRCSBSS, 20, 3)) + 1; - $streaminfo['bits_per_sample'] = getid3_lib::Bin2Dec(substr($SRCSBSS, 23, 5)) + 1; - $streaminfo['samples_stream'] = getid3_lib::Bin2Dec(substr($SRCSBSS, 28, 36)); - - $streaminfo['audio_signature'] = substr($BlockData, 18, 16); - - if (!empty($streaminfo['sample_rate'])) { - - $info['audio']['bitrate_mode'] = 'vbr'; - $info['audio']['sample_rate'] = $streaminfo['sample_rate']; - $info['audio']['channels'] = $streaminfo['channels']; - $info['audio']['bits_per_sample'] = $streaminfo['bits_per_sample']; - $info['playtime_seconds'] = $streaminfo['samples_stream'] / $streaminfo['sample_rate']; - if ($info['playtime_seconds'] > 0) { - if (!$this->isDependencyFor('matroska')) { - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; - } - else { - $this->warning('Cannot determine audio bitrate because total stream size is unknown'); - } - } - - } else { - return $this->error('Corrupt METAdata block: STREAMINFO'); - } - - return true; - } - - private function parseAPPLICATION($BlockData) { - $info = &$this->getid3->info; - - $ApplicationID = getid3_lib::BigEndian2Int(substr($BlockData, 0, 4)); - $info['flac']['APPLICATION'][$ApplicationID]['name'] = self::applicationIDLookup($ApplicationID); - $info['flac']['APPLICATION'][$ApplicationID]['data'] = substr($BlockData, 4); - - return true; - } - - private function parseSEEKTABLE($BlockData) { - $info = &$this->getid3->info; - - $offset = 0; - $BlockLength = strlen($BlockData); - $placeholderpattern = str_repeat("\xFF", 8); - while ($offset < $BlockLength) { - $SampleNumberString = substr($BlockData, $offset, 8); - $offset += 8; - if ($SampleNumberString == $placeholderpattern) { - - // placeholder point - getid3_lib::safe_inc($info['flac']['SEEKTABLE']['placeholders'], 1); - $offset += 10; - - } else { - - $SampleNumber = getid3_lib::BigEndian2Int($SampleNumberString); - $info['flac']['SEEKTABLE'][$SampleNumber]['offset'] = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 8)); - $offset += 8; - $info['flac']['SEEKTABLE'][$SampleNumber]['samples'] = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 2)); - $offset += 2; - - } - } - - return true; - } - - private function parseVORBIS_COMMENT($BlockData) { - $info = &$this->getid3->info; - - $getid3_ogg = new getid3_ogg($this->getid3); - if ($this->isDependencyFor('matroska')) { - $getid3_ogg->setStringMode($this->data_string); - } - $getid3_ogg->ParseVorbisComments(); - if (isset($info['ogg'])) { - unset($info['ogg']['comments_raw']); - $info['flac']['VORBIS_COMMENT'] = $info['ogg']; - unset($info['ogg']); - } - - unset($getid3_ogg); - - return true; - } - - private function parseCUESHEET($BlockData) { - $info = &$this->getid3->info; - $offset = 0; - $info['flac']['CUESHEET']['media_catalog_number'] = trim(substr($BlockData, $offset, 128), "\0"); - $offset += 128; - $info['flac']['CUESHEET']['lead_in_samples'] = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 8)); - $offset += 8; - $info['flac']['CUESHEET']['flags']['is_cd'] = (bool) (getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1)) & 0x80); - $offset += 1; - - $offset += 258; // reserved - - $info['flac']['CUESHEET']['number_tracks'] = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1)); - $offset += 1; - - for ($track = 0; $track < $info['flac']['CUESHEET']['number_tracks']; $track++) { - $TrackSampleOffset = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 8)); - $offset += 8; - $TrackNumber = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1)); - $offset += 1; - - $info['flac']['CUESHEET']['tracks'][$TrackNumber]['sample_offset'] = $TrackSampleOffset; - - $info['flac']['CUESHEET']['tracks'][$TrackNumber]['isrc'] = substr($BlockData, $offset, 12); - $offset += 12; - - $TrackFlagsRaw = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1)); - $offset += 1; - $info['flac']['CUESHEET']['tracks'][$TrackNumber]['flags']['is_audio'] = (bool) ($TrackFlagsRaw & 0x80); - $info['flac']['CUESHEET']['tracks'][$TrackNumber]['flags']['pre_emphasis'] = (bool) ($TrackFlagsRaw & 0x40); - - $offset += 13; // reserved - - $info['flac']['CUESHEET']['tracks'][$TrackNumber]['index_points'] = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1)); - $offset += 1; - - for ($index = 0; $index < $info['flac']['CUESHEET']['tracks'][$TrackNumber]['index_points']; $index++) { - $IndexSampleOffset = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 8)); - $offset += 8; - $IndexNumber = getid3_lib::BigEndian2Int(substr($BlockData, $offset, 1)); - $offset += 1; - - $offset += 3; // reserved - - $info['flac']['CUESHEET']['tracks'][$TrackNumber]['indexes'][$IndexNumber] = $IndexSampleOffset; - } - } - - return true; - } - - /** - * Parse METADATA_BLOCK_PICTURE flac structure and extract attachment - * External usage: audio.ogg - */ - public function parsePICTURE() { - $info = &$this->getid3->info; - - $picture['typeid'] = getid3_lib::BigEndian2Int($this->fread(4)); - $picture['type'] = self::pictureTypeLookup($picture['typeid']); - $picture['image_mime'] = $this->fread(getid3_lib::BigEndian2Int($this->fread(4))); - $descr_length = getid3_lib::BigEndian2Int($this->fread(4)); - if ($descr_length) { - $picture['description'] = $this->fread($descr_length); - } - $picture['width'] = getid3_lib::BigEndian2Int($this->fread(4)); - $picture['height'] = getid3_lib::BigEndian2Int($this->fread(4)); - $picture['color_depth'] = getid3_lib::BigEndian2Int($this->fread(4)); - $picture['colors_indexed'] = getid3_lib::BigEndian2Int($this->fread(4)); - $data_length = getid3_lib::BigEndian2Int($this->fread(4)); - - if ($picture['image_mime'] == '-->') { - $picture['data'] = $this->fread($data_length); - } else { - $picture['data'] = $this->saveAttachment( - str_replace('/', '_', $picture['type']).'_'.$this->ftell(), - $this->ftell(), - $data_length, - $picture['image_mime']); - } - - $info['flac']['PICTURE'][] = $picture; - - return true; - } - - public static function metaBlockTypeLookup($blocktype) { - static $lookup = array( - 0 => 'STREAMINFO', - 1 => 'PADDING', - 2 => 'APPLICATION', - 3 => 'SEEKTABLE', - 4 => 'VORBIS_COMMENT', - 5 => 'CUESHEET', - 6 => 'PICTURE', - ); - return (isset($lookup[$blocktype]) ? $lookup[$blocktype] : 'reserved'); - } - - public static function applicationIDLookup($applicationid) { - // http://flac.sourceforge.net/id.html - static $lookup = array( - 0x41544348 => 'FlacFile', // "ATCH" - 0x42534F4C => 'beSolo', // "BSOL" - 0x42554753 => 'Bugs Player', // "BUGS" - 0x43756573 => 'GoldWave cue points (specification)', // "Cues" - 0x46696361 => 'CUE Splitter', // "Fica" - 0x46746F6C => 'flac-tools', // "Ftol" - 0x4D4F5442 => 'MOTB MetaCzar', // "MOTB" - 0x4D505345 => 'MP3 Stream Editor', // "MPSE" - 0x4D754D4C => 'MusicML: Music Metadata Language', // "MuML" - 0x52494646 => 'Sound Devices RIFF chunk storage', // "RIFF" - 0x5346464C => 'Sound Font FLAC', // "SFFL" - 0x534F4E59 => 'Sony Creative Software', // "SONY" - 0x5351455A => 'flacsqueeze', // "SQEZ" - 0x54745776 => 'TwistedWave', // "TtWv" - 0x55495453 => 'UITS Embedding tools', // "UITS" - 0x61696666 => 'FLAC AIFF chunk storage', // "aiff" - 0x696D6167 => 'flac-image application for storing arbitrary files in APPLICATION metadata blocks', // "imag" - 0x7065656D => 'Parseable Embedded Extensible Metadata (specification)', // "peem" - 0x71667374 => 'QFLAC Studio', // "qfst" - 0x72696666 => 'FLAC RIFF chunk storage', // "riff" - 0x74756E65 => 'TagTuner', // "tune" - 0x78626174 => 'XBAT', // "xbat" - 0x786D6364 => 'xmcd', // "xmcd" - ); - return (isset($lookup[$applicationid]) ? $lookup[$applicationid] : 'reserved'); - } - - public static function pictureTypeLookup($type_id) { - static $lookup = array ( - 0 => 'Other', - 1 => '32x32 pixels \'file icon\' (PNG only)', - 2 => 'Other file icon', - 3 => 'Cover (front)', - 4 => 'Cover (back)', - 5 => 'Leaflet page', - 6 => 'Media (e.g. label side of CD)', - 7 => 'Lead artist/lead performer/soloist', - 8 => 'Artist/performer', - 9 => 'Conductor', - 10 => 'Band/Orchestra', - 11 => 'Composer', - 12 => 'Lyricist/text writer', - 13 => 'Recording Location', - 14 => 'During recording', - 15 => 'During performance', - 16 => 'Movie/video screen capture', - 17 => 'A bright coloured fish', - 18 => 'Illustration', - 19 => 'Band/artist logotype', - 20 => 'Publisher/Studio logotype', - ); - return (isset($lookup[$type_id]) ? $lookup[$type_id] : 'reserved'); - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.la.php b/src/Classes/Vendor/getid3/module.audio.la.php deleted file mode 100755 index 943a4ca78..000000000 --- a/src/Classes/Vendor/getid3/module.audio.la.php +++ /dev/null @@ -1,225 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.la.php // -// module for analyzing LA (LosslessAudio) audio files // -// dependencies: module.audio.riff.php // -// /// -///////////////////////////////////////////////////////////////// - -getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); - -class getid3_la extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - $offset = 0; - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $rawdata = fread($this->getid3->fp, $this->getid3->fread_buffer_size()); - - switch (substr($rawdata, $offset, 4)) { - case 'LA02': - case 'LA03': - case 'LA04': - $info['fileformat'] = 'la'; - $info['audio']['dataformat'] = 'la'; - $info['audio']['lossless'] = true; - - $info['la']['version_major'] = (int) substr($rawdata, $offset + 2, 1); - $info['la']['version_minor'] = (int) substr($rawdata, $offset + 3, 1); - $info['la']['version'] = (float) $info['la']['version_major'] + ($info['la']['version_minor'] / 10); - $offset += 4; - - $info['la']['uncompressed_size'] = getid3_lib::LittleEndian2Int(substr($rawdata, $offset, 4)); - $offset += 4; - if ($info['la']['uncompressed_size'] == 0) { - $info['error'][] = 'Corrupt LA file: uncompressed_size == zero'; - return false; - } - - $WAVEchunk = substr($rawdata, $offset, 4); - if ($WAVEchunk !== 'WAVE') { - $info['error'][] = 'Expected "WAVE" ('.getid3_lib::PrintHexBytes('WAVE').') at offset '.$offset.', found "'.$WAVEchunk.'" ('.getid3_lib::PrintHexBytes($WAVEchunk).') instead.'; - return false; - } - $offset += 4; - - $info['la']['fmt_size'] = 24; - if ($info['la']['version'] >= 0.3) { - - $info['la']['fmt_size'] = getid3_lib::LittleEndian2Int(substr($rawdata, $offset, 4)); - $info['la']['header_size'] = 49 + $info['la']['fmt_size'] - 24; - $offset += 4; - - } else { - - // version 0.2 didn't support additional data blocks - $info['la']['header_size'] = 41; - - } - - $fmt_chunk = substr($rawdata, $offset, 4); - if ($fmt_chunk !== 'fmt ') { - $info['error'][] = 'Expected "fmt " ('.getid3_lib::PrintHexBytes('fmt ').') at offset '.$offset.', found "'.$fmt_chunk.'" ('.getid3_lib::PrintHexBytes($fmt_chunk).') instead.'; - return false; - } - $offset += 4; - $fmt_size = getid3_lib::LittleEndian2Int(substr($rawdata, $offset, 4)); - $offset += 4; - - $info['la']['raw']['format'] = getid3_lib::LittleEndian2Int(substr($rawdata, $offset, 2)); - $offset += 2; - - $info['la']['channels'] = getid3_lib::LittleEndian2Int(substr($rawdata, $offset, 2)); - $offset += 2; - if ($info['la']['channels'] == 0) { - $info['error'][] = 'Corrupt LA file: channels == zero'; - return false; - } - - $info['la']['sample_rate'] = getid3_lib::LittleEndian2Int(substr($rawdata, $offset, 4)); - $offset += 4; - if ($info['la']['sample_rate'] == 0) { - $info['error'][] = 'Corrupt LA file: sample_rate == zero'; - return false; - } - - $info['la']['bytes_per_second'] = getid3_lib::LittleEndian2Int(substr($rawdata, $offset, 4)); - $offset += 4; - $info['la']['bytes_per_sample'] = getid3_lib::LittleEndian2Int(substr($rawdata, $offset, 2)); - $offset += 2; - $info['la']['bits_per_sample'] = getid3_lib::LittleEndian2Int(substr($rawdata, $offset, 2)); - $offset += 2; - - $info['la']['samples'] = getid3_lib::LittleEndian2Int(substr($rawdata, $offset, 4)); - $offset += 4; - - $info['la']['raw']['flags'] = getid3_lib::LittleEndian2Int(substr($rawdata, $offset, 1)); - $offset += 1; - $info['la']['flags']['seekable'] = (bool) ($info['la']['raw']['flags'] & 0x01); - if ($info['la']['version'] >= 0.4) { - $info['la']['flags']['high_compression'] = (bool) ($info['la']['raw']['flags'] & 0x02); - } - - $info['la']['original_crc'] = getid3_lib::LittleEndian2Int(substr($rawdata, $offset, 4)); - $offset += 4; - - // mikeØbevin*de - // Basically, the blocksize/seekevery are 61440/19 in La0.4 and 73728/16 - // in earlier versions. A seekpoint is added every blocksize * seekevery - // samples, so 4 * int(totalSamples / (blockSize * seekEvery)) should - // give the number of bytes used for the seekpoints. Of course, if seeking - // is disabled, there are no seekpoints stored. - if ($info['la']['version'] >= 0.4) { - $info['la']['blocksize'] = 61440; - $info['la']['seekevery'] = 19; - } else { - $info['la']['blocksize'] = 73728; - $info['la']['seekevery'] = 16; - } - - $info['la']['seekpoint_count'] = 0; - if ($info['la']['flags']['seekable']) { - $info['la']['seekpoint_count'] = floor($info['la']['samples'] / ($info['la']['blocksize'] * $info['la']['seekevery'])); - - for ($i = 0; $i < $info['la']['seekpoint_count']; $i++) { - $info['la']['seekpoints'][] = getid3_lib::LittleEndian2Int(substr($rawdata, $offset, 4)); - $offset += 4; - } - } - - if ($info['la']['version'] >= 0.3) { - - // Following the main header information, the program outputs all of the - // seekpoints. Following these is what I called the 'footer start', - // i.e. the position immediately after the La audio data is finished. - $info['la']['footerstart'] = getid3_lib::LittleEndian2Int(substr($rawdata, $offset, 4)); - $offset += 4; - - if ($info['la']['footerstart'] > $info['filesize']) { - $info['warning'][] = 'FooterStart value points to offset '.$info['la']['footerstart'].' which is beyond end-of-file ('.$info['filesize'].')'; - $info['la']['footerstart'] = $info['filesize']; - } - - } else { - - // La v0.2 didn't have FooterStart value - $info['la']['footerstart'] = $info['avdataend']; - - } - - if ($info['la']['footerstart'] < $info['avdataend']) { - if ($RIFFtempfilename = tempnam(GETID3_TEMP_DIR, 'id3')) { - if ($RIFF_fp = fopen($RIFFtempfilename, 'w+b')) { - $RIFFdata = 'WAVE'; - if ($info['la']['version'] == 0.2) { - $RIFFdata .= substr($rawdata, 12, 24); - } else { - $RIFFdata .= substr($rawdata, 16, 24); - } - if ($info['la']['footerstart'] < $info['avdataend']) { - fseek($this->getid3->fp, $info['la']['footerstart'], SEEK_SET); - $RIFFdata .= fread($this->getid3->fp, $info['avdataend'] - $info['la']['footerstart']); - } - $RIFFdata = 'RIFF'.getid3_lib::LittleEndian2String(strlen($RIFFdata), 4, false).$RIFFdata; - fwrite($RIFF_fp, $RIFFdata, strlen($RIFFdata)); - fclose($RIFF_fp); - - $getid3_temp = new getID3(); - $getid3_temp->openfile($RIFFtempfilename); - $getid3_riff = new getid3_riff($getid3_temp); - $getid3_riff->Analyze(); - - if (empty($getid3_temp->info['error'])) { - $info['riff'] = $getid3_temp->info['riff']; - } else { - $info['warning'][] = 'Error parsing RIFF portion of La file: '.implode($getid3_temp->info['error']); - } - unset($getid3_temp, $getid3_riff); - } - unlink($RIFFtempfilename); - } - } - - // $info['avdataoffset'] should be zero to begin with, but just in case it's not, include the addition anyway - $info['avdataend'] = $info['avdataoffset'] + $info['la']['footerstart']; - $info['avdataoffset'] = $info['avdataoffset'] + $offset; - - $info['la']['compression_ratio'] = (float) (($info['avdataend'] - $info['avdataoffset']) / $info['la']['uncompressed_size']); - $info['playtime_seconds'] = (float) ($info['la']['samples'] / $info['la']['sample_rate']) / $info['la']['channels']; - if ($info['playtime_seconds'] == 0) { - $info['error'][] = 'Corrupt LA file: playtime_seconds == zero'; - return false; - } - - $info['audio']['bitrate'] = ($info['avdataend'] - $info['avdataoffset']) * 8 / $info['playtime_seconds']; - //$info['audio']['codec'] = $info['la']['codec']; - $info['audio']['bits_per_sample'] = $info['la']['bits_per_sample']; - break; - - default: - if (substr($rawdata, $offset, 2) == 'LA') { - $info['error'][] = 'This version of getID3() ['.$this->getid3->version().'] does not support LA version '.substr($rawdata, $offset + 2, 1).'.'.substr($rawdata, $offset + 3, 1).' which this appears to be - check http://getid3.sourceforge.net for updates.'; - } else { - $info['error'][] = 'Not a LA (Lossless-Audio) file'; - } - return false; - break; - } - - $info['audio']['channels'] = $info['la']['channels']; - $info['audio']['sample_rate'] = (int) $info['la']['sample_rate']; - $info['audio']['encoder'] = 'LA v'.$info['la']['version']; - - return true; - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.lpac.php b/src/Classes/Vendor/getid3/module.audio.lpac.php deleted file mode 100755 index 3d45e0000..000000000 --- a/src/Classes/Vendor/getid3/module.audio.lpac.php +++ /dev/null @@ -1,127 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.lpac.php // -// module for analyzing LPAC Audio files // -// dependencies: module.audio-video.riff.php // -// /// -///////////////////////////////////////////////////////////////// - -getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); - -class getid3_lpac extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $LPACheader = fread($this->getid3->fp, 14); - if (substr($LPACheader, 0, 4) != 'LPAC') { - $info['error'][] = 'Expected "LPAC" at offset '.$info['avdataoffset'].', found "'.$StreamMarker.'"'; - return false; - } - $info['avdataoffset'] += 14; - - $info['fileformat'] = 'lpac'; - $info['audio']['dataformat'] = 'lpac'; - $info['audio']['lossless'] = true; - $info['audio']['bitrate_mode'] = 'vbr'; - - $info['lpac']['file_version'] = getid3_lib::BigEndian2Int(substr($LPACheader, 4, 1)); - $flags['audio_type'] = getid3_lib::BigEndian2Int(substr($LPACheader, 5, 1)); - $info['lpac']['total_samples']= getid3_lib::BigEndian2Int(substr($LPACheader, 6, 4)); - $flags['parameters'] = getid3_lib::BigEndian2Int(substr($LPACheader, 10, 4)); - - $info['lpac']['flags']['is_wave'] = (bool) ($flags['audio_type'] & 0x40); - $info['lpac']['flags']['stereo'] = (bool) ($flags['audio_type'] & 0x04); - $info['lpac']['flags']['24_bit'] = (bool) ($flags['audio_type'] & 0x02); - $info['lpac']['flags']['16_bit'] = (bool) ($flags['audio_type'] & 0x01); - - if ($info['lpac']['flags']['24_bit'] && $info['lpac']['flags']['16_bit']) { - $info['warning'][] = '24-bit and 16-bit flags cannot both be set'; - } - - $info['lpac']['flags']['fast_compress'] = (bool) ($flags['parameters'] & 0x40000000); - $info['lpac']['flags']['random_access'] = (bool) ($flags['parameters'] & 0x08000000); - $info['lpac']['block_length'] = pow(2, (($flags['parameters'] & 0x07000000) >> 24)) * 256; - $info['lpac']['flags']['adaptive_prediction_order'] = (bool) ($flags['parameters'] & 0x00800000); - $info['lpac']['flags']['adaptive_quantization'] = (bool) ($flags['parameters'] & 0x00400000); - $info['lpac']['flags']['joint_stereo'] = (bool) ($flags['parameters'] & 0x00040000); - $info['lpac']['quantization'] = ($flags['parameters'] & 0x00001F00) >> 8; - $info['lpac']['max_prediction_order'] = ($flags['parameters'] & 0x0000003F); - - if ($info['lpac']['flags']['fast_compress'] && ($info['lpac']['max_prediction_order'] != 3)) { - $info['warning'][] = 'max_prediction_order expected to be "3" if fast_compress is true, actual value is "'.$info['lpac']['max_prediction_order'].'"'; - } - switch ($info['lpac']['file_version']) { - case 6: - if ($info['lpac']['flags']['adaptive_quantization']) { - $info['warning'][] = 'adaptive_quantization expected to be false in LPAC file stucture v6, actually true'; - } - if ($info['lpac']['quantization'] != 20) { - $info['warning'][] = 'Quantization expected to be 20 in LPAC file stucture v6, actually '.$info['lpac']['flags']['Q']; - } - break; - - default: - //$info['warning'][] = 'This version of getID3() ['.$this->getid3->version().'] only supports LPAC file format version 6, this file is version '.$info['lpac']['file_version'].' - please report to info@getid3.org'; - break; - } - - $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); - $getid3_temp->info = $info; - $getid3_riff = new getid3_riff($getid3_temp); - $getid3_riff->Analyze(); - $info['avdataoffset'] = $getid3_temp->info['avdataoffset']; - $info['riff'] = $getid3_temp->info['riff']; - $info['error'] = $getid3_temp->info['error']; - $info['warning'] = $getid3_temp->info['warning']; - $info['lpac']['comments']['comment'] = $getid3_temp->info['comments']; - $info['audio']['sample_rate'] = $getid3_temp->info['audio']['sample_rate']; - unset($getid3_temp, $getid3_riff); - - $info['audio']['channels'] = ($info['lpac']['flags']['stereo'] ? 2 : 1); - - if ($info['lpac']['flags']['24_bit']) { - $info['audio']['bits_per_sample'] = $info['riff']['audio'][0]['bits_per_sample']; - } elseif ($info['lpac']['flags']['16_bit']) { - $info['audio']['bits_per_sample'] = 16; - } else { - $info['audio']['bits_per_sample'] = 8; - } - - if ($info['lpac']['flags']['fast_compress']) { - // fast - $info['audio']['encoder_options'] = '-1'; - } else { - switch ($info['lpac']['max_prediction_order']) { - case 20: // simple - $info['audio']['encoder_options'] = '-2'; - break; - case 30: // medium - $info['audio']['encoder_options'] = '-3'; - break; - case 40: // high - $info['audio']['encoder_options'] = '-4'; - break; - case 60: // extrahigh - $info['audio']['encoder_options'] = '-5'; - break; - } - } - - $info['playtime_seconds'] = $info['lpac']['total_samples'] / $info['audio']['sample_rate']; - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; - - return true; - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.midi.php b/src/Classes/Vendor/getid3/module.audio.midi.php deleted file mode 100755 index 17a870847..000000000 --- a/src/Classes/Vendor/getid3/module.audio.midi.php +++ /dev/null @@ -1,523 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.midi.php // -// module for Midi Audio files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - -define('GETID3_MIDI_MAGIC_MTHD', 'MThd'); // MIDI file header magic -define('GETID3_MIDI_MAGIC_MTRK', 'MTrk'); // MIDI track header magic - -class getid3_midi extends getid3_handler -{ - public $scanwholefile = true; - - public function Analyze() { - $info = &$this->getid3->info; - - // shortcut - $info['midi']['raw'] = array(); - $thisfile_midi = &$info['midi']; - $thisfile_midi_raw = &$thisfile_midi['raw']; - - $info['fileformat'] = 'midi'; - $info['audio']['dataformat'] = 'midi'; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $MIDIdata = fread($this->getid3->fp, $this->getid3->fread_buffer_size()); - $offset = 0; - $MIDIheaderID = substr($MIDIdata, $offset, 4); // 'MThd' - if ($MIDIheaderID != GETID3_MIDI_MAGIC_MTHD) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes(GETID3_MIDI_MAGIC_MTHD).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($MIDIheaderID).'"'; - unset($info['fileformat']); - return false; - } - $offset += 4; - $thisfile_midi_raw['headersize'] = getid3_lib::BigEndian2Int(substr($MIDIdata, $offset, 4)); - $offset += 4; - $thisfile_midi_raw['fileformat'] = getid3_lib::BigEndian2Int(substr($MIDIdata, $offset, 2)); - $offset += 2; - $thisfile_midi_raw['tracks'] = getid3_lib::BigEndian2Int(substr($MIDIdata, $offset, 2)); - $offset += 2; - $thisfile_midi_raw['ticksperqnote'] = getid3_lib::BigEndian2Int(substr($MIDIdata, $offset, 2)); - $offset += 2; - - for ($i = 0; $i < $thisfile_midi_raw['tracks']; $i++) { - while ((strlen($MIDIdata) - $offset) < 8) { - $MIDIdata .= fread($this->getid3->fp, $this->getid3->fread_buffer_size()); - } - $trackID = substr($MIDIdata, $offset, 4); - $offset += 4; - if ($trackID == GETID3_MIDI_MAGIC_MTRK) { - $tracksize = getid3_lib::BigEndian2Int(substr($MIDIdata, $offset, 4)); - $offset += 4; - // $thisfile_midi['tracks'][$i]['size'] = $tracksize; - $trackdataarray[$i] = substr($MIDIdata, $offset, $tracksize); - $offset += $tracksize; - } else { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes(GETID3_MIDI_MAGIC_MTRK).'" at '.($offset - 4).', found "'.getid3_lib::PrintHexBytes($trackID).'" instead'; - return false; - } - } - - if (!isset($trackdataarray) || !is_array($trackdataarray)) { - $info['error'][] = 'Cannot find MIDI track information'; - unset($thisfile_midi); - unset($info['fileformat']); - return false; - } - - if ($this->scanwholefile) { // this can take quite a long time, so have the option to bypass it if speed is very important - $thisfile_midi['totalticks'] = 0; - $info['playtime_seconds'] = 0; - $CurrentMicroSecondsPerBeat = 500000; // 120 beats per minute; 60,000,000 microseconds per minute -> 500,000 microseconds per beat - $CurrentBeatsPerMinute = 120; // 120 beats per minute; 60,000,000 microseconds per minute -> 500,000 microseconds per beat - $MicroSecondsPerQuarterNoteAfter = array (); - - foreach ($trackdataarray as $tracknumber => $trackdata) { - - $eventsoffset = 0; - $LastIssuedMIDIcommand = 0; - $LastIssuedMIDIchannel = 0; - $CumulativeDeltaTime = 0; - $TicksAtCurrentBPM = 0; - while ($eventsoffset < strlen($trackdata)) { - $eventid = 0; - if (isset($MIDIevents[$tracknumber]) && is_array($MIDIevents[$tracknumber])) { - $eventid = count($MIDIevents[$tracknumber]); - } - $deltatime = 0; - for ($i = 0; $i < 4; $i++) { - $deltatimebyte = ord(substr($trackdata, $eventsoffset++, 1)); - $deltatime = ($deltatime << 7) + ($deltatimebyte & 0x7F); - if ($deltatimebyte & 0x80) { - // another byte follows - } else { - break; - } - } - $CumulativeDeltaTime += $deltatime; - $TicksAtCurrentBPM += $deltatime; - $MIDIevents[$tracknumber][$eventid]['deltatime'] = $deltatime; - $MIDI_event_channel = ord(substr($trackdata, $eventsoffset++, 1)); - if ($MIDI_event_channel & 0x80) { - // OK, normal event - MIDI command has MSB set - $LastIssuedMIDIcommand = $MIDI_event_channel >> 4; - $LastIssuedMIDIchannel = $MIDI_event_channel & 0x0F; - } else { - // running event - assume last command - $eventsoffset--; - } - $MIDIevents[$tracknumber][$eventid]['eventid'] = $LastIssuedMIDIcommand; - $MIDIevents[$tracknumber][$eventid]['channel'] = $LastIssuedMIDIchannel; - if ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x08) { // Note off (key is released) - - $notenumber = ord(substr($trackdata, $eventsoffset++, 1)); - $velocity = ord(substr($trackdata, $eventsoffset++, 1)); - - } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x09) { // Note on (key is pressed) - - $notenumber = ord(substr($trackdata, $eventsoffset++, 1)); - $velocity = ord(substr($trackdata, $eventsoffset++, 1)); - - } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0A) { // Key after-touch - - $notenumber = ord(substr($trackdata, $eventsoffset++, 1)); - $velocity = ord(substr($trackdata, $eventsoffset++, 1)); - - } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0B) { // Control Change - - $controllernum = ord(substr($trackdata, $eventsoffset++, 1)); - $newvalue = ord(substr($trackdata, $eventsoffset++, 1)); - - } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0C) { // Program (patch) change - - $newprogramnum = ord(substr($trackdata, $eventsoffset++, 1)); - - $thisfile_midi_raw['track'][$tracknumber]['instrumentid'] = $newprogramnum; - if ($tracknumber == 10) { - $thisfile_midi_raw['track'][$tracknumber]['instrument'] = $this->GeneralMIDIpercussionLookup($newprogramnum); - } else { - $thisfile_midi_raw['track'][$tracknumber]['instrument'] = $this->GeneralMIDIinstrumentLookup($newprogramnum); - } - - } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0D) { // Channel after-touch - - $channelnumber = ord(substr($trackdata, $eventsoffset++, 1)); - - } elseif ($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0E) { // Pitch wheel change (2000H is normal or no change) - - $changeLSB = ord(substr($trackdata, $eventsoffset++, 1)); - $changeMSB = ord(substr($trackdata, $eventsoffset++, 1)); - $pitchwheelchange = (($changeMSB & 0x7F) << 7) & ($changeLSB & 0x7F); - - } elseif (($MIDIevents[$tracknumber][$eventid]['eventid'] == 0x0F) && ($MIDIevents[$tracknumber][$eventid]['channel'] == 0x0F)) { - - $METAeventCommand = ord(substr($trackdata, $eventsoffset++, 1)); - $METAeventLength = ord(substr($trackdata, $eventsoffset++, 1)); - $METAeventData = substr($trackdata, $eventsoffset, $METAeventLength); - $eventsoffset += $METAeventLength; - switch ($METAeventCommand) { - case 0x00: // Set track sequence number - $track_sequence_number = getid3_lib::BigEndian2Int(substr($METAeventData, 0, $METAeventLength)); - //$thisfile_midi_raw['events'][$tracknumber][$eventid]['seqno'] = $track_sequence_number; - break; - - case 0x01: // Text: generic - $text_generic = substr($METAeventData, 0, $METAeventLength); - //$thisfile_midi_raw['events'][$tracknumber][$eventid]['text'] = $text_generic; - $thisfile_midi['comments']['comment'][] = $text_generic; - break; - - case 0x02: // Text: copyright - $text_copyright = substr($METAeventData, 0, $METAeventLength); - //$thisfile_midi_raw['events'][$tracknumber][$eventid]['copyright'] = $text_copyright; - $thisfile_midi['comments']['copyright'][] = $text_copyright; - break; - - case 0x03: // Text: track name - $text_trackname = substr($METAeventData, 0, $METAeventLength); - $thisfile_midi_raw['track'][$tracknumber]['name'] = $text_trackname; - break; - - case 0x04: // Text: track instrument name - $text_instrument = substr($METAeventData, 0, $METAeventLength); - //$thisfile_midi_raw['events'][$tracknumber][$eventid]['instrument'] = $text_instrument; - break; - - case 0x05: // Text: lyrics - $text_lyrics = substr($METAeventData, 0, $METAeventLength); - //$thisfile_midi_raw['events'][$tracknumber][$eventid]['lyrics'] = $text_lyrics; - if (!isset($thisfile_midi['lyrics'])) { - $thisfile_midi['lyrics'] = ''; - } - $thisfile_midi['lyrics'] .= $text_lyrics."\n"; - break; - - case 0x06: // Text: marker - $text_marker = substr($METAeventData, 0, $METAeventLength); - //$thisfile_midi_raw['events'][$tracknumber][$eventid]['marker'] = $text_marker; - break; - - case 0x07: // Text: cue point - $text_cuepoint = substr($METAeventData, 0, $METAeventLength); - //$thisfile_midi_raw['events'][$tracknumber][$eventid]['cuepoint'] = $text_cuepoint; - break; - - case 0x2F: // End Of Track - //$thisfile_midi_raw['events'][$tracknumber][$eventid]['EOT'] = $CumulativeDeltaTime; - break; - - case 0x51: // Tempo: microseconds / quarter note - $CurrentMicroSecondsPerBeat = getid3_lib::BigEndian2Int(substr($METAeventData, 0, $METAeventLength)); - if ($CurrentMicroSecondsPerBeat == 0) { - $info['error'][] = 'Corrupt MIDI file: CurrentMicroSecondsPerBeat == zero'; - return false; - } - $thisfile_midi_raw['events'][$tracknumber][$CumulativeDeltaTime]['us_qnote'] = $CurrentMicroSecondsPerBeat; - $CurrentBeatsPerMinute = (1000000 / $CurrentMicroSecondsPerBeat) * 60; - $MicroSecondsPerQuarterNoteAfter[$CumulativeDeltaTime] = $CurrentMicroSecondsPerBeat; - $TicksAtCurrentBPM = 0; - break; - - case 0x58: // Time signature - $timesig_numerator = getid3_lib::BigEndian2Int($METAeventData{0}); - $timesig_denominator = pow(2, getid3_lib::BigEndian2Int($METAeventData{1})); // $02 -> x/4, $03 -> x/8, etc - $timesig_32inqnote = getid3_lib::BigEndian2Int($METAeventData{2}); // number of 32nd notes to the quarter note - //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_32inqnote'] = $timesig_32inqnote; - //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_numerator'] = $timesig_numerator; - //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_denominator'] = $timesig_denominator; - //$thisfile_midi_raw['events'][$tracknumber][$eventid]['timesig_text'] = $timesig_numerator.'/'.$timesig_denominator; - $thisfile_midi['timesignature'][] = $timesig_numerator.'/'.$timesig_denominator; - break; - - case 0x59: // Keysignature - $keysig_sharpsflats = getid3_lib::BigEndian2Int($METAeventData{0}); - if ($keysig_sharpsflats & 0x80) { - // (-7 -> 7 flats, 0 ->key of C, 7 -> 7 sharps) - $keysig_sharpsflats -= 256; - } - - $keysig_majorminor = getid3_lib::BigEndian2Int($METAeventData{1}); // 0 -> major, 1 -> minor - $keysigs = array(-7=>'Cb', -6=>'Gb', -5=>'Db', -4=>'Ab', -3=>'Eb', -2=>'Bb', -1=>'F', 0=>'C', 1=>'G', 2=>'D', 3=>'A', 4=>'E', 5=>'B', 6=>'F#', 7=>'C#'); - //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_sharps'] = (($keysig_sharpsflats > 0) ? abs($keysig_sharpsflats) : 0); - //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_flats'] = (($keysig_sharpsflats < 0) ? abs($keysig_sharpsflats) : 0); - //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_minor'] = (bool) $keysig_majorminor; - //$thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_text'] = $keysigs[$keysig_sharpsflats].' '.($thisfile_midi_raw['events'][$tracknumber][$eventid]['keysig_minor'] ? 'minor' : 'major'); - - // $keysigs[$keysig_sharpsflats] gets an int key (correct) - $keysigs["$keysig_sharpsflats"] gets a string key (incorrect) - $thisfile_midi['keysignature'][] = $keysigs[$keysig_sharpsflats].' '.((bool) $keysig_majorminor ? 'minor' : 'major'); - break; - - case 0x7F: // Sequencer specific information - $custom_data = substr($METAeventData, 0, $METAeventLength); - break; - - default: - $info['warning'][] = 'Unhandled META Event Command: '.$METAeventCommand; - break; - } - - } else { - - $info['warning'][] = 'Unhandled MIDI Event ID: '.$MIDIevents[$tracknumber][$eventid]['eventid'].' + Channel ID: '.$MIDIevents[$tracknumber][$eventid]['channel']; - - } - } - if (($tracknumber > 0) || (count($trackdataarray) == 1)) { - $thisfile_midi['totalticks'] = max($thisfile_midi['totalticks'], $CumulativeDeltaTime); - } - } - $previoustickoffset = null; - - ksort($MicroSecondsPerQuarterNoteAfter); - foreach ($MicroSecondsPerQuarterNoteAfter as $tickoffset => $microsecondsperbeat) { - if (is_null($previoustickoffset)) { - $prevmicrosecondsperbeat = $microsecondsperbeat; - $previoustickoffset = $tickoffset; - continue; - } - if ($thisfile_midi['totalticks'] > $tickoffset) { - - if ($thisfile_midi_raw['ticksperqnote'] == 0) { - $info['error'][] = 'Corrupt MIDI file: ticksperqnote == zero'; - return false; - } - - $info['playtime_seconds'] += (($tickoffset - $previoustickoffset) / $thisfile_midi_raw['ticksperqnote']) * ($prevmicrosecondsperbeat / 1000000); - - $prevmicrosecondsperbeat = $microsecondsperbeat; - $previoustickoffset = $tickoffset; - } - } - if ($thisfile_midi['totalticks'] > $previoustickoffset) { - - if ($thisfile_midi_raw['ticksperqnote'] == 0) { - $info['error'][] = 'Corrupt MIDI file: ticksperqnote == zero'; - return false; - } - - $info['playtime_seconds'] += (($thisfile_midi['totalticks'] - $previoustickoffset) / $thisfile_midi_raw['ticksperqnote']) * ($microsecondsperbeat / 1000000); - - } - } - - - if (!empty($info['playtime_seconds'])) { - $info['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; - } - - if (!empty($thisfile_midi['lyrics'])) { - $thisfile_midi['comments']['lyrics'][] = $thisfile_midi['lyrics']; - } - - return true; - } - - public function GeneralMIDIinstrumentLookup($instrumentid) { - - $begin = __LINE__; - - /** This is not a comment! - - 0 Acoustic Grand - 1 Bright Acoustic - 2 Electric Grand - 3 Honky-Tonk - 4 Electric Piano 1 - 5 Electric Piano 2 - 6 Harpsichord - 7 Clavier - 8 Celesta - 9 Glockenspiel - 10 Music Box - 11 Vibraphone - 12 Marimba - 13 Xylophone - 14 Tubular Bells - 15 Dulcimer - 16 Drawbar Organ - 17 Percussive Organ - 18 Rock Organ - 19 Church Organ - 20 Reed Organ - 21 Accordian - 22 Harmonica - 23 Tango Accordian - 24 Acoustic Guitar (nylon) - 25 Acoustic Guitar (steel) - 26 Electric Guitar (jazz) - 27 Electric Guitar (clean) - 28 Electric Guitar (muted) - 29 Overdriven Guitar - 30 Distortion Guitar - 31 Guitar Harmonics - 32 Acoustic Bass - 33 Electric Bass (finger) - 34 Electric Bass (pick) - 35 Fretless Bass - 36 Slap Bass 1 - 37 Slap Bass 2 - 38 Synth Bass 1 - 39 Synth Bass 2 - 40 Violin - 41 Viola - 42 Cello - 43 Contrabass - 44 Tremolo Strings - 45 Pizzicato Strings - 46 Orchestral Strings - 47 Timpani - 48 String Ensemble 1 - 49 String Ensemble 2 - 50 SynthStrings 1 - 51 SynthStrings 2 - 52 Choir Aahs - 53 Voice Oohs - 54 Synth Voice - 55 Orchestra Hit - 56 Trumpet - 57 Trombone - 58 Tuba - 59 Muted Trumpet - 60 French Horn - 61 Brass Section - 62 SynthBrass 1 - 63 SynthBrass 2 - 64 Soprano Sax - 65 Alto Sax - 66 Tenor Sax - 67 Baritone Sax - 68 Oboe - 69 English Horn - 70 Bassoon - 71 Clarinet - 72 Piccolo - 73 Flute - 74 Recorder - 75 Pan Flute - 76 Blown Bottle - 77 Shakuhachi - 78 Whistle - 79 Ocarina - 80 Lead 1 (square) - 81 Lead 2 (sawtooth) - 82 Lead 3 (calliope) - 83 Lead 4 (chiff) - 84 Lead 5 (charang) - 85 Lead 6 (voice) - 86 Lead 7 (fifths) - 87 Lead 8 (bass + lead) - 88 Pad 1 (new age) - 89 Pad 2 (warm) - 90 Pad 3 (polysynth) - 91 Pad 4 (choir) - 92 Pad 5 (bowed) - 93 Pad 6 (metallic) - 94 Pad 7 (halo) - 95 Pad 8 (sweep) - 96 FX 1 (rain) - 97 FX 2 (soundtrack) - 98 FX 3 (crystal) - 99 FX 4 (atmosphere) - 100 FX 5 (brightness) - 101 FX 6 (goblins) - 102 FX 7 (echoes) - 103 FX 8 (sci-fi) - 104 Sitar - 105 Banjo - 106 Shamisen - 107 Koto - 108 Kalimba - 109 Bagpipe - 110 Fiddle - 111 Shanai - 112 Tinkle Bell - 113 Agogo - 114 Steel Drums - 115 Woodblock - 116 Taiko Drum - 117 Melodic Tom - 118 Synth Drum - 119 Reverse Cymbal - 120 Guitar Fret Noise - 121 Breath Noise - 122 Seashore - 123 Bird Tweet - 124 Telephone Ring - 125 Helicopter - 126 Applause - 127 Gunshot - - */ - - return getid3_lib::EmbeddedLookup($instrumentid, $begin, __LINE__, __FILE__, 'GeneralMIDIinstrument'); - } - - public function GeneralMIDIpercussionLookup($instrumentid) { - - $begin = __LINE__; - - /** This is not a comment! - - 35 Acoustic Bass Drum - 36 Bass Drum 1 - 37 Side Stick - 38 Acoustic Snare - 39 Hand Clap - 40 Electric Snare - 41 Low Floor Tom - 42 Closed Hi-Hat - 43 High Floor Tom - 44 Pedal Hi-Hat - 45 Low Tom - 46 Open Hi-Hat - 47 Low-Mid Tom - 48 Hi-Mid Tom - 49 Crash Cymbal 1 - 50 High Tom - 51 Ride Cymbal 1 - 52 Chinese Cymbal - 53 Ride Bell - 54 Tambourine - 55 Splash Cymbal - 56 Cowbell - 57 Crash Cymbal 2 - 59 Ride Cymbal 2 - 60 Hi Bongo - 61 Low Bongo - 62 Mute Hi Conga - 63 Open Hi Conga - 64 Low Conga - 65 High Timbale - 66 Low Timbale - 67 High Agogo - 68 Low Agogo - 69 Cabasa - 70 Maracas - 71 Short Whistle - 72 Long Whistle - 73 Short Guiro - 74 Long Guiro - 75 Claves - 76 Hi Wood Block - 77 Low Wood Block - 78 Mute Cuica - 79 Open Cuica - 80 Mute Triangle - 81 Open Triangle - - */ - - return getid3_lib::EmbeddedLookup($instrumentid, $begin, __LINE__, __FILE__, 'GeneralMIDIpercussion'); - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.mod.php b/src/Classes/Vendor/getid3/module.audio.mod.php deleted file mode 100755 index 3bed8586c..000000000 --- a/src/Classes/Vendor/getid3/module.audio.mod.php +++ /dev/null @@ -1,98 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.mod.php // -// module for analyzing MOD Audio files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_mod extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $fileheader = fread($this->getid3->fp, 1088); - if (preg_match('#^IMPM#', $fileheader)) { - return $this->getITheaderFilepointer(); - } elseif (preg_match('#^Extended Module#', $fileheader)) { - return $this->getXMheaderFilepointer(); - } elseif (preg_match('#^.{44}SCRM#', $fileheader)) { - return $this->getS3MheaderFilepointer(); - } elseif (preg_match('#^.{1080}(M\\.K\\.|M!K!|FLT4|FLT8|[5-9]CHN|[1-3][0-9]CH)#', $fileheader)) { - return $this->getMODheaderFilepointer(); - } - $info['error'][] = 'This is not a known type of MOD file'; - return false; - } - - - public function getMODheaderFilepointer() { - $info = &$this->getid3->info; - fseek($this->getid3->fp, $info['avdataoffset'] + 1080); - $FormatID = fread($this->getid3->fp, 4); - if (!preg_match('#^(M.K.|[5-9]CHN|[1-3][0-9]CH)$#', $FormatID)) { - $info['error'][] = 'This is not a known type of MOD file'; - return false; - } - - $info['fileformat'] = 'mod'; - - $info['error'][] = 'MOD parsing not enabled in this version of getID3() ['.$this->getid3->version().']'; - return false; - } - - public function getXMheaderFilepointer() { - $info = &$this->getid3->info; - fseek($this->getid3->fp, $info['avdataoffset']); - $FormatID = fread($this->getid3->fp, 15); - if (!preg_match('#^Extended Module$#', $FormatID)) { - $info['error'][] = 'This is not a known type of XM-MOD file'; - return false; - } - - $info['fileformat'] = 'xm'; - - $info['error'][] = 'XM-MOD parsing not enabled in this version of getID3() ['.$this->getid3->version().']'; - return false; - } - - public function getS3MheaderFilepointer() { - $info = &$this->getid3->info; - fseek($this->getid3->fp, $info['avdataoffset'] + 44); - $FormatID = fread($this->getid3->fp, 4); - if (!preg_match('#^SCRM$#', $FormatID)) { - $info['error'][] = 'This is not a ScreamTracker MOD file'; - return false; - } - - $info['fileformat'] = 's3m'; - - $info['error'][] = 'ScreamTracker parsing not enabled in this version of getID3() ['.$this->getid3->version().']'; - return false; - } - - public function getITheaderFilepointer() { - $info = &$this->getid3->info; - fseek($this->getid3->fp, $info['avdataoffset']); - $FormatID = fread($this->getid3->fp, 4); - if (!preg_match('#^IMPM$#', $FormatID)) { - $info['error'][] = 'This is not an ImpulseTracker MOD file'; - return false; - } - - $info['fileformat'] = 'it'; - - $info['error'][] = 'ImpulseTracker parsing not enabled in this version of getID3() ['.$this->getid3->version().']'; - return false; - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.monkey.php b/src/Classes/Vendor/getid3/module.audio.monkey.php deleted file mode 100755 index 9edba5d85..000000000 --- a/src/Classes/Vendor/getid3/module.audio.monkey.php +++ /dev/null @@ -1,203 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.monkey.php // -// module for analyzing Monkey's Audio files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_monkey extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - // based loosely on code from TMonkey by Jurgen Faul - // http://jfaul.de/atl or http://j-faul.virtualave.net/atl/atl.html - - $info['fileformat'] = 'mac'; - $info['audio']['dataformat'] = 'mac'; - $info['audio']['bitrate_mode'] = 'vbr'; - $info['audio']['lossless'] = true; - - $info['monkeys_audio']['raw'] = array(); - $thisfile_monkeysaudio = &$info['monkeys_audio']; - $thisfile_monkeysaudio_raw = &$thisfile_monkeysaudio['raw']; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $MACheaderData = fread($this->getid3->fp, 74); - - $thisfile_monkeysaudio_raw['magic'] = substr($MACheaderData, 0, 4); - $magic = 'MAC '; - if ($thisfile_monkeysaudio_raw['magic'] != $magic) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($thisfile_monkeysaudio_raw['magic']).'"'; - unset($info['fileformat']); - return false; - } - $thisfile_monkeysaudio_raw['nVersion'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, 4, 2)); // appears to be uint32 in 3.98+ - - if ($thisfile_monkeysaudio_raw['nVersion'] < 3980) { - $thisfile_monkeysaudio_raw['nCompressionLevel'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, 6, 2)); - $thisfile_monkeysaudio_raw['nFormatFlags'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, 8, 2)); - $thisfile_monkeysaudio_raw['nChannels'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, 10, 2)); - $thisfile_monkeysaudio_raw['nSampleRate'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, 12, 4)); - $thisfile_monkeysaudio_raw['nHeaderDataBytes'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, 16, 4)); - $thisfile_monkeysaudio_raw['nWAVTerminatingBytes'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, 20, 4)); - $thisfile_monkeysaudio_raw['nTotalFrames'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, 24, 4)); - $thisfile_monkeysaudio_raw['nFinalFrameSamples'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, 28, 4)); - $thisfile_monkeysaudio_raw['nPeakLevel'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, 32, 4)); - $thisfile_monkeysaudio_raw['nSeekElements'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, 38, 2)); - $offset = 8; - } else { - $offset = 8; - // APE_DESCRIPTOR - $thisfile_monkeysaudio_raw['nDescriptorBytes'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, $offset, 4)); - $offset += 4; - $thisfile_monkeysaudio_raw['nHeaderBytes'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, $offset, 4)); - $offset += 4; - $thisfile_monkeysaudio_raw['nSeekTableBytes'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, $offset, 4)); - $offset += 4; - $thisfile_monkeysaudio_raw['nHeaderDataBytes'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, $offset, 4)); - $offset += 4; - $thisfile_monkeysaudio_raw['nAPEFrameDataBytes'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, $offset, 4)); - $offset += 4; - $thisfile_monkeysaudio_raw['nAPEFrameDataBytesHigh'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, $offset, 4)); - $offset += 4; - $thisfile_monkeysaudio_raw['nTerminatingDataBytes'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, $offset, 4)); - $offset += 4; - $thisfile_monkeysaudio_raw['cFileMD5'] = substr($MACheaderData, $offset, 16); - $offset += 16; - - // APE_HEADER - $thisfile_monkeysaudio_raw['nCompressionLevel'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, $offset, 2)); - $offset += 2; - $thisfile_monkeysaudio_raw['nFormatFlags'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, $offset, 2)); - $offset += 2; - $thisfile_monkeysaudio_raw['nBlocksPerFrame'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, $offset, 4)); - $offset += 4; - $thisfile_monkeysaudio_raw['nFinalFrameBlocks'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, $offset, 4)); - $offset += 4; - $thisfile_monkeysaudio_raw['nTotalFrames'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, $offset, 4)); - $offset += 4; - $thisfile_monkeysaudio_raw['nBitsPerSample'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, $offset, 2)); - $offset += 2; - $thisfile_monkeysaudio_raw['nChannels'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, $offset, 2)); - $offset += 2; - $thisfile_monkeysaudio_raw['nSampleRate'] = getid3_lib::LittleEndian2Int(substr($MACheaderData, $offset, 4)); - $offset += 4; - } - - $thisfile_monkeysaudio['flags']['8-bit'] = (bool) ($thisfile_monkeysaudio_raw['nFormatFlags'] & 0x0001); - $thisfile_monkeysaudio['flags']['crc-32'] = (bool) ($thisfile_monkeysaudio_raw['nFormatFlags'] & 0x0002); - $thisfile_monkeysaudio['flags']['peak_level'] = (bool) ($thisfile_monkeysaudio_raw['nFormatFlags'] & 0x0004); - $thisfile_monkeysaudio['flags']['24-bit'] = (bool) ($thisfile_monkeysaudio_raw['nFormatFlags'] & 0x0008); - $thisfile_monkeysaudio['flags']['seek_elements'] = (bool) ($thisfile_monkeysaudio_raw['nFormatFlags'] & 0x0010); - $thisfile_monkeysaudio['flags']['no_wav_header'] = (bool) ($thisfile_monkeysaudio_raw['nFormatFlags'] & 0x0020); - $thisfile_monkeysaudio['version'] = $thisfile_monkeysaudio_raw['nVersion'] / 1000; - $thisfile_monkeysaudio['compression'] = $this->MonkeyCompressionLevelNameLookup($thisfile_monkeysaudio_raw['nCompressionLevel']); - if ($thisfile_monkeysaudio_raw['nVersion'] < 3980) { - $thisfile_monkeysaudio['samples_per_frame'] = $this->MonkeySamplesPerFrame($thisfile_monkeysaudio_raw['nVersion'], $thisfile_monkeysaudio_raw['nCompressionLevel']); - } - $thisfile_monkeysaudio['bits_per_sample'] = ($thisfile_monkeysaudio['flags']['24-bit'] ? 24 : ($thisfile_monkeysaudio['flags']['8-bit'] ? 8 : 16)); - $thisfile_monkeysaudio['channels'] = $thisfile_monkeysaudio_raw['nChannels']; - $info['audio']['channels'] = $thisfile_monkeysaudio['channels']; - $thisfile_monkeysaudio['sample_rate'] = $thisfile_monkeysaudio_raw['nSampleRate']; - if ($thisfile_monkeysaudio['sample_rate'] == 0) { - $info['error'][] = 'Corrupt MAC file: frequency == zero'; - return false; - } - $info['audio']['sample_rate'] = $thisfile_monkeysaudio['sample_rate']; - if ($thisfile_monkeysaudio['flags']['peak_level']) { - $thisfile_monkeysaudio['peak_level'] = $thisfile_monkeysaudio_raw['nPeakLevel']; - $thisfile_monkeysaudio['peak_ratio'] = $thisfile_monkeysaudio['peak_level'] / pow(2, $thisfile_monkeysaudio['bits_per_sample'] - 1); - } - if ($thisfile_monkeysaudio_raw['nVersion'] >= 3980) { - $thisfile_monkeysaudio['samples'] = (($thisfile_monkeysaudio_raw['nTotalFrames'] - 1) * $thisfile_monkeysaudio_raw['nBlocksPerFrame']) + $thisfile_monkeysaudio_raw['nFinalFrameBlocks']; - } else { - $thisfile_monkeysaudio['samples'] = (($thisfile_monkeysaudio_raw['nTotalFrames'] - 1) * $thisfile_monkeysaudio['samples_per_frame']) + $thisfile_monkeysaudio_raw['nFinalFrameSamples']; - } - $thisfile_monkeysaudio['playtime'] = $thisfile_monkeysaudio['samples'] / $thisfile_monkeysaudio['sample_rate']; - if ($thisfile_monkeysaudio['playtime'] == 0) { - $info['error'][] = 'Corrupt MAC file: playtime == zero'; - return false; - } - $info['playtime_seconds'] = $thisfile_monkeysaudio['playtime']; - $thisfile_monkeysaudio['compressed_size'] = $info['avdataend'] - $info['avdataoffset']; - $thisfile_monkeysaudio['uncompressed_size'] = $thisfile_monkeysaudio['samples'] * $thisfile_monkeysaudio['channels'] * ($thisfile_monkeysaudio['bits_per_sample'] / 8); - if ($thisfile_monkeysaudio['uncompressed_size'] == 0) { - $info['error'][] = 'Corrupt MAC file: uncompressed_size == zero'; - return false; - } - $thisfile_monkeysaudio['compression_ratio'] = $thisfile_monkeysaudio['compressed_size'] / ($thisfile_monkeysaudio['uncompressed_size'] + $thisfile_monkeysaudio_raw['nHeaderDataBytes']); - $thisfile_monkeysaudio['bitrate'] = (($thisfile_monkeysaudio['samples'] * $thisfile_monkeysaudio['channels'] * $thisfile_monkeysaudio['bits_per_sample']) / $thisfile_monkeysaudio['playtime']) * $thisfile_monkeysaudio['compression_ratio']; - $info['audio']['bitrate'] = $thisfile_monkeysaudio['bitrate']; - - // add size of MAC header to avdataoffset - if ($thisfile_monkeysaudio_raw['nVersion'] >= 3980) { - $info['avdataoffset'] += $thisfile_monkeysaudio_raw['nDescriptorBytes']; - $info['avdataoffset'] += $thisfile_monkeysaudio_raw['nHeaderBytes']; - $info['avdataoffset'] += $thisfile_monkeysaudio_raw['nSeekTableBytes']; - $info['avdataoffset'] += $thisfile_monkeysaudio_raw['nHeaderDataBytes']; - - $info['avdataend'] -= $thisfile_monkeysaudio_raw['nTerminatingDataBytes']; - } else { - $info['avdataoffset'] += $offset; - } - - if ($thisfile_monkeysaudio_raw['nVersion'] >= 3980) { - if ($thisfile_monkeysaudio_raw['cFileMD5'] === str_repeat("\x00", 16)) { - //$info['warning'][] = 'cFileMD5 is null'; - } else { - $info['md5_data_source'] = ''; - $md5 = $thisfile_monkeysaudio_raw['cFileMD5']; - for ($i = 0; $i < strlen($md5); $i++) { - $info['md5_data_source'] .= str_pad(dechex(ord($md5{$i})), 2, '00', STR_PAD_LEFT); - } - if (!preg_match('/^[0-9a-f]{32}$/', $info['md5_data_source'])) { - unset($info['md5_data_source']); - } - } - } - - - - $info['audio']['bits_per_sample'] = $thisfile_monkeysaudio['bits_per_sample']; - $info['audio']['encoder'] = 'MAC v'.number_format($thisfile_monkeysaudio['version'], 2); - $info['audio']['encoder_options'] = ucfirst($thisfile_monkeysaudio['compression']).' compression'; - - return true; - } - - public function MonkeyCompressionLevelNameLookup($compressionlevel) { - static $MonkeyCompressionLevelNameLookup = array( - 0 => 'unknown', - 1000 => 'fast', - 2000 => 'normal', - 3000 => 'high', - 4000 => 'extra-high', - 5000 => 'insane' - ); - return (isset($MonkeyCompressionLevelNameLookup[$compressionlevel]) ? $MonkeyCompressionLevelNameLookup[$compressionlevel] : 'invalid'); - } - - public function MonkeySamplesPerFrame($versionid, $compressionlevel) { - if ($versionid >= 3950) { - return 73728 * 4; - } elseif ($versionid >= 3900) { - return 73728; - } elseif (($versionid >= 3800) && ($compressionlevel == 4000)) { - return 73728; - } else { - return 9216; - } - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.mp3.php b/src/Classes/Vendor/getid3/module.audio.mp3.php deleted file mode 100755 index e6ffea947..000000000 --- a/src/Classes/Vendor/getid3/module.audio.mp3.php +++ /dev/null @@ -1,2009 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.mp3.php // -// module for analyzing MP3 files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -// number of frames to scan to determine if MPEG-audio sequence is valid -// Lower this number to 5-20 for faster scanning -// Increase this number to 50+ for most accurate detection of valid VBR/CBR -// mpeg-audio streams -define('GETID3_MP3_VALID_CHECK_FRAMES', 35); - - -class getid3_mp3 extends getid3_handler -{ - - public $allow_bruteforce = false; // forces getID3() to scan the file byte-by-byte and log all the valid audio frame headers - extremely slow, unrecommended, but may provide data from otherwise-unusuable files - - public function Analyze() { - $info = &$this->getid3->info; - - $initialOffset = $info['avdataoffset']; - - if (!$this->getOnlyMPEGaudioInfo($info['avdataoffset'])) { - if ($this->allow_bruteforce) { - $info['error'][] = 'Rescanning file in BruteForce mode'; - $this->getOnlyMPEGaudioInfoBruteForce($this->getid3->fp, $info); - } - } - - - if (isset($info['mpeg']['audio']['bitrate_mode'])) { - $info['audio']['bitrate_mode'] = strtolower($info['mpeg']['audio']['bitrate_mode']); - } - - if (((isset($info['id3v2']['headerlength']) && ($info['avdataoffset'] > $info['id3v2']['headerlength'])) || (!isset($info['id3v2']) && ($info['avdataoffset'] > 0) && ($info['avdataoffset'] != $initialOffset)))) { - - $synchoffsetwarning = 'Unknown data before synch '; - if (isset($info['id3v2']['headerlength'])) { - $synchoffsetwarning .= '(ID3v2 header ends at '.$info['id3v2']['headerlength'].', then '.($info['avdataoffset'] - $info['id3v2']['headerlength']).' bytes garbage, '; - } elseif ($initialOffset > 0) { - $synchoffsetwarning .= '(should be at '.$initialOffset.', '; - } else { - $synchoffsetwarning .= '(should be at beginning of file, '; - } - $synchoffsetwarning .= 'synch detected at '.$info['avdataoffset'].')'; - if (isset($info['audio']['bitrate_mode']) && ($info['audio']['bitrate_mode'] == 'cbr')) { - - if (!empty($info['id3v2']['headerlength']) && (($info['avdataoffset'] - $info['id3v2']['headerlength']) == $info['mpeg']['audio']['framelength'])) { - - $synchoffsetwarning .= '. This is a known problem with some versions of LAME (3.90-3.92) DLL in CBR mode.'; - $info['audio']['codec'] = 'LAME'; - $CurrentDataLAMEversionString = 'LAME3.'; - - } elseif (empty($info['id3v2']['headerlength']) && ($info['avdataoffset'] == $info['mpeg']['audio']['framelength'])) { - - $synchoffsetwarning .= '. This is a known problem with some versions of LAME (3.90 - 3.92) DLL in CBR mode.'; - $info['audio']['codec'] = 'LAME'; - $CurrentDataLAMEversionString = 'LAME3.'; - - } - - } - $info['warning'][] = $synchoffsetwarning; - - } - - if (isset($info['mpeg']['audio']['LAME'])) { - $info['audio']['codec'] = 'LAME'; - if (!empty($info['mpeg']['audio']['LAME']['long_version'])) { - $info['audio']['encoder'] = rtrim($info['mpeg']['audio']['LAME']['long_version'], "\x00"); - } elseif (!empty($info['mpeg']['audio']['LAME']['short_version'])) { - $info['audio']['encoder'] = rtrim($info['mpeg']['audio']['LAME']['short_version'], "\x00"); - } - } - - $CurrentDataLAMEversionString = (!empty($CurrentDataLAMEversionString) ? $CurrentDataLAMEversionString : (isset($info['audio']['encoder']) ? $info['audio']['encoder'] : '')); - if (!empty($CurrentDataLAMEversionString) && (substr($CurrentDataLAMEversionString, 0, 6) == 'LAME3.') && !preg_match('[0-9\)]', substr($CurrentDataLAMEversionString, -1))) { - // a version number of LAME that does not end with a number like "LAME3.92" - // or with a closing parenthesis like "LAME3.88 (alpha)" - // or a version of LAME with the LAMEtag-not-filled-in-DLL-mode bug (3.90-3.92) - - // not sure what the actual last frame length will be, but will be less than or equal to 1441 - $PossiblyLongerLAMEversion_FrameLength = 1441; - - // Not sure what version of LAME this is - look in padding of last frame for longer version string - $PossibleLAMEversionStringOffset = $info['avdataend'] - $PossiblyLongerLAMEversion_FrameLength; - fseek($this->getid3->fp, $PossibleLAMEversionStringOffset); - $PossiblyLongerLAMEversion_Data = fread($this->getid3->fp, $PossiblyLongerLAMEversion_FrameLength); - switch (substr($CurrentDataLAMEversionString, -1)) { - case 'a': - case 'b': - // "LAME3.94a" will have a longer version string of "LAME3.94 (alpha)" for example - // need to trim off "a" to match longer string - $CurrentDataLAMEversionString = substr($CurrentDataLAMEversionString, 0, -1); - break; - } - if (($PossiblyLongerLAMEversion_String = strstr($PossiblyLongerLAMEversion_Data, $CurrentDataLAMEversionString)) !== false) { - if (substr($PossiblyLongerLAMEversion_String, 0, strlen($CurrentDataLAMEversionString)) == $CurrentDataLAMEversionString) { - $PossiblyLongerLAMEversion_NewString = substr($PossiblyLongerLAMEversion_String, 0, strspn($PossiblyLongerLAMEversion_String, 'LAME0123456789., (abcdefghijklmnopqrstuvwxyzJFSOND)')); //"LAME3.90.3" "LAME3.87 (beta 1, Sep 27 2000)" "LAME3.88 (beta)" - if (empty($info['audio']['encoder']) || (strlen($PossiblyLongerLAMEversion_NewString) > strlen($info['audio']['encoder']))) { - $info['audio']['encoder'] = $PossiblyLongerLAMEversion_NewString; - } - } - } - } - if (!empty($info['audio']['encoder'])) { - $info['audio']['encoder'] = rtrim($info['audio']['encoder'], "\x00 "); - } - - switch (isset($info['mpeg']['audio']['layer']) ? $info['mpeg']['audio']['layer'] : '') { - case 1: - case 2: - $info['audio']['dataformat'] = 'mp'.$info['mpeg']['audio']['layer']; - break; - } - if (isset($info['fileformat']) && ($info['fileformat'] == 'mp3')) { - switch ($info['audio']['dataformat']) { - case 'mp1': - case 'mp2': - case 'mp3': - $info['fileformat'] = $info['audio']['dataformat']; - break; - - default: - $info['warning'][] = 'Expecting [audio][dataformat] to be mp1/mp2/mp3 when fileformat == mp3, [audio][dataformat] actually "'.$info['audio']['dataformat'].'"'; - break; - } - } - - if (empty($info['fileformat'])) { - unset($info['fileformat']); - unset($info['audio']['bitrate_mode']); - unset($info['avdataoffset']); - unset($info['avdataend']); - return false; - } - - $info['mime_type'] = 'audio/mpeg'; - $info['audio']['lossless'] = false; - - // Calculate playtime - if (!isset($info['playtime_seconds']) && isset($info['audio']['bitrate']) && ($info['audio']['bitrate'] > 0)) { - $info['playtime_seconds'] = ($info['avdataend'] - $info['avdataoffset']) * 8 / $info['audio']['bitrate']; - } - - $info['audio']['encoder_options'] = $this->GuessEncoderOptions(); - - return true; - } - - - public function GuessEncoderOptions() { - // shortcuts - $info = &$this->getid3->info; - if (!empty($info['mpeg']['audio'])) { - $thisfile_mpeg_audio = &$info['mpeg']['audio']; - if (!empty($thisfile_mpeg_audio['LAME'])) { - $thisfile_mpeg_audio_lame = &$thisfile_mpeg_audio['LAME']; - } - } - - $encoder_options = ''; - static $NamedPresetBitrates = array(16, 24, 40, 56, 112, 128, 160, 192, 256); - - if (isset($thisfile_mpeg_audio['VBR_method']) && ($thisfile_mpeg_audio['VBR_method'] == 'Fraunhofer') && !empty($thisfile_mpeg_audio['VBR_quality'])) { - - $encoder_options = 'VBR q'.$thisfile_mpeg_audio['VBR_quality']; - - } elseif (!empty($thisfile_mpeg_audio_lame['preset_used']) && (!in_array($thisfile_mpeg_audio_lame['preset_used_id'], $NamedPresetBitrates))) { - - $encoder_options = $thisfile_mpeg_audio_lame['preset_used']; - - } elseif (!empty($thisfile_mpeg_audio_lame['vbr_quality'])) { - - static $KnownEncoderValues = array(); - if (empty($KnownEncoderValues)) { - - //$KnownEncoderValues[abrbitrate_minbitrate][vbr_quality][raw_vbr_method][raw_noise_shaping][raw_stereo_mode][ath_type][lowpass_frequency] = 'preset name'; - $KnownEncoderValues[0xFF][58][1][1][3][2][20500] = '--alt-preset insane'; // 3.90, 3.90.1, 3.92 - $KnownEncoderValues[0xFF][58][1][1][3][2][20600] = '--alt-preset insane'; // 3.90.2, 3.90.3, 3.91 - $KnownEncoderValues[0xFF][57][1][1][3][4][20500] = '--alt-preset insane'; // 3.94, 3.95 - $KnownEncoderValues['**'][78][3][2][3][2][19500] = '--alt-preset extreme'; // 3.90, 3.90.1, 3.92 - $KnownEncoderValues['**'][78][3][2][3][2][19600] = '--alt-preset extreme'; // 3.90.2, 3.91 - $KnownEncoderValues['**'][78][3][1][3][2][19600] = '--alt-preset extreme'; // 3.90.3 - $KnownEncoderValues['**'][78][4][2][3][2][19500] = '--alt-preset fast extreme'; // 3.90, 3.90.1, 3.92 - $KnownEncoderValues['**'][78][4][2][3][2][19600] = '--alt-preset fast extreme'; // 3.90.2, 3.90.3, 3.91 - $KnownEncoderValues['**'][78][3][2][3][4][19000] = '--alt-preset standard'; // 3.90, 3.90.1, 3.90.2, 3.91, 3.92 - $KnownEncoderValues['**'][78][3][1][3][4][19000] = '--alt-preset standard'; // 3.90.3 - $KnownEncoderValues['**'][78][4][2][3][4][19000] = '--alt-preset fast standard'; // 3.90, 3.90.1, 3.90.2, 3.91, 3.92 - $KnownEncoderValues['**'][78][4][1][3][4][19000] = '--alt-preset fast standard'; // 3.90.3 - $KnownEncoderValues['**'][88][4][1][3][3][19500] = '--r3mix'; // 3.90, 3.90.1, 3.92 - $KnownEncoderValues['**'][88][4][1][3][3][19600] = '--r3mix'; // 3.90.2, 3.90.3, 3.91 - $KnownEncoderValues['**'][67][4][1][3][4][18000] = '--r3mix'; // 3.94, 3.95 - $KnownEncoderValues['**'][68][3][2][3][4][18000] = '--alt-preset medium'; // 3.90.3 - $KnownEncoderValues['**'][68][4][2][3][4][18000] = '--alt-preset fast medium'; // 3.90.3 - - $KnownEncoderValues[0xFF][99][1][1][1][2][0] = '--preset studio'; // 3.90, 3.90.1, 3.90.2, 3.91, 3.92 - $KnownEncoderValues[0xFF][58][2][1][3][2][20600] = '--preset studio'; // 3.90.3, 3.93.1 - $KnownEncoderValues[0xFF][58][2][1][3][2][20500] = '--preset studio'; // 3.93 - $KnownEncoderValues[0xFF][57][2][1][3][4][20500] = '--preset studio'; // 3.94, 3.95 - $KnownEncoderValues[0xC0][88][1][1][1][2][0] = '--preset cd'; // 3.90, 3.90.1, 3.90.2, 3.91, 3.92 - $KnownEncoderValues[0xC0][58][2][2][3][2][19600] = '--preset cd'; // 3.90.3, 3.93.1 - $KnownEncoderValues[0xC0][58][2][2][3][2][19500] = '--preset cd'; // 3.93 - $KnownEncoderValues[0xC0][57][2][1][3][4][19500] = '--preset cd'; // 3.94, 3.95 - $KnownEncoderValues[0xA0][78][1][1][3][2][18000] = '--preset hifi'; // 3.90, 3.90.1, 3.90.2, 3.91, 3.92 - $KnownEncoderValues[0xA0][58][2][2][3][2][18000] = '--preset hifi'; // 3.90.3, 3.93, 3.93.1 - $KnownEncoderValues[0xA0][57][2][1][3][4][18000] = '--preset hifi'; // 3.94, 3.95 - $KnownEncoderValues[0x80][67][1][1][3][2][18000] = '--preset tape'; // 3.90, 3.90.1, 3.90.2, 3.91, 3.92 - $KnownEncoderValues[0x80][67][1][1][3][2][15000] = '--preset radio'; // 3.90, 3.90.1, 3.90.2, 3.91, 3.92 - $KnownEncoderValues[0x70][67][1][1][3][2][15000] = '--preset fm'; // 3.90, 3.90.1, 3.90.2, 3.91, 3.92 - $KnownEncoderValues[0x70][58][2][2][3][2][16000] = '--preset tape/radio/fm'; // 3.90.3, 3.93, 3.93.1 - $KnownEncoderValues[0x70][57][2][1][3][4][16000] = '--preset tape/radio/fm'; // 3.94, 3.95 - $KnownEncoderValues[0x38][58][2][2][0][2][10000] = '--preset voice'; // 3.90.3, 3.93, 3.93.1 - $KnownEncoderValues[0x38][57][2][1][0][4][15000] = '--preset voice'; // 3.94, 3.95 - $KnownEncoderValues[0x38][57][2][1][0][4][16000] = '--preset voice'; // 3.94a14 - $KnownEncoderValues[0x28][65][1][1][0][2][7500] = '--preset mw-us'; // 3.90, 3.90.1, 3.92 - $KnownEncoderValues[0x28][65][1][1][0][2][7600] = '--preset mw-us'; // 3.90.2, 3.91 - $KnownEncoderValues[0x28][58][2][2][0][2][7000] = '--preset mw-us'; // 3.90.3, 3.93, 3.93.1 - $KnownEncoderValues[0x28][57][2][1][0][4][10500] = '--preset mw-us'; // 3.94, 3.95 - $KnownEncoderValues[0x28][57][2][1][0][4][11200] = '--preset mw-us'; // 3.94a14 - $KnownEncoderValues[0x28][57][2][1][0][4][8800] = '--preset mw-us'; // 3.94a15 - $KnownEncoderValues[0x18][58][2][2][0][2][4000] = '--preset phon+/lw/mw-eu/sw'; // 3.90.3, 3.93.1 - $KnownEncoderValues[0x18][58][2][2][0][2][3900] = '--preset phon+/lw/mw-eu/sw'; // 3.93 - $KnownEncoderValues[0x18][57][2][1][0][4][5900] = '--preset phon+/lw/mw-eu/sw'; // 3.94, 3.95 - $KnownEncoderValues[0x18][57][2][1][0][4][6200] = '--preset phon+/lw/mw-eu/sw'; // 3.94a14 - $KnownEncoderValues[0x18][57][2][1][0][4][3200] = '--preset phon+/lw/mw-eu/sw'; // 3.94a15 - $KnownEncoderValues[0x10][58][2][2][0][2][3800] = '--preset phone'; // 3.90.3, 3.93.1 - $KnownEncoderValues[0x10][58][2][2][0][2][3700] = '--preset phone'; // 3.93 - $KnownEncoderValues[0x10][57][2][1][0][4][5600] = '--preset phone'; // 3.94, 3.95 - } - - if (isset($KnownEncoderValues[$thisfile_mpeg_audio_lame['raw']['abrbitrate_minbitrate']][$thisfile_mpeg_audio_lame['vbr_quality']][$thisfile_mpeg_audio_lame['raw']['vbr_method']][$thisfile_mpeg_audio_lame['raw']['noise_shaping']][$thisfile_mpeg_audio_lame['raw']['stereo_mode']][$thisfile_mpeg_audio_lame['ath_type']][$thisfile_mpeg_audio_lame['lowpass_frequency']])) { - - $encoder_options = $KnownEncoderValues[$thisfile_mpeg_audio_lame['raw']['abrbitrate_minbitrate']][$thisfile_mpeg_audio_lame['vbr_quality']][$thisfile_mpeg_audio_lame['raw']['vbr_method']][$thisfile_mpeg_audio_lame['raw']['noise_shaping']][$thisfile_mpeg_audio_lame['raw']['stereo_mode']][$thisfile_mpeg_audio_lame['ath_type']][$thisfile_mpeg_audio_lame['lowpass_frequency']]; - - } elseif (isset($KnownEncoderValues['**'][$thisfile_mpeg_audio_lame['vbr_quality']][$thisfile_mpeg_audio_lame['raw']['vbr_method']][$thisfile_mpeg_audio_lame['raw']['noise_shaping']][$thisfile_mpeg_audio_lame['raw']['stereo_mode']][$thisfile_mpeg_audio_lame['ath_type']][$thisfile_mpeg_audio_lame['lowpass_frequency']])) { - - $encoder_options = $KnownEncoderValues['**'][$thisfile_mpeg_audio_lame['vbr_quality']][$thisfile_mpeg_audio_lame['raw']['vbr_method']][$thisfile_mpeg_audio_lame['raw']['noise_shaping']][$thisfile_mpeg_audio_lame['raw']['stereo_mode']][$thisfile_mpeg_audio_lame['ath_type']][$thisfile_mpeg_audio_lame['lowpass_frequency']]; - - } elseif ($info['audio']['bitrate_mode'] == 'vbr') { - - // http://gabriel.mp3-tech.org/mp3infotag.html - // int Quality = (100 - 10 * gfp->VBR_q - gfp->quality)h - - - $LAME_V_value = 10 - ceil($thisfile_mpeg_audio_lame['vbr_quality'] / 10); - $LAME_q_value = 100 - $thisfile_mpeg_audio_lame['vbr_quality'] - ($LAME_V_value * 10); - $encoder_options = '-V'.$LAME_V_value.' -q'.$LAME_q_value; - - } elseif ($info['audio']['bitrate_mode'] == 'cbr') { - - $encoder_options = strtoupper($info['audio']['bitrate_mode']).ceil($info['audio']['bitrate'] / 1000); - - } else { - - $encoder_options = strtoupper($info['audio']['bitrate_mode']); - - } - - } elseif (!empty($thisfile_mpeg_audio_lame['bitrate_abr'])) { - - $encoder_options = 'ABR'.$thisfile_mpeg_audio_lame['bitrate_abr']; - - } elseif (!empty($info['audio']['bitrate'])) { - - if ($info['audio']['bitrate_mode'] == 'cbr') { - $encoder_options = strtoupper($info['audio']['bitrate_mode']).ceil($info['audio']['bitrate'] / 1000); - } else { - $encoder_options = strtoupper($info['audio']['bitrate_mode']); - } - - } - if (!empty($thisfile_mpeg_audio_lame['bitrate_min'])) { - $encoder_options .= ' -b'.$thisfile_mpeg_audio_lame['bitrate_min']; - } - - if (!empty($thisfile_mpeg_audio_lame['encoding_flags']['nogap_prev']) || !empty($thisfile_mpeg_audio_lame['encoding_flags']['nogap_next'])) { - $encoder_options .= ' --nogap'; - } - - if (!empty($thisfile_mpeg_audio_lame['lowpass_frequency'])) { - $ExplodedOptions = explode(' ', $encoder_options, 4); - if ($ExplodedOptions[0] == '--r3mix') { - $ExplodedOptions[1] = 'r3mix'; - } - switch ($ExplodedOptions[0]) { - case '--preset': - case '--alt-preset': - case '--r3mix': - if ($ExplodedOptions[1] == 'fast') { - $ExplodedOptions[1] .= ' '.$ExplodedOptions[2]; - } - switch ($ExplodedOptions[1]) { - case 'portable': - case 'medium': - case 'standard': - case 'extreme': - case 'insane': - case 'fast portable': - case 'fast medium': - case 'fast standard': - case 'fast extreme': - case 'fast insane': - case 'r3mix': - static $ExpectedLowpass = array( - 'insane|20500' => 20500, - 'insane|20600' => 20600, // 3.90.2, 3.90.3, 3.91 - 'medium|18000' => 18000, - 'fast medium|18000' => 18000, - 'extreme|19500' => 19500, // 3.90, 3.90.1, 3.92, 3.95 - 'extreme|19600' => 19600, // 3.90.2, 3.90.3, 3.91, 3.93.1 - 'fast extreme|19500' => 19500, // 3.90, 3.90.1, 3.92, 3.95 - 'fast extreme|19600' => 19600, // 3.90.2, 3.90.3, 3.91, 3.93.1 - 'standard|19000' => 19000, - 'fast standard|19000' => 19000, - 'r3mix|19500' => 19500, // 3.90, 3.90.1, 3.92 - 'r3mix|19600' => 19600, // 3.90.2, 3.90.3, 3.91 - 'r3mix|18000' => 18000, // 3.94, 3.95 - ); - if (!isset($ExpectedLowpass[$ExplodedOptions[1].'|'.$thisfile_mpeg_audio_lame['lowpass_frequency']]) && ($thisfile_mpeg_audio_lame['lowpass_frequency'] < 22050) && (round($thisfile_mpeg_audio_lame['lowpass_frequency'] / 1000) < round($thisfile_mpeg_audio['sample_rate'] / 2000))) { - $encoder_options .= ' --lowpass '.$thisfile_mpeg_audio_lame['lowpass_frequency']; - } - break; - - default: - break; - } - break; - } - } - - if (isset($thisfile_mpeg_audio_lame['raw']['source_sample_freq'])) { - if (($thisfile_mpeg_audio['sample_rate'] == 44100) && ($thisfile_mpeg_audio_lame['raw']['source_sample_freq'] != 1)) { - $encoder_options .= ' --resample 44100'; - } elseif (($thisfile_mpeg_audio['sample_rate'] == 48000) && ($thisfile_mpeg_audio_lame['raw']['source_sample_freq'] != 2)) { - $encoder_options .= ' --resample 48000'; - } elseif ($thisfile_mpeg_audio['sample_rate'] < 44100) { - switch ($thisfile_mpeg_audio_lame['raw']['source_sample_freq']) { - case 0: // <= 32000 - // may or may not be same as source frequency - ignore - break; - case 1: // 44100 - case 2: // 48000 - case 3: // 48000+ - $ExplodedOptions = explode(' ', $encoder_options, 4); - switch ($ExplodedOptions[0]) { - case '--preset': - case '--alt-preset': - switch ($ExplodedOptions[1]) { - case 'fast': - case 'portable': - case 'medium': - case 'standard': - case 'extreme': - case 'insane': - $encoder_options .= ' --resample '.$thisfile_mpeg_audio['sample_rate']; - break; - - default: - static $ExpectedResampledRate = array( - 'phon+/lw/mw-eu/sw|16000' => 16000, - 'mw-us|24000' => 24000, // 3.95 - 'mw-us|32000' => 32000, // 3.93 - 'mw-us|16000' => 16000, // 3.92 - 'phone|16000' => 16000, - 'phone|11025' => 11025, // 3.94a15 - 'radio|32000' => 32000, // 3.94a15 - 'fm/radio|32000' => 32000, // 3.92 - 'fm|32000' => 32000, // 3.90 - 'voice|32000' => 32000); - if (!isset($ExpectedResampledRate[$ExplodedOptions[1].'|'.$thisfile_mpeg_audio['sample_rate']])) { - $encoder_options .= ' --resample '.$thisfile_mpeg_audio['sample_rate']; - } - break; - } - break; - - case '--r3mix': - default: - $encoder_options .= ' --resample '.$thisfile_mpeg_audio['sample_rate']; - break; - } - break; - } - } - } - if (empty($encoder_options) && !empty($info['audio']['bitrate']) && !empty($info['audio']['bitrate_mode'])) { - //$encoder_options = strtoupper($info['audio']['bitrate_mode']).ceil($info['audio']['bitrate'] / 1000); - $encoder_options = strtoupper($info['audio']['bitrate_mode']); - } - - return $encoder_options; - } - - - public function decodeMPEGaudioHeader($offset, &$info, $recursivesearch=true, $ScanAsCBR=false, $FastMPEGheaderScan=false) { - static $MPEGaudioVersionLookup; - static $MPEGaudioLayerLookup; - static $MPEGaudioBitrateLookup; - static $MPEGaudioFrequencyLookup; - static $MPEGaudioChannelModeLookup; - static $MPEGaudioModeExtensionLookup; - static $MPEGaudioEmphasisLookup; - if (empty($MPEGaudioVersionLookup)) { - $MPEGaudioVersionLookup = self::MPEGaudioVersionArray(); - $MPEGaudioLayerLookup = self::MPEGaudioLayerArray(); - $MPEGaudioBitrateLookup = self::MPEGaudioBitrateArray(); - $MPEGaudioFrequencyLookup = self::MPEGaudioFrequencyArray(); - $MPEGaudioChannelModeLookup = self::MPEGaudioChannelModeArray(); - $MPEGaudioModeExtensionLookup = self::MPEGaudioModeExtensionArray(); - $MPEGaudioEmphasisLookup = self::MPEGaudioEmphasisArray(); - } - - if (fseek($this->getid3->fp, $offset, SEEK_SET) != 0) { - $info['error'][] = 'decodeMPEGaudioHeader() failed to seek to next offset at '.$offset; - return false; - } - //$headerstring = fread($this->getid3->fp, 1441); // worst-case max length = 32kHz @ 320kbps layer 3 = 1441 bytes/frame - $headerstring = fread($this->getid3->fp, 226); // LAME header at offset 36 + 190 bytes of Xing/LAME data - - // MP3 audio frame structure: - // $aa $aa $aa $aa [$bb $bb] $cc... - // where $aa..$aa is the four-byte mpeg-audio header (below) - // $bb $bb is the optional 2-byte CRC - // and $cc... is the audio data - - $head4 = substr($headerstring, 0, 4); - - static $MPEGaudioHeaderDecodeCache = array(); - if (isset($MPEGaudioHeaderDecodeCache[$head4])) { - $MPEGheaderRawArray = $MPEGaudioHeaderDecodeCache[$head4]; - } else { - $MPEGheaderRawArray = self::MPEGaudioHeaderDecode($head4); - $MPEGaudioHeaderDecodeCache[$head4] = $MPEGheaderRawArray; - } - - static $MPEGaudioHeaderValidCache = array(); - if (!isset($MPEGaudioHeaderValidCache[$head4])) { // Not in cache - //$MPEGaudioHeaderValidCache[$head4] = self::MPEGaudioHeaderValid($MPEGheaderRawArray, false, true); // allow badly-formatted freeformat (from LAME 3.90 - 3.93.1) - $MPEGaudioHeaderValidCache[$head4] = self::MPEGaudioHeaderValid($MPEGheaderRawArray, false, false); - } - - // shortcut - if (!isset($info['mpeg']['audio'])) { - $info['mpeg']['audio'] = array(); - } - $thisfile_mpeg_audio = &$info['mpeg']['audio']; - - - if ($MPEGaudioHeaderValidCache[$head4]) { - $thisfile_mpeg_audio['raw'] = $MPEGheaderRawArray; - } else { - $info['error'][] = 'Invalid MPEG audio header ('.getid3_lib::PrintHexBytes($head4).') at offset '.$offset; - return false; - } - - if (!$FastMPEGheaderScan) { - $thisfile_mpeg_audio['version'] = $MPEGaudioVersionLookup[$thisfile_mpeg_audio['raw']['version']]; - $thisfile_mpeg_audio['layer'] = $MPEGaudioLayerLookup[$thisfile_mpeg_audio['raw']['layer']]; - - $thisfile_mpeg_audio['channelmode'] = $MPEGaudioChannelModeLookup[$thisfile_mpeg_audio['raw']['channelmode']]; - $thisfile_mpeg_audio['channels'] = (($thisfile_mpeg_audio['channelmode'] == 'mono') ? 1 : 2); - $thisfile_mpeg_audio['sample_rate'] = $MPEGaudioFrequencyLookup[$thisfile_mpeg_audio['version']][$thisfile_mpeg_audio['raw']['sample_rate']]; - $thisfile_mpeg_audio['protection'] = !$thisfile_mpeg_audio['raw']['protection']; - $thisfile_mpeg_audio['private'] = (bool) $thisfile_mpeg_audio['raw']['private']; - $thisfile_mpeg_audio['modeextension'] = $MPEGaudioModeExtensionLookup[$thisfile_mpeg_audio['layer']][$thisfile_mpeg_audio['raw']['modeextension']]; - $thisfile_mpeg_audio['copyright'] = (bool) $thisfile_mpeg_audio['raw']['copyright']; - $thisfile_mpeg_audio['original'] = (bool) $thisfile_mpeg_audio['raw']['original']; - $thisfile_mpeg_audio['emphasis'] = $MPEGaudioEmphasisLookup[$thisfile_mpeg_audio['raw']['emphasis']]; - - $info['audio']['channels'] = $thisfile_mpeg_audio['channels']; - $info['audio']['sample_rate'] = $thisfile_mpeg_audio['sample_rate']; - - if ($thisfile_mpeg_audio['protection']) { - $thisfile_mpeg_audio['crc'] = getid3_lib::BigEndian2Int(substr($headerstring, 4, 2)); - } - } - - if ($thisfile_mpeg_audio['raw']['bitrate'] == 15) { - // http://www.hydrogenaudio.org/?act=ST&f=16&t=9682&st=0 - $info['warning'][] = 'Invalid bitrate index (15), this is a known bug in free-format MP3s encoded by LAME v3.90 - 3.93.1'; - $thisfile_mpeg_audio['raw']['bitrate'] = 0; - } - $thisfile_mpeg_audio['padding'] = (bool) $thisfile_mpeg_audio['raw']['padding']; - $thisfile_mpeg_audio['bitrate'] = $MPEGaudioBitrateLookup[$thisfile_mpeg_audio['version']][$thisfile_mpeg_audio['layer']][$thisfile_mpeg_audio['raw']['bitrate']]; - - if (($thisfile_mpeg_audio['bitrate'] == 'free') && ($offset == $info['avdataoffset'])) { - // only skip multiple frame check if free-format bitstream found at beginning of file - // otherwise is quite possibly simply corrupted data - $recursivesearch = false; - } - - // For Layer 2 there are some combinations of bitrate and mode which are not allowed. - if (!$FastMPEGheaderScan && ($thisfile_mpeg_audio['layer'] == '2')) { - - $info['audio']['dataformat'] = 'mp2'; - switch ($thisfile_mpeg_audio['channelmode']) { - - case 'mono': - if (($thisfile_mpeg_audio['bitrate'] == 'free') || ($thisfile_mpeg_audio['bitrate'] <= 192000)) { - // these are ok - } else { - $info['error'][] = $thisfile_mpeg_audio['bitrate'].'kbps not allowed in Layer 2, '.$thisfile_mpeg_audio['channelmode'].'.'; - return false; - } - break; - - case 'stereo': - case 'joint stereo': - case 'dual channel': - if (($thisfile_mpeg_audio['bitrate'] == 'free') || ($thisfile_mpeg_audio['bitrate'] == 64000) || ($thisfile_mpeg_audio['bitrate'] >= 96000)) { - // these are ok - } else { - $info['error'][] = intval(round($thisfile_mpeg_audio['bitrate'] / 1000)).'kbps not allowed in Layer 2, '.$thisfile_mpeg_audio['channelmode'].'.'; - return false; - } - break; - - } - - } - - - if ($info['audio']['sample_rate'] > 0) { - $thisfile_mpeg_audio['framelength'] = self::MPEGaudioFrameLength($thisfile_mpeg_audio['bitrate'], $thisfile_mpeg_audio['version'], $thisfile_mpeg_audio['layer'], (int) $thisfile_mpeg_audio['padding'], $info['audio']['sample_rate']); - } - - $nextframetestoffset = $offset + 1; - if ($thisfile_mpeg_audio['bitrate'] != 'free') { - - $info['audio']['bitrate'] = $thisfile_mpeg_audio['bitrate']; - - if (isset($thisfile_mpeg_audio['framelength'])) { - $nextframetestoffset = $offset + $thisfile_mpeg_audio['framelength']; - } else { - $info['error'][] = 'Frame at offset('.$offset.') is has an invalid frame length.'; - return false; - } - - } - - $ExpectedNumberOfAudioBytes = 0; - - //////////////////////////////////////////////////////////////////////////////////// - // Variable-bitrate headers - - if (substr($headerstring, 4 + 32, 4) == 'VBRI') { - // Fraunhofer VBR header is hardcoded 'VBRI' at offset 0x24 (36) - // specs taken from http://minnie.tuhs.org/pipermail/mp3encoder/2001-January/001800.html - - $thisfile_mpeg_audio['bitrate_mode'] = 'vbr'; - $thisfile_mpeg_audio['VBR_method'] = 'Fraunhofer'; - $info['audio']['codec'] = 'Fraunhofer'; - - $SideInfoData = substr($headerstring, 4 + 2, 32); - - $FraunhoferVBROffset = 36; - - $thisfile_mpeg_audio['VBR_encoder_version'] = getid3_lib::BigEndian2Int(substr($headerstring, $FraunhoferVBROffset + 4, 2)); // VbriVersion - $thisfile_mpeg_audio['VBR_encoder_delay'] = getid3_lib::BigEndian2Int(substr($headerstring, $FraunhoferVBROffset + 6, 2)); // VbriDelay - $thisfile_mpeg_audio['VBR_quality'] = getid3_lib::BigEndian2Int(substr($headerstring, $FraunhoferVBROffset + 8, 2)); // VbriQuality - $thisfile_mpeg_audio['VBR_bytes'] = getid3_lib::BigEndian2Int(substr($headerstring, $FraunhoferVBROffset + 10, 4)); // VbriStreamBytes - $thisfile_mpeg_audio['VBR_frames'] = getid3_lib::BigEndian2Int(substr($headerstring, $FraunhoferVBROffset + 14, 4)); // VbriStreamFrames - $thisfile_mpeg_audio['VBR_seek_offsets'] = getid3_lib::BigEndian2Int(substr($headerstring, $FraunhoferVBROffset + 18, 2)); // VbriTableSize - $thisfile_mpeg_audio['VBR_seek_scale'] = getid3_lib::BigEndian2Int(substr($headerstring, $FraunhoferVBROffset + 20, 2)); // VbriTableScale - $thisfile_mpeg_audio['VBR_entry_bytes'] = getid3_lib::BigEndian2Int(substr($headerstring, $FraunhoferVBROffset + 22, 2)); // VbriEntryBytes - $thisfile_mpeg_audio['VBR_entry_frames'] = getid3_lib::BigEndian2Int(substr($headerstring, $FraunhoferVBROffset + 24, 2)); // VbriEntryFrames - - $ExpectedNumberOfAudioBytes = $thisfile_mpeg_audio['VBR_bytes']; - - $previousbyteoffset = $offset; - for ($i = 0; $i < $thisfile_mpeg_audio['VBR_seek_offsets']; $i++) { - $Fraunhofer_OffsetN = getid3_lib::BigEndian2Int(substr($headerstring, $FraunhoferVBROffset, $thisfile_mpeg_audio['VBR_entry_bytes'])); - $FraunhoferVBROffset += $thisfile_mpeg_audio['VBR_entry_bytes']; - $thisfile_mpeg_audio['VBR_offsets_relative'][$i] = ($Fraunhofer_OffsetN * $thisfile_mpeg_audio['VBR_seek_scale']); - $thisfile_mpeg_audio['VBR_offsets_absolute'][$i] = ($Fraunhofer_OffsetN * $thisfile_mpeg_audio['VBR_seek_scale']) + $previousbyteoffset; - $previousbyteoffset += $Fraunhofer_OffsetN; - } - - - } else { - - // Xing VBR header is hardcoded 'Xing' at a offset 0x0D (13), 0x15 (21) or 0x24 (36) - // depending on MPEG layer and number of channels - - $VBRidOffset = self::XingVBRidOffset($thisfile_mpeg_audio['version'], $thisfile_mpeg_audio['channelmode']); - $SideInfoData = substr($headerstring, 4 + 2, $VBRidOffset - 4); - - if ((substr($headerstring, $VBRidOffset, strlen('Xing')) == 'Xing') || (substr($headerstring, $VBRidOffset, strlen('Info')) == 'Info')) { - // 'Xing' is traditional Xing VBR frame - // 'Info' is LAME-encoded CBR (This was done to avoid CBR files to be recognized as traditional Xing VBR files by some decoders.) - // 'Info' *can* legally be used to specify a VBR file as well, however. - - // http://www.multiweb.cz/twoinches/MP3inside.htm - //00..03 = "Xing" or "Info" - //04..07 = Flags: - // 0x01 Frames Flag set if value for number of frames in file is stored - // 0x02 Bytes Flag set if value for filesize in bytes is stored - // 0x04 TOC Flag set if values for TOC are stored - // 0x08 VBR Scale Flag set if values for VBR scale is stored - //08..11 Frames: Number of frames in file (including the first Xing/Info one) - //12..15 Bytes: File length in Bytes - //16..115 TOC (Table of Contents): - // Contains of 100 indexes (one Byte length) for easier lookup in file. Approximately solves problem with moving inside file. - // Each Byte has a value according this formula: - // (TOC[i] / 256) * fileLenInBytes - // So if song lasts eg. 240 sec. and you want to jump to 60. sec. (and file is 5 000 000 Bytes length) you can use: - // TOC[(60/240)*100] = TOC[25] - // and corresponding Byte in file is then approximately at: - // (TOC[25]/256) * 5000000 - //116..119 VBR Scale - - - // should be safe to leave this at 'vbr' and let it be overriden to 'cbr' if a CBR preset/mode is used by LAME -// if (substr($headerstring, $VBRidOffset, strlen('Info')) == 'Xing') { - $thisfile_mpeg_audio['bitrate_mode'] = 'vbr'; - $thisfile_mpeg_audio['VBR_method'] = 'Xing'; -// } else { -// $ScanAsCBR = true; -// $thisfile_mpeg_audio['bitrate_mode'] = 'cbr'; -// } - - $thisfile_mpeg_audio['xing_flags_raw'] = getid3_lib::BigEndian2Int(substr($headerstring, $VBRidOffset + 4, 4)); - - $thisfile_mpeg_audio['xing_flags']['frames'] = (bool) ($thisfile_mpeg_audio['xing_flags_raw'] & 0x00000001); - $thisfile_mpeg_audio['xing_flags']['bytes'] = (bool) ($thisfile_mpeg_audio['xing_flags_raw'] & 0x00000002); - $thisfile_mpeg_audio['xing_flags']['toc'] = (bool) ($thisfile_mpeg_audio['xing_flags_raw'] & 0x00000004); - $thisfile_mpeg_audio['xing_flags']['vbr_scale'] = (bool) ($thisfile_mpeg_audio['xing_flags_raw'] & 0x00000008); - - if ($thisfile_mpeg_audio['xing_flags']['frames']) { - $thisfile_mpeg_audio['VBR_frames'] = getid3_lib::BigEndian2Int(substr($headerstring, $VBRidOffset + 8, 4)); - //$thisfile_mpeg_audio['VBR_frames']--; // don't count header Xing/Info frame - } - if ($thisfile_mpeg_audio['xing_flags']['bytes']) { - $thisfile_mpeg_audio['VBR_bytes'] = getid3_lib::BigEndian2Int(substr($headerstring, $VBRidOffset + 12, 4)); - } - - //if (($thisfile_mpeg_audio['bitrate'] == 'free') && !empty($thisfile_mpeg_audio['VBR_frames']) && !empty($thisfile_mpeg_audio['VBR_bytes'])) { - if (!empty($thisfile_mpeg_audio['VBR_frames']) && !empty($thisfile_mpeg_audio['VBR_bytes'])) { - - $framelengthfloat = $thisfile_mpeg_audio['VBR_bytes'] / $thisfile_mpeg_audio['VBR_frames']; - - if ($thisfile_mpeg_audio['layer'] == '1') { - // BitRate = (((FrameLengthInBytes / 4) - Padding) * SampleRate) / 12 - //$info['audio']['bitrate'] = ((($framelengthfloat / 4) - intval($thisfile_mpeg_audio['padding'])) * $thisfile_mpeg_audio['sample_rate']) / 12; - $info['audio']['bitrate'] = ($framelengthfloat / 4) * $thisfile_mpeg_audio['sample_rate'] * (2 / $info['audio']['channels']) / 12; - } else { - // Bitrate = ((FrameLengthInBytes - Padding) * SampleRate) / 144 - //$info['audio']['bitrate'] = (($framelengthfloat - intval($thisfile_mpeg_audio['padding'])) * $thisfile_mpeg_audio['sample_rate']) / 144; - $info['audio']['bitrate'] = $framelengthfloat * $thisfile_mpeg_audio['sample_rate'] * (2 / $info['audio']['channels']) / 144; - } - $thisfile_mpeg_audio['framelength'] = floor($framelengthfloat); - } - - if ($thisfile_mpeg_audio['xing_flags']['toc']) { - $LAMEtocData = substr($headerstring, $VBRidOffset + 16, 100); - for ($i = 0; $i < 100; $i++) { - $thisfile_mpeg_audio['toc'][$i] = ord($LAMEtocData{$i}); - } - } - if ($thisfile_mpeg_audio['xing_flags']['vbr_scale']) { - $thisfile_mpeg_audio['VBR_scale'] = getid3_lib::BigEndian2Int(substr($headerstring, $VBRidOffset + 116, 4)); - } - - - // http://gabriel.mp3-tech.org/mp3infotag.html - if (substr($headerstring, $VBRidOffset + 120, 4) == 'LAME') { - - // shortcut - $thisfile_mpeg_audio['LAME'] = array(); - $thisfile_mpeg_audio_lame = &$thisfile_mpeg_audio['LAME']; - - - $thisfile_mpeg_audio_lame['long_version'] = substr($headerstring, $VBRidOffset + 120, 20); - $thisfile_mpeg_audio_lame['short_version'] = substr($thisfile_mpeg_audio_lame['long_version'], 0, 9); - - if ($thisfile_mpeg_audio_lame['short_version'] >= 'LAME3.90') { - - // extra 11 chars are not part of version string when LAMEtag present - unset($thisfile_mpeg_audio_lame['long_version']); - - // It the LAME tag was only introduced in LAME v3.90 - // http://www.hydrogenaudio.org/?act=ST&f=15&t=9933 - - // Offsets of various bytes in http://gabriel.mp3-tech.org/mp3infotag.html - // are assuming a 'Xing' identifier offset of 0x24, which is the case for - // MPEG-1 non-mono, but not for other combinations - $LAMEtagOffsetContant = $VBRidOffset - 0x24; - - // shortcuts - $thisfile_mpeg_audio_lame['RGAD'] = array('track'=>array(), 'album'=>array()); - $thisfile_mpeg_audio_lame_RGAD = &$thisfile_mpeg_audio_lame['RGAD']; - $thisfile_mpeg_audio_lame_RGAD_track = &$thisfile_mpeg_audio_lame_RGAD['track']; - $thisfile_mpeg_audio_lame_RGAD_album = &$thisfile_mpeg_audio_lame_RGAD['album']; - $thisfile_mpeg_audio_lame['raw'] = array(); - $thisfile_mpeg_audio_lame_raw = &$thisfile_mpeg_audio_lame['raw']; - - // byte $9B VBR Quality - // This field is there to indicate a quality level, although the scale was not precised in the original Xing specifications. - // Actually overwrites original Xing bytes - unset($thisfile_mpeg_audio['VBR_scale']); - $thisfile_mpeg_audio_lame['vbr_quality'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0x9B, 1)); - - // bytes $9C-$A4 Encoder short VersionString - $thisfile_mpeg_audio_lame['short_version'] = substr($headerstring, $LAMEtagOffsetContant + 0x9C, 9); - - // byte $A5 Info Tag revision + VBR method - $LAMEtagRevisionVBRmethod = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xA5, 1)); - - $thisfile_mpeg_audio_lame['tag_revision'] = ($LAMEtagRevisionVBRmethod & 0xF0) >> 4; - $thisfile_mpeg_audio_lame_raw['vbr_method'] = $LAMEtagRevisionVBRmethod & 0x0F; - $thisfile_mpeg_audio_lame['vbr_method'] = self::LAMEvbrMethodLookup($thisfile_mpeg_audio_lame_raw['vbr_method']); - $thisfile_mpeg_audio['bitrate_mode'] = substr($thisfile_mpeg_audio_lame['vbr_method'], 0, 3); // usually either 'cbr' or 'vbr', but truncates 'vbr-old / vbr-rh' to 'vbr' - - // byte $A6 Lowpass filter value - $thisfile_mpeg_audio_lame['lowpass_frequency'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xA6, 1)) * 100; - - // bytes $A7-$AE Replay Gain - // http://privatewww.essex.ac.uk/~djmrob/replaygain/rg_data_format.html - // bytes $A7-$AA : 32 bit floating point "Peak signal amplitude" - if ($thisfile_mpeg_audio_lame['short_version'] >= 'LAME3.94b') { - // LAME 3.94a16 and later - 9.23 fixed point - // ie 0x0059E2EE / (2^23) = 5890798 / 8388608 = 0.7022378444671630859375 - $thisfile_mpeg_audio_lame_RGAD['peak_amplitude'] = (float) ((getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xA7, 4))) / 8388608); - } else { - // LAME 3.94a15 and earlier - 32-bit floating point - // Actually 3.94a16 will fall in here too and be WRONG, but is hard to detect 3.94a16 vs 3.94a15 - $thisfile_mpeg_audio_lame_RGAD['peak_amplitude'] = getid3_lib::LittleEndian2Float(substr($headerstring, $LAMEtagOffsetContant + 0xA7, 4)); - } - if ($thisfile_mpeg_audio_lame_RGAD['peak_amplitude'] == 0) { - unset($thisfile_mpeg_audio_lame_RGAD['peak_amplitude']); - } else { - $thisfile_mpeg_audio_lame_RGAD['peak_db'] = getid3_lib::RGADamplitude2dB($thisfile_mpeg_audio_lame_RGAD['peak_amplitude']); - } - - $thisfile_mpeg_audio_lame_raw['RGAD_track'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xAB, 2)); - $thisfile_mpeg_audio_lame_raw['RGAD_album'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xAD, 2)); - - - if ($thisfile_mpeg_audio_lame_raw['RGAD_track'] != 0) { - - $thisfile_mpeg_audio_lame_RGAD_track['raw']['name'] = ($thisfile_mpeg_audio_lame_raw['RGAD_track'] & 0xE000) >> 13; - $thisfile_mpeg_audio_lame_RGAD_track['raw']['originator'] = ($thisfile_mpeg_audio_lame_raw['RGAD_track'] & 0x1C00) >> 10; - $thisfile_mpeg_audio_lame_RGAD_track['raw']['sign_bit'] = ($thisfile_mpeg_audio_lame_raw['RGAD_track'] & 0x0200) >> 9; - $thisfile_mpeg_audio_lame_RGAD_track['raw']['gain_adjust'] = $thisfile_mpeg_audio_lame_raw['RGAD_track'] & 0x01FF; - $thisfile_mpeg_audio_lame_RGAD_track['name'] = getid3_lib::RGADnameLookup($thisfile_mpeg_audio_lame_RGAD_track['raw']['name']); - $thisfile_mpeg_audio_lame_RGAD_track['originator'] = getid3_lib::RGADoriginatorLookup($thisfile_mpeg_audio_lame_RGAD_track['raw']['originator']); - $thisfile_mpeg_audio_lame_RGAD_track['gain_db'] = getid3_lib::RGADadjustmentLookup($thisfile_mpeg_audio_lame_RGAD_track['raw']['gain_adjust'], $thisfile_mpeg_audio_lame_RGAD_track['raw']['sign_bit']); - - if (!empty($thisfile_mpeg_audio_lame_RGAD['peak_amplitude'])) { - $info['replay_gain']['track']['peak'] = $thisfile_mpeg_audio_lame_RGAD['peak_amplitude']; - } - $info['replay_gain']['track']['originator'] = $thisfile_mpeg_audio_lame_RGAD_track['originator']; - $info['replay_gain']['track']['adjustment'] = $thisfile_mpeg_audio_lame_RGAD_track['gain_db']; - } else { - unset($thisfile_mpeg_audio_lame_RGAD['track']); - } - if ($thisfile_mpeg_audio_lame_raw['RGAD_album'] != 0) { - - $thisfile_mpeg_audio_lame_RGAD_album['raw']['name'] = ($thisfile_mpeg_audio_lame_raw['RGAD_album'] & 0xE000) >> 13; - $thisfile_mpeg_audio_lame_RGAD_album['raw']['originator'] = ($thisfile_mpeg_audio_lame_raw['RGAD_album'] & 0x1C00) >> 10; - $thisfile_mpeg_audio_lame_RGAD_album['raw']['sign_bit'] = ($thisfile_mpeg_audio_lame_raw['RGAD_album'] & 0x0200) >> 9; - $thisfile_mpeg_audio_lame_RGAD_album['raw']['gain_adjust'] = $thisfile_mpeg_audio_lame_raw['RGAD_album'] & 0x01FF; - $thisfile_mpeg_audio_lame_RGAD_album['name'] = getid3_lib::RGADnameLookup($thisfile_mpeg_audio_lame_RGAD_album['raw']['name']); - $thisfile_mpeg_audio_lame_RGAD_album['originator'] = getid3_lib::RGADoriginatorLookup($thisfile_mpeg_audio_lame_RGAD_album['raw']['originator']); - $thisfile_mpeg_audio_lame_RGAD_album['gain_db'] = getid3_lib::RGADadjustmentLookup($thisfile_mpeg_audio_lame_RGAD_album['raw']['gain_adjust'], $thisfile_mpeg_audio_lame_RGAD_album['raw']['sign_bit']); - - if (!empty($thisfile_mpeg_audio_lame_RGAD['peak_amplitude'])) { - $info['replay_gain']['album']['peak'] = $thisfile_mpeg_audio_lame_RGAD['peak_amplitude']; - } - $info['replay_gain']['album']['originator'] = $thisfile_mpeg_audio_lame_RGAD_album['originator']; - $info['replay_gain']['album']['adjustment'] = $thisfile_mpeg_audio_lame_RGAD_album['gain_db']; - } else { - unset($thisfile_mpeg_audio_lame_RGAD['album']); - } - if (empty($thisfile_mpeg_audio_lame_RGAD)) { - unset($thisfile_mpeg_audio_lame['RGAD']); - } - - - // byte $AF Encoding flags + ATH Type - $EncodingFlagsATHtype = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xAF, 1)); - $thisfile_mpeg_audio_lame['encoding_flags']['nspsytune'] = (bool) ($EncodingFlagsATHtype & 0x10); - $thisfile_mpeg_audio_lame['encoding_flags']['nssafejoint'] = (bool) ($EncodingFlagsATHtype & 0x20); - $thisfile_mpeg_audio_lame['encoding_flags']['nogap_next'] = (bool) ($EncodingFlagsATHtype & 0x40); - $thisfile_mpeg_audio_lame['encoding_flags']['nogap_prev'] = (bool) ($EncodingFlagsATHtype & 0x80); - $thisfile_mpeg_audio_lame['ath_type'] = $EncodingFlagsATHtype & 0x0F; - - // byte $B0 if ABR {specified bitrate} else {minimal bitrate} - $thisfile_mpeg_audio_lame['raw']['abrbitrate_minbitrate'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB0, 1)); - if ($thisfile_mpeg_audio_lame_raw['vbr_method'] == 2) { // Average BitRate (ABR) - $thisfile_mpeg_audio_lame['bitrate_abr'] = $thisfile_mpeg_audio_lame['raw']['abrbitrate_minbitrate']; - } elseif ($thisfile_mpeg_audio_lame_raw['vbr_method'] == 1) { // Constant BitRate (CBR) - // ignore - } elseif ($thisfile_mpeg_audio_lame['raw']['abrbitrate_minbitrate'] > 0) { // Variable BitRate (VBR) - minimum bitrate - $thisfile_mpeg_audio_lame['bitrate_min'] = $thisfile_mpeg_audio_lame['raw']['abrbitrate_minbitrate']; - } - - // bytes $B1-$B3 Encoder delays - $EncoderDelays = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB1, 3)); - $thisfile_mpeg_audio_lame['encoder_delay'] = ($EncoderDelays & 0xFFF000) >> 12; - $thisfile_mpeg_audio_lame['end_padding'] = $EncoderDelays & 0x000FFF; - - // byte $B4 Misc - $MiscByte = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB4, 1)); - $thisfile_mpeg_audio_lame_raw['noise_shaping'] = ($MiscByte & 0x03); - $thisfile_mpeg_audio_lame_raw['stereo_mode'] = ($MiscByte & 0x1C) >> 2; - $thisfile_mpeg_audio_lame_raw['not_optimal_quality'] = ($MiscByte & 0x20) >> 5; - $thisfile_mpeg_audio_lame_raw['source_sample_freq'] = ($MiscByte & 0xC0) >> 6; - $thisfile_mpeg_audio_lame['noise_shaping'] = $thisfile_mpeg_audio_lame_raw['noise_shaping']; - $thisfile_mpeg_audio_lame['stereo_mode'] = self::LAMEmiscStereoModeLookup($thisfile_mpeg_audio_lame_raw['stereo_mode']); - $thisfile_mpeg_audio_lame['not_optimal_quality'] = (bool) $thisfile_mpeg_audio_lame_raw['not_optimal_quality']; - $thisfile_mpeg_audio_lame['source_sample_freq'] = self::LAMEmiscSourceSampleFrequencyLookup($thisfile_mpeg_audio_lame_raw['source_sample_freq']); - - // byte $B5 MP3 Gain - $thisfile_mpeg_audio_lame_raw['mp3_gain'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB5, 1), false, true); - $thisfile_mpeg_audio_lame['mp3_gain_db'] = (getid3_lib::RGADamplitude2dB(2) / 4) * $thisfile_mpeg_audio_lame_raw['mp3_gain']; - $thisfile_mpeg_audio_lame['mp3_gain_factor'] = pow(2, ($thisfile_mpeg_audio_lame['mp3_gain_db'] / 6)); - - // bytes $B6-$B7 Preset and surround info - $PresetSurroundBytes = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB6, 2)); - // Reserved = ($PresetSurroundBytes & 0xC000); - $thisfile_mpeg_audio_lame_raw['surround_info'] = ($PresetSurroundBytes & 0x3800); - $thisfile_mpeg_audio_lame['surround_info'] = self::LAMEsurroundInfoLookup($thisfile_mpeg_audio_lame_raw['surround_info']); - $thisfile_mpeg_audio_lame['preset_used_id'] = ($PresetSurroundBytes & 0x07FF); - $thisfile_mpeg_audio_lame['preset_used'] = self::LAMEpresetUsedLookup($thisfile_mpeg_audio_lame); - if (!empty($thisfile_mpeg_audio_lame['preset_used_id']) && empty($thisfile_mpeg_audio_lame['preset_used'])) { - $info['warning'][] = 'Unknown LAME preset used ('.$thisfile_mpeg_audio_lame['preset_used_id'].') - please report to info@getid3.org'; - } - if (($thisfile_mpeg_audio_lame['short_version'] == 'LAME3.90.') && !empty($thisfile_mpeg_audio_lame['preset_used_id'])) { - // this may change if 3.90.4 ever comes out - $thisfile_mpeg_audio_lame['short_version'] = 'LAME3.90.3'; - } - - // bytes $B8-$BB MusicLength - $thisfile_mpeg_audio_lame['audio_bytes'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xB8, 4)); - $ExpectedNumberOfAudioBytes = (($thisfile_mpeg_audio_lame['audio_bytes'] > 0) ? $thisfile_mpeg_audio_lame['audio_bytes'] : $thisfile_mpeg_audio['VBR_bytes']); - - // bytes $BC-$BD MusicCRC - $thisfile_mpeg_audio_lame['music_crc'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xBC, 2)); - - // bytes $BE-$BF CRC-16 of Info Tag - $thisfile_mpeg_audio_lame['lame_tag_crc'] = getid3_lib::BigEndian2Int(substr($headerstring, $LAMEtagOffsetContant + 0xBE, 2)); - - - // LAME CBR - if ($thisfile_mpeg_audio_lame_raw['vbr_method'] == 1) { - - $thisfile_mpeg_audio['bitrate_mode'] = 'cbr'; - $thisfile_mpeg_audio['bitrate'] = self::ClosestStandardMP3Bitrate($thisfile_mpeg_audio['bitrate']); - $info['audio']['bitrate'] = $thisfile_mpeg_audio['bitrate']; - //if (empty($thisfile_mpeg_audio['bitrate']) || (!empty($thisfile_mpeg_audio_lame['bitrate_min']) && ($thisfile_mpeg_audio_lame['bitrate_min'] != 255))) { - // $thisfile_mpeg_audio['bitrate'] = $thisfile_mpeg_audio_lame['bitrate_min']; - //} - - } - - } - } - - } else { - - // not Fraunhofer or Xing VBR methods, most likely CBR (but could be VBR with no header) - $thisfile_mpeg_audio['bitrate_mode'] = 'cbr'; - if ($recursivesearch) { - $thisfile_mpeg_audio['bitrate_mode'] = 'vbr'; - if ($this->RecursiveFrameScanning($offset, $nextframetestoffset, true)) { - $recursivesearch = false; - $thisfile_mpeg_audio['bitrate_mode'] = 'cbr'; - } - if ($thisfile_mpeg_audio['bitrate_mode'] == 'vbr') { - $info['warning'][] = 'VBR file with no VBR header. Bitrate values calculated from actual frame bitrates.'; - } - } - - } - - } - - if (($ExpectedNumberOfAudioBytes > 0) && ($ExpectedNumberOfAudioBytes != ($info['avdataend'] - $info['avdataoffset']))) { - if ($ExpectedNumberOfAudioBytes > ($info['avdataend'] - $info['avdataoffset'])) { - if (isset($info['fileformat']) && ($info['fileformat'] == 'riff')) { - // ignore, audio data is broken into chunks so will always be data "missing" - } elseif (($ExpectedNumberOfAudioBytes - ($info['avdataend'] - $info['avdataoffset'])) == 1) { - $info['warning'][] = 'Last byte of data truncated (this is a known bug in Meracl ID3 Tag Writer before v1.3.5)'; - } else { - $info['warning'][] = 'Probable truncated file: expecting '.$ExpectedNumberOfAudioBytes.' bytes of audio data, only found '.($info['avdataend'] - $info['avdataoffset']).' (short by '.($ExpectedNumberOfAudioBytes - ($info['avdataend'] - $info['avdataoffset'])).' bytes)'; - } - } else { - if ((($info['avdataend'] - $info['avdataoffset']) - $ExpectedNumberOfAudioBytes) == 1) { - // $prenullbytefileoffset = ftell($this->getid3->fp); - // fseek($this->getid3->fp, $info['avdataend'], SEEK_SET); - // $PossibleNullByte = fread($this->getid3->fp, 1); - // fseek($this->getid3->fp, $prenullbytefileoffset, SEEK_SET); - // if ($PossibleNullByte === "\x00") { - $info['avdataend']--; - // $info['warning'][] = 'Extra null byte at end of MP3 data assumed to be RIFF padding and therefore ignored'; - // } else { - // $info['warning'][] = 'Too much data in file: expecting '.$ExpectedNumberOfAudioBytes.' bytes of audio data, found '.($info['avdataend'] - $info['avdataoffset']).' ('.(($info['avdataend'] - $info['avdataoffset']) - $ExpectedNumberOfAudioBytes).' bytes too many)'; - // } - } else { - $info['warning'][] = 'Too much data in file: expecting '.$ExpectedNumberOfAudioBytes.' bytes of audio data, found '.($info['avdataend'] - $info['avdataoffset']).' ('.(($info['avdataend'] - $info['avdataoffset']) - $ExpectedNumberOfAudioBytes).' bytes too many)'; - } - } - } - - if (($thisfile_mpeg_audio['bitrate'] == 'free') && empty($info['audio']['bitrate'])) { - if (($offset == $info['avdataoffset']) && empty($thisfile_mpeg_audio['VBR_frames'])) { - $framebytelength = $this->FreeFormatFrameLength($offset, true); - if ($framebytelength > 0) { - $thisfile_mpeg_audio['framelength'] = $framebytelength; - if ($thisfile_mpeg_audio['layer'] == '1') { - // BitRate = (((FrameLengthInBytes / 4) - Padding) * SampleRate) / 12 - $info['audio']['bitrate'] = ((($framebytelength / 4) - intval($thisfile_mpeg_audio['padding'])) * $thisfile_mpeg_audio['sample_rate']) / 12; - } else { - // Bitrate = ((FrameLengthInBytes - Padding) * SampleRate) / 144 - $info['audio']['bitrate'] = (($framebytelength - intval($thisfile_mpeg_audio['padding'])) * $thisfile_mpeg_audio['sample_rate']) / 144; - } - } else { - $info['error'][] = 'Error calculating frame length of free-format MP3 without Xing/LAME header'; - } - } - } - - if (isset($thisfile_mpeg_audio['VBR_frames']) ? $thisfile_mpeg_audio['VBR_frames'] : '') { - switch ($thisfile_mpeg_audio['bitrate_mode']) { - case 'vbr': - case 'abr': - $bytes_per_frame = 1152; - if (($thisfile_mpeg_audio['version'] == '1') && ($thisfile_mpeg_audio['layer'] == 1)) { - $bytes_per_frame = 384; - } elseif ((($thisfile_mpeg_audio['version'] == '2') || ($thisfile_mpeg_audio['version'] == '2.5')) && ($thisfile_mpeg_audio['layer'] == 3)) { - $bytes_per_frame = 576; - } - $thisfile_mpeg_audio['VBR_bitrate'] = (isset($thisfile_mpeg_audio['VBR_bytes']) ? (($thisfile_mpeg_audio['VBR_bytes'] / $thisfile_mpeg_audio['VBR_frames']) * 8) * ($info['audio']['sample_rate'] / $bytes_per_frame) : 0); - if ($thisfile_mpeg_audio['VBR_bitrate'] > 0) { - $info['audio']['bitrate'] = $thisfile_mpeg_audio['VBR_bitrate']; - $thisfile_mpeg_audio['bitrate'] = $thisfile_mpeg_audio['VBR_bitrate']; // to avoid confusion - } - break; - } - } - - // End variable-bitrate headers - //////////////////////////////////////////////////////////////////////////////////// - - if ($recursivesearch) { - - if (!$this->RecursiveFrameScanning($offset, $nextframetestoffset, $ScanAsCBR)) { - return false; - } - - } - - - //if (false) { - // // experimental side info parsing section - not returning anything useful yet - // - // $SideInfoBitstream = getid3_lib::BigEndian2Bin($SideInfoData); - // $SideInfoOffset = 0; - // - // if ($thisfile_mpeg_audio['version'] == '1') { - // if ($thisfile_mpeg_audio['channelmode'] == 'mono') { - // // MPEG-1 (mono) - // $thisfile_mpeg_audio['side_info']['main_data_begin'] = substr($SideInfoBitstream, $SideInfoOffset, 9); - // $SideInfoOffset += 9; - // $SideInfoOffset += 5; - // } else { - // // MPEG-1 (stereo, joint-stereo, dual-channel) - // $thisfile_mpeg_audio['side_info']['main_data_begin'] = substr($SideInfoBitstream, $SideInfoOffset, 9); - // $SideInfoOffset += 9; - // $SideInfoOffset += 3; - // } - // } else { // 2 or 2.5 - // if ($thisfile_mpeg_audio['channelmode'] == 'mono') { - // // MPEG-2, MPEG-2.5 (mono) - // $thisfile_mpeg_audio['side_info']['main_data_begin'] = substr($SideInfoBitstream, $SideInfoOffset, 8); - // $SideInfoOffset += 8; - // $SideInfoOffset += 1; - // } else { - // // MPEG-2, MPEG-2.5 (stereo, joint-stereo, dual-channel) - // $thisfile_mpeg_audio['side_info']['main_data_begin'] = substr($SideInfoBitstream, $SideInfoOffset, 8); - // $SideInfoOffset += 8; - // $SideInfoOffset += 2; - // } - // } - // - // if ($thisfile_mpeg_audio['version'] == '1') { - // for ($channel = 0; $channel < $info['audio']['channels']; $channel++) { - // for ($scfsi_band = 0; $scfsi_band < 4; $scfsi_band++) { - // $thisfile_mpeg_audio['scfsi'][$channel][$scfsi_band] = substr($SideInfoBitstream, $SideInfoOffset, 1); - // $SideInfoOffset += 2; - // } - // } - // } - // for ($granule = 0; $granule < (($thisfile_mpeg_audio['version'] == '1') ? 2 : 1); $granule++) { - // for ($channel = 0; $channel < $info['audio']['channels']; $channel++) { - // $thisfile_mpeg_audio['part2_3_length'][$granule][$channel] = substr($SideInfoBitstream, $SideInfoOffset, 12); - // $SideInfoOffset += 12; - // $thisfile_mpeg_audio['big_values'][$granule][$channel] = substr($SideInfoBitstream, $SideInfoOffset, 9); - // $SideInfoOffset += 9; - // $thisfile_mpeg_audio['global_gain'][$granule][$channel] = substr($SideInfoBitstream, $SideInfoOffset, 8); - // $SideInfoOffset += 8; - // if ($thisfile_mpeg_audio['version'] == '1') { - // $thisfile_mpeg_audio['scalefac_compress'][$granule][$channel] = substr($SideInfoBitstream, $SideInfoOffset, 4); - // $SideInfoOffset += 4; - // } else { - // $thisfile_mpeg_audio['scalefac_compress'][$granule][$channel] = substr($SideInfoBitstream, $SideInfoOffset, 9); - // $SideInfoOffset += 9; - // } - // $thisfile_mpeg_audio['window_switching_flag'][$granule][$channel] = substr($SideInfoBitstream, $SideInfoOffset, 1); - // $SideInfoOffset += 1; - // - // if ($thisfile_mpeg_audio['window_switching_flag'][$granule][$channel] == '1') { - // - // $thisfile_mpeg_audio['block_type'][$granule][$channel] = substr($SideInfoBitstream, $SideInfoOffset, 2); - // $SideInfoOffset += 2; - // $thisfile_mpeg_audio['mixed_block_flag'][$granule][$channel] = substr($SideInfoBitstream, $SideInfoOffset, 1); - // $SideInfoOffset += 1; - // - // for ($region = 0; $region < 2; $region++) { - // $thisfile_mpeg_audio['table_select'][$granule][$channel][$region] = substr($SideInfoBitstream, $SideInfoOffset, 5); - // $SideInfoOffset += 5; - // } - // $thisfile_mpeg_audio['table_select'][$granule][$channel][2] = 0; - // - // for ($window = 0; $window < 3; $window++) { - // $thisfile_mpeg_audio['subblock_gain'][$granule][$channel][$window] = substr($SideInfoBitstream, $SideInfoOffset, 3); - // $SideInfoOffset += 3; - // } - // - // } else { - // - // for ($region = 0; $region < 3; $region++) { - // $thisfile_mpeg_audio['table_select'][$granule][$channel][$region] = substr($SideInfoBitstream, $SideInfoOffset, 5); - // $SideInfoOffset += 5; - // } - // - // $thisfile_mpeg_audio['region0_count'][$granule][$channel] = substr($SideInfoBitstream, $SideInfoOffset, 4); - // $SideInfoOffset += 4; - // $thisfile_mpeg_audio['region1_count'][$granule][$channel] = substr($SideInfoBitstream, $SideInfoOffset, 3); - // $SideInfoOffset += 3; - // $thisfile_mpeg_audio['block_type'][$granule][$channel] = 0; - // } - // - // if ($thisfile_mpeg_audio['version'] == '1') { - // $thisfile_mpeg_audio['preflag'][$granule][$channel] = substr($SideInfoBitstream, $SideInfoOffset, 1); - // $SideInfoOffset += 1; - // } - // $thisfile_mpeg_audio['scalefac_scale'][$granule][$channel] = substr($SideInfoBitstream, $SideInfoOffset, 1); - // $SideInfoOffset += 1; - // $thisfile_mpeg_audio['count1table_select'][$granule][$channel] = substr($SideInfoBitstream, $SideInfoOffset, 1); - // $SideInfoOffset += 1; - // } - // } - //} - - return true; - } - - public function RecursiveFrameScanning(&$offset, &$nextframetestoffset, $ScanAsCBR) { - $info = &$this->getid3->info; - $firstframetestarray = array('error'=>'', 'warning'=>'', 'avdataend'=>$info['avdataend'], 'avdataoffset'=>$info['avdataoffset']); - $this->decodeMPEGaudioHeader($offset, $firstframetestarray, false); - - for ($i = 0; $i < GETID3_MP3_VALID_CHECK_FRAMES; $i++) { - // check next GETID3_MP3_VALID_CHECK_FRAMES frames for validity, to make sure we haven't run across a false synch - if (($nextframetestoffset + 4) >= $info['avdataend']) { - // end of file - return true; - } - - $nextframetestarray = array('error'=>'', 'warning'=>'', 'avdataend'=>$info['avdataend'], 'avdataoffset'=>$info['avdataoffset']); - if ($this->decodeMPEGaudioHeader($nextframetestoffset, $nextframetestarray, false)) { - if ($ScanAsCBR) { - // force CBR mode, used for trying to pick out invalid audio streams with valid(?) VBR headers, or VBR streams with no VBR header - if (!isset($nextframetestarray['mpeg']['audio']['bitrate']) || !isset($firstframetestarray['mpeg']['audio']['bitrate']) || ($nextframetestarray['mpeg']['audio']['bitrate'] != $firstframetestarray['mpeg']['audio']['bitrate'])) { - return false; - } - } - - - // next frame is OK, get ready to check the one after that - if (isset($nextframetestarray['mpeg']['audio']['framelength']) && ($nextframetestarray['mpeg']['audio']['framelength'] > 0)) { - $nextframetestoffset += $nextframetestarray['mpeg']['audio']['framelength']; - } else { - $info['error'][] = 'Frame at offset ('.$offset.') is has an invalid frame length.'; - return false; - } - - } elseif (!empty($firstframetestarray['mpeg']['audio']['framelength']) && (($nextframetestoffset + $firstframetestarray['mpeg']['audio']['framelength']) > $info['avdataend'])) { - - // it's not the end of the file, but there's not enough data left for another frame, so assume it's garbage/padding and return OK - return true; - - } else { - - // next frame is not valid, note the error and fail, so scanning can contiue for a valid frame sequence - $info['warning'][] = 'Frame at offset ('.$offset.') is valid, but the next one at ('.$nextframetestoffset.') is not.'; - - return false; - } - } - return true; - } - - public function FreeFormatFrameLength($offset, $deepscan=false) { - $info = &$this->getid3->info; - - fseek($this->getid3->fp, $offset, SEEK_SET); - $MPEGaudioData = fread($this->getid3->fp, 32768); - - $SyncPattern1 = substr($MPEGaudioData, 0, 4); - // may be different pattern due to padding - $SyncPattern2 = $SyncPattern1{0}.$SyncPattern1{1}.chr(ord($SyncPattern1{2}) | 0x02).$SyncPattern1{3}; - if ($SyncPattern2 === $SyncPattern1) { - $SyncPattern2 = $SyncPattern1{0}.$SyncPattern1{1}.chr(ord($SyncPattern1{2}) & 0xFD).$SyncPattern1{3}; - } - - $framelength = false; - $framelength1 = strpos($MPEGaudioData, $SyncPattern1, 4); - $framelength2 = strpos($MPEGaudioData, $SyncPattern2, 4); - if ($framelength1 > 4) { - $framelength = $framelength1; - } - if (($framelength2 > 4) && ($framelength2 < $framelength1)) { - $framelength = $framelength2; - } - if (!$framelength) { - - // LAME 3.88 has a different value for modeextension on the first frame vs the rest - $framelength1 = strpos($MPEGaudioData, substr($SyncPattern1, 0, 3), 4); - $framelength2 = strpos($MPEGaudioData, substr($SyncPattern2, 0, 3), 4); - - if ($framelength1 > 4) { - $framelength = $framelength1; - } - if (($framelength2 > 4) && ($framelength2 < $framelength1)) { - $framelength = $framelength2; - } - if (!$framelength) { - $info['error'][] = 'Cannot find next free-format synch pattern ('.getid3_lib::PrintHexBytes($SyncPattern1).' or '.getid3_lib::PrintHexBytes($SyncPattern2).') after offset '.$offset; - return false; - } else { - $info['warning'][] = 'ModeExtension varies between first frame and other frames (known free-format issue in LAME 3.88)'; - $info['audio']['codec'] = 'LAME'; - $info['audio']['encoder'] = 'LAME3.88'; - $SyncPattern1 = substr($SyncPattern1, 0, 3); - $SyncPattern2 = substr($SyncPattern2, 0, 3); - } - } - - if ($deepscan) { - - $ActualFrameLengthValues = array(); - $nextoffset = $offset + $framelength; - while ($nextoffset < ($info['avdataend'] - 6)) { - fseek($this->getid3->fp, $nextoffset - 1, SEEK_SET); - $NextSyncPattern = fread($this->getid3->fp, 6); - if ((substr($NextSyncPattern, 1, strlen($SyncPattern1)) == $SyncPattern1) || (substr($NextSyncPattern, 1, strlen($SyncPattern2)) == $SyncPattern2)) { - // good - found where expected - $ActualFrameLengthValues[] = $framelength; - } elseif ((substr($NextSyncPattern, 0, strlen($SyncPattern1)) == $SyncPattern1) || (substr($NextSyncPattern, 0, strlen($SyncPattern2)) == $SyncPattern2)) { - // ok - found one byte earlier than expected (last frame wasn't padded, first frame was) - $ActualFrameLengthValues[] = ($framelength - 1); - $nextoffset--; - } elseif ((substr($NextSyncPattern, 2, strlen($SyncPattern1)) == $SyncPattern1) || (substr($NextSyncPattern, 2, strlen($SyncPattern2)) == $SyncPattern2)) { - // ok - found one byte later than expected (last frame was padded, first frame wasn't) - $ActualFrameLengthValues[] = ($framelength + 1); - $nextoffset++; - } else { - $info['error'][] = 'Did not find expected free-format sync pattern at offset '.$nextoffset; - return false; - } - $nextoffset += $framelength; - } - if (count($ActualFrameLengthValues) > 0) { - $framelength = intval(round(array_sum($ActualFrameLengthValues) / count($ActualFrameLengthValues))); - } - } - return $framelength; - } - - public function getOnlyMPEGaudioInfoBruteForce() { - $MPEGaudioHeaderDecodeCache = array(); - $MPEGaudioHeaderValidCache = array(); - $MPEGaudioHeaderLengthCache = array(); - $MPEGaudioVersionLookup = self::MPEGaudioVersionArray(); - $MPEGaudioLayerLookup = self::MPEGaudioLayerArray(); - $MPEGaudioBitrateLookup = self::MPEGaudioBitrateArray(); - $MPEGaudioFrequencyLookup = self::MPEGaudioFrequencyArray(); - $MPEGaudioChannelModeLookup = self::MPEGaudioChannelModeArray(); - $MPEGaudioModeExtensionLookup = self::MPEGaudioModeExtensionArray(); - $MPEGaudioEmphasisLookup = self::MPEGaudioEmphasisArray(); - $LongMPEGversionLookup = array(); - $LongMPEGlayerLookup = array(); - $LongMPEGbitrateLookup = array(); - $LongMPEGpaddingLookup = array(); - $LongMPEGfrequencyLookup = array(); - $Distribution['bitrate'] = array(); - $Distribution['frequency'] = array(); - $Distribution['layer'] = array(); - $Distribution['version'] = array(); - $Distribution['padding'] = array(); - - $info = &$this->getid3->info; - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - - $max_frames_scan = 5000; - $frames_scanned = 0; - - $previousvalidframe = $info['avdataoffset']; - while (ftell($this->getid3->fp) < $info['avdataend']) { - set_time_limit(30); - $head4 = fread($this->getid3->fp, 4); - if (strlen($head4) < 4) { - break; - } - if ($head4{0} != "\xFF") { - for ($i = 1; $i < 4; $i++) { - if ($head4{$i} == "\xFF") { - fseek($this->getid3->fp, $i - 4, SEEK_CUR); - continue 2; - } - } - continue; - } - if (!isset($MPEGaudioHeaderDecodeCache[$head4])) { - $MPEGaudioHeaderDecodeCache[$head4] = self::MPEGaudioHeaderDecode($head4); - } - if (!isset($MPEGaudioHeaderValidCache[$head4])) { - $MPEGaudioHeaderValidCache[$head4] = self::MPEGaudioHeaderValid($MPEGaudioHeaderDecodeCache[$head4], false, false); - } - if ($MPEGaudioHeaderValidCache[$head4]) { - - if (!isset($MPEGaudioHeaderLengthCache[$head4])) { - $LongMPEGversionLookup[$head4] = $MPEGaudioVersionLookup[$MPEGaudioHeaderDecodeCache[$head4]['version']]; - $LongMPEGlayerLookup[$head4] = $MPEGaudioLayerLookup[$MPEGaudioHeaderDecodeCache[$head4]['layer']]; - $LongMPEGbitrateLookup[$head4] = $MPEGaudioBitrateLookup[$LongMPEGversionLookup[$head4]][$LongMPEGlayerLookup[$head4]][$MPEGaudioHeaderDecodeCache[$head4]['bitrate']]; - $LongMPEGpaddingLookup[$head4] = (bool) $MPEGaudioHeaderDecodeCache[$head4]['padding']; - $LongMPEGfrequencyLookup[$head4] = $MPEGaudioFrequencyLookup[$LongMPEGversionLookup[$head4]][$MPEGaudioHeaderDecodeCache[$head4]['sample_rate']]; - $MPEGaudioHeaderLengthCache[$head4] = self::MPEGaudioFrameLength( - $LongMPEGbitrateLookup[$head4], - $LongMPEGversionLookup[$head4], - $LongMPEGlayerLookup[$head4], - $LongMPEGpaddingLookup[$head4], - $LongMPEGfrequencyLookup[$head4]); - } - if ($MPEGaudioHeaderLengthCache[$head4] > 4) { - $WhereWeWere = ftell($this->getid3->fp); - fseek($this->getid3->fp, $MPEGaudioHeaderLengthCache[$head4] - 4, SEEK_CUR); - $next4 = fread($this->getid3->fp, 4); - if ($next4{0} == "\xFF") { - if (!isset($MPEGaudioHeaderDecodeCache[$next4])) { - $MPEGaudioHeaderDecodeCache[$next4] = self::MPEGaudioHeaderDecode($next4); - } - if (!isset($MPEGaudioHeaderValidCache[$next4])) { - $MPEGaudioHeaderValidCache[$next4] = self::MPEGaudioHeaderValid($MPEGaudioHeaderDecodeCache[$next4], false, false); - } - if ($MPEGaudioHeaderValidCache[$next4]) { - fseek($this->getid3->fp, -4, SEEK_CUR); - - getid3_lib::safe_inc($Distribution['bitrate'][$LongMPEGbitrateLookup[$head4]]); - getid3_lib::safe_inc($Distribution['layer'][$LongMPEGlayerLookup[$head4]]); - getid3_lib::safe_inc($Distribution['version'][$LongMPEGversionLookup[$head4]]); - getid3_lib::safe_inc($Distribution['padding'][intval($LongMPEGpaddingLookup[$head4])]); - getid3_lib::safe_inc($Distribution['frequency'][$LongMPEGfrequencyLookup[$head4]]); - if ($max_frames_scan && (++$frames_scanned >= $max_frames_scan)) { - $pct_data_scanned = (ftell($this->getid3->fp) - $info['avdataoffset']) / ($info['avdataend'] - $info['avdataoffset']); - $info['warning'][] = 'too many MPEG audio frames to scan, only scanned first '.$max_frames_scan.' frames ('.number_format($pct_data_scanned * 100, 1).'% of file) and extrapolated distribution, playtime and bitrate may be incorrect.'; - foreach ($Distribution as $key1 => $value1) { - foreach ($value1 as $key2 => $value2) { - $Distribution[$key1][$key2] = round($value2 / $pct_data_scanned); - } - } - break; - } - continue; - } - } - unset($next4); - fseek($this->getid3->fp, $WhereWeWere - 3, SEEK_SET); - } - - } - } - foreach ($Distribution as $key => $value) { - ksort($Distribution[$key], SORT_NUMERIC); - } - ksort($Distribution['version'], SORT_STRING); - $info['mpeg']['audio']['bitrate_distribution'] = $Distribution['bitrate']; - $info['mpeg']['audio']['frequency_distribution'] = $Distribution['frequency']; - $info['mpeg']['audio']['layer_distribution'] = $Distribution['layer']; - $info['mpeg']['audio']['version_distribution'] = $Distribution['version']; - $info['mpeg']['audio']['padding_distribution'] = $Distribution['padding']; - if (count($Distribution['version']) > 1) { - $info['error'][] = 'Corrupt file - more than one MPEG version detected'; - } - if (count($Distribution['layer']) > 1) { - $info['error'][] = 'Corrupt file - more than one MPEG layer detected'; - } - if (count($Distribution['frequency']) > 1) { - $info['error'][] = 'Corrupt file - more than one MPEG sample rate detected'; - } - - - $bittotal = 0; - foreach ($Distribution['bitrate'] as $bitratevalue => $bitratecount) { - if ($bitratevalue != 'free') { - $bittotal += ($bitratevalue * $bitratecount); - } - } - $info['mpeg']['audio']['frame_count'] = array_sum($Distribution['bitrate']); - if ($info['mpeg']['audio']['frame_count'] == 0) { - $info['error'][] = 'no MPEG audio frames found'; - return false; - } - $info['mpeg']['audio']['bitrate'] = ($bittotal / $info['mpeg']['audio']['frame_count']); - $info['mpeg']['audio']['bitrate_mode'] = ((count($Distribution['bitrate']) > 0) ? 'vbr' : 'cbr'); - $info['mpeg']['audio']['sample_rate'] = getid3_lib::array_max($Distribution['frequency'], true); - - $info['audio']['bitrate'] = $info['mpeg']['audio']['bitrate']; - $info['audio']['bitrate_mode'] = $info['mpeg']['audio']['bitrate_mode']; - $info['audio']['sample_rate'] = $info['mpeg']['audio']['sample_rate']; - $info['audio']['dataformat'] = 'mp'.getid3_lib::array_max($Distribution['layer'], true); - $info['fileformat'] = $info['audio']['dataformat']; - - return true; - } - - - public function getOnlyMPEGaudioInfo($avdataoffset, $BitrateHistogram=false) { - // looks for synch, decodes MPEG audio header - - $info = &$this->getid3->info; - - static $MPEGaudioVersionLookup; - static $MPEGaudioLayerLookup; - static $MPEGaudioBitrateLookup; - if (empty($MPEGaudioVersionLookup)) { - $MPEGaudioVersionLookup = self::MPEGaudioVersionArray(); - $MPEGaudioLayerLookup = self::MPEGaudioLayerArray(); - $MPEGaudioBitrateLookup = self::MPEGaudioBitrateArray(); - - } - - fseek($this->getid3->fp, $avdataoffset, SEEK_SET); - $sync_seek_buffer_size = min(128 * 1024, $info['avdataend'] - $avdataoffset); - if ($sync_seek_buffer_size <= 0) { - $info['error'][] = 'Invalid $sync_seek_buffer_size at offset '.$avdataoffset; - return false; - } - $header = fread($this->getid3->fp, $sync_seek_buffer_size); - $sync_seek_buffer_size = strlen($header); - $SynchSeekOffset = 0; - while ($SynchSeekOffset < $sync_seek_buffer_size) { - if ((($avdataoffset + $SynchSeekOffset) < $info['avdataend']) && !feof($this->getid3->fp)) { - - if ($SynchSeekOffset > $sync_seek_buffer_size) { - // if a synch's not found within the first 128k bytes, then give up - $info['error'][] = 'Could not find valid MPEG audio synch within the first '.round($sync_seek_buffer_size / 1024).'kB'; - if (isset($info['audio']['bitrate'])) { - unset($info['audio']['bitrate']); - } - if (isset($info['mpeg']['audio'])) { - unset($info['mpeg']['audio']); - } - if (empty($info['mpeg'])) { - unset($info['mpeg']); - } - return false; - - } elseif (feof($this->getid3->fp)) { - - $info['error'][] = 'Could not find valid MPEG audio synch before end of file'; - if (isset($info['audio']['bitrate'])) { - unset($info['audio']['bitrate']); - } - if (isset($info['mpeg']['audio'])) { - unset($info['mpeg']['audio']); - } - if (isset($info['mpeg']) && (!is_array($info['mpeg']) || (count($info['mpeg']) == 0))) { - unset($info['mpeg']); - } - return false; - } - } - - if (($SynchSeekOffset + 1) >= strlen($header)) { - $info['error'][] = 'Could not find valid MPEG synch before end of file'; - return false; - } - - if (($header{$SynchSeekOffset} == "\xFF") && ($header{($SynchSeekOffset + 1)} > "\xE0")) { // synch detected - if (!isset($FirstFrameThisfileInfo) && !isset($info['mpeg']['audio'])) { - $FirstFrameThisfileInfo = $info; - $FirstFrameAVDataOffset = $avdataoffset + $SynchSeekOffset; - if (!$this->decodeMPEGaudioHeader($FirstFrameAVDataOffset, $FirstFrameThisfileInfo, false)) { - // if this is the first valid MPEG-audio frame, save it in case it's a VBR header frame and there's - // garbage between this frame and a valid sequence of MPEG-audio frames, to be restored below - unset($FirstFrameThisfileInfo); - } - } - - $dummy = $info; // only overwrite real data if valid header found - if ($this->decodeMPEGaudioHeader($avdataoffset + $SynchSeekOffset, $dummy, true)) { - $info = $dummy; - $info['avdataoffset'] = $avdataoffset + $SynchSeekOffset; - switch (isset($info['fileformat']) ? $info['fileformat'] : '') { - case '': - case 'id3': - case 'ape': - case 'mp3': - $info['fileformat'] = 'mp3'; - $info['audio']['dataformat'] = 'mp3'; - break; - } - if (isset($FirstFrameThisfileInfo['mpeg']['audio']['bitrate_mode']) && ($FirstFrameThisfileInfo['mpeg']['audio']['bitrate_mode'] == 'vbr')) { - if (!(abs($info['audio']['bitrate'] - $FirstFrameThisfileInfo['audio']['bitrate']) <= 1)) { - // If there is garbage data between a valid VBR header frame and a sequence - // of valid MPEG-audio frames the VBR data is no longer discarded. - $info = $FirstFrameThisfileInfo; - $info['avdataoffset'] = $FirstFrameAVDataOffset; - $info['fileformat'] = 'mp3'; - $info['audio']['dataformat'] = 'mp3'; - $dummy = $info; - unset($dummy['mpeg']['audio']); - $GarbageOffsetStart = $FirstFrameAVDataOffset + $FirstFrameThisfileInfo['mpeg']['audio']['framelength']; - $GarbageOffsetEnd = $avdataoffset + $SynchSeekOffset; - if ($this->decodeMPEGaudioHeader($GarbageOffsetEnd, $dummy, true, true)) { - $info = $dummy; - $info['avdataoffset'] = $GarbageOffsetEnd; - $info['warning'][] = 'apparently-valid VBR header not used because could not find '.GETID3_MP3_VALID_CHECK_FRAMES.' consecutive MPEG-audio frames immediately after VBR header (garbage data for '.($GarbageOffsetEnd - $GarbageOffsetStart).' bytes between '.$GarbageOffsetStart.' and '.$GarbageOffsetEnd.'), but did find valid CBR stream starting at '.$GarbageOffsetEnd; - } else { - $info['warning'][] = 'using data from VBR header even though could not find '.GETID3_MP3_VALID_CHECK_FRAMES.' consecutive MPEG-audio frames immediately after VBR header (garbage data for '.($GarbageOffsetEnd - $GarbageOffsetStart).' bytes between '.$GarbageOffsetStart.' and '.$GarbageOffsetEnd.')'; - } - } - } - if (isset($info['mpeg']['audio']['bitrate_mode']) && ($info['mpeg']['audio']['bitrate_mode'] == 'vbr') && !isset($info['mpeg']['audio']['VBR_method'])) { - // VBR file with no VBR header - $BitrateHistogram = true; - } - - if ($BitrateHistogram) { - - $info['mpeg']['audio']['stereo_distribution'] = array('stereo'=>0, 'joint stereo'=>0, 'dual channel'=>0, 'mono'=>0); - $info['mpeg']['audio']['version_distribution'] = array('1'=>0, '2'=>0, '2.5'=>0); - - if ($info['mpeg']['audio']['version'] == '1') { - if ($info['mpeg']['audio']['layer'] == 3) { - $info['mpeg']['audio']['bitrate_distribution'] = array('free'=>0, 32000=>0, 40000=>0, 48000=>0, 56000=>0, 64000=>0, 80000=>0, 96000=>0, 112000=>0, 128000=>0, 160000=>0, 192000=>0, 224000=>0, 256000=>0, 320000=>0); - } elseif ($info['mpeg']['audio']['layer'] == 2) { - $info['mpeg']['audio']['bitrate_distribution'] = array('free'=>0, 32000=>0, 48000=>0, 56000=>0, 64000=>0, 80000=>0, 96000=>0, 112000=>0, 128000=>0, 160000=>0, 192000=>0, 224000=>0, 256000=>0, 320000=>0, 384000=>0); - } elseif ($info['mpeg']['audio']['layer'] == 1) { - $info['mpeg']['audio']['bitrate_distribution'] = array('free'=>0, 32000=>0, 64000=>0, 96000=>0, 128000=>0, 160000=>0, 192000=>0, 224000=>0, 256000=>0, 288000=>0, 320000=>0, 352000=>0, 384000=>0, 416000=>0, 448000=>0); - } - } elseif ($info['mpeg']['audio']['layer'] == 1) { - $info['mpeg']['audio']['bitrate_distribution'] = array('free'=>0, 32000=>0, 48000=>0, 56000=>0, 64000=>0, 80000=>0, 96000=>0, 112000=>0, 128000=>0, 144000=>0, 160000=>0, 176000=>0, 192000=>0, 224000=>0, 256000=>0); - } else { - $info['mpeg']['audio']['bitrate_distribution'] = array('free'=>0, 8000=>0, 16000=>0, 24000=>0, 32000=>0, 40000=>0, 48000=>0, 56000=>0, 64000=>0, 80000=>0, 96000=>0, 112000=>0, 128000=>0, 144000=>0, 160000=>0); - } - - $dummy = array('error'=>$info['error'], 'warning'=>$info['warning'], 'avdataend'=>$info['avdataend'], 'avdataoffset'=>$info['avdataoffset']); - $synchstartoffset = $info['avdataoffset']; - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - - // you can play with these numbers: - $max_frames_scan = 50000; - $max_scan_segments = 10; - - // don't play with these numbers: - $FastMode = false; - $SynchErrorsFound = 0; - $frames_scanned = 0; - $this_scan_segment = 0; - $frames_scan_per_segment = ceil($max_frames_scan / $max_scan_segments); - $pct_data_scanned = 0; - for ($current_segment = 0; $current_segment < $max_scan_segments; $current_segment++) { - $frames_scanned_this_segment = 0; - if (ftell($this->getid3->fp) >= $info['avdataend']) { - break; - } - $scan_start_offset[$current_segment] = max(ftell($this->getid3->fp), $info['avdataoffset'] + round($current_segment * (($info['avdataend'] - $info['avdataoffset']) / $max_scan_segments))); - if ($current_segment > 0) { - fseek($this->getid3->fp, $scan_start_offset[$current_segment], SEEK_SET); - $buffer_4k = fread($this->getid3->fp, 4096); - for ($j = 0; $j < (strlen($buffer_4k) - 4); $j++) { - if (($buffer_4k{$j} == "\xFF") && ($buffer_4k{($j + 1)} > "\xE0")) { // synch detected - if ($this->decodeMPEGaudioHeader($scan_start_offset[$current_segment] + $j, $dummy, false, false, $FastMode)) { - $calculated_next_offset = $scan_start_offset[$current_segment] + $j + $dummy['mpeg']['audio']['framelength']; - if ($this->decodeMPEGaudioHeader($calculated_next_offset, $dummy, false, false, $FastMode)) { - $scan_start_offset[$current_segment] += $j; - break; - } - } - } - } - } - $synchstartoffset = $scan_start_offset[$current_segment]; - while ($this->decodeMPEGaudioHeader($synchstartoffset, $dummy, false, false, $FastMode)) { - $FastMode = true; - $thisframebitrate = $MPEGaudioBitrateLookup[$MPEGaudioVersionLookup[$dummy['mpeg']['audio']['raw']['version']]][$MPEGaudioLayerLookup[$dummy['mpeg']['audio']['raw']['layer']]][$dummy['mpeg']['audio']['raw']['bitrate']]; - - if (empty($dummy['mpeg']['audio']['framelength'])) { - $SynchErrorsFound++; - $synchstartoffset++; - } else { - getid3_lib::safe_inc($info['mpeg']['audio']['bitrate_distribution'][$thisframebitrate]); - getid3_lib::safe_inc($info['mpeg']['audio']['stereo_distribution'][$dummy['mpeg']['audio']['channelmode']]); - getid3_lib::safe_inc($info['mpeg']['audio']['version_distribution'][$dummy['mpeg']['audio']['version']]); - $synchstartoffset += $dummy['mpeg']['audio']['framelength']; - } - $frames_scanned++; - if ($frames_scan_per_segment && (++$frames_scanned_this_segment >= $frames_scan_per_segment)) { - $this_pct_scanned = (ftell($this->getid3->fp) - $scan_start_offset[$current_segment]) / ($info['avdataend'] - $info['avdataoffset']); - if (($current_segment == 0) && (($this_pct_scanned * $max_scan_segments) >= 1)) { - // file likely contains < $max_frames_scan, just scan as one segment - $max_scan_segments = 1; - $frames_scan_per_segment = $max_frames_scan; - } else { - $pct_data_scanned += $this_pct_scanned; - break; - } - } - } - } - if ($pct_data_scanned > 0) { - $info['warning'][] = 'too many MPEG audio frames to scan, only scanned '.$frames_scanned.' frames in '.$max_scan_segments.' segments ('.number_format($pct_data_scanned * 100, 1).'% of file) and extrapolated distribution, playtime and bitrate may be incorrect.'; - foreach ($info['mpeg']['audio'] as $key1 => $value1) { - if (!preg_match('#_distribution$#i', $key1)) { - continue; - } - foreach ($value1 as $key2 => $value2) { - $info['mpeg']['audio'][$key1][$key2] = round($value2 / $pct_data_scanned); - } - } - } - - if ($SynchErrorsFound > 0) { - $info['warning'][] = 'Found '.$SynchErrorsFound.' synch errors in histogram analysis'; - //return false; - } - - $bittotal = 0; - $framecounter = 0; - foreach ($info['mpeg']['audio']['bitrate_distribution'] as $bitratevalue => $bitratecount) { - $framecounter += $bitratecount; - if ($bitratevalue != 'free') { - $bittotal += ($bitratevalue * $bitratecount); - } - } - if ($framecounter == 0) { - $info['error'][] = 'Corrupt MP3 file: framecounter == zero'; - return false; - } - $info['mpeg']['audio']['frame_count'] = getid3_lib::CastAsInt($framecounter); - $info['mpeg']['audio']['bitrate'] = ($bittotal / $framecounter); - - $info['audio']['bitrate'] = $info['mpeg']['audio']['bitrate']; - - - // Definitively set VBR vs CBR, even if the Xing/LAME/VBRI header says differently - $distinct_bitrates = 0; - foreach ($info['mpeg']['audio']['bitrate_distribution'] as $bitrate_value => $bitrate_count) { - if ($bitrate_count > 0) { - $distinct_bitrates++; - } - } - if ($distinct_bitrates > 1) { - $info['mpeg']['audio']['bitrate_mode'] = 'vbr'; - } else { - $info['mpeg']['audio']['bitrate_mode'] = 'cbr'; - } - $info['audio']['bitrate_mode'] = $info['mpeg']['audio']['bitrate_mode']; - - } - - break; // exit while() - } - } - - $SynchSeekOffset++; - if (($avdataoffset + $SynchSeekOffset) >= $info['avdataend']) { - // end of file/data - - if (empty($info['mpeg']['audio'])) { - - $info['error'][] = 'could not find valid MPEG synch before end of file'; - if (isset($info['audio']['bitrate'])) { - unset($info['audio']['bitrate']); - } - if (isset($info['mpeg']['audio'])) { - unset($info['mpeg']['audio']); - } - if (isset($info['mpeg']) && (!is_array($info['mpeg']) || empty($info['mpeg']))) { - unset($info['mpeg']); - } - return false; - - } - break; - } - - } - $info['audio']['channels'] = $info['mpeg']['audio']['channels']; - $info['audio']['channelmode'] = $info['mpeg']['audio']['channelmode']; - $info['audio']['sample_rate'] = $info['mpeg']['audio']['sample_rate']; - return true; - } - - - public static function MPEGaudioVersionArray() { - static $MPEGaudioVersion = array('2.5', false, '2', '1'); - return $MPEGaudioVersion; - } - - public static function MPEGaudioLayerArray() { - static $MPEGaudioLayer = array(false, 3, 2, 1); - return $MPEGaudioLayer; - } - - public static function MPEGaudioBitrateArray() { - static $MPEGaudioBitrate; - if (empty($MPEGaudioBitrate)) { - $MPEGaudioBitrate = array ( - '1' => array (1 => array('free', 32000, 64000, 96000, 128000, 160000, 192000, 224000, 256000, 288000, 320000, 352000, 384000, 416000, 448000), - 2 => array('free', 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, 320000, 384000), - 3 => array('free', 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 160000, 192000, 224000, 256000, 320000) - ), - - '2' => array (1 => array('free', 32000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000, 176000, 192000, 224000, 256000), - 2 => array('free', 8000, 16000, 24000, 32000, 40000, 48000, 56000, 64000, 80000, 96000, 112000, 128000, 144000, 160000), - ) - ); - $MPEGaudioBitrate['2'][3] = $MPEGaudioBitrate['2'][2]; - $MPEGaudioBitrate['2.5'] = $MPEGaudioBitrate['2']; - } - return $MPEGaudioBitrate; - } - - public static function MPEGaudioFrequencyArray() { - static $MPEGaudioFrequency; - if (empty($MPEGaudioFrequency)) { - $MPEGaudioFrequency = array ( - '1' => array(44100, 48000, 32000), - '2' => array(22050, 24000, 16000), - '2.5' => array(11025, 12000, 8000) - ); - } - return $MPEGaudioFrequency; - } - - public static function MPEGaudioChannelModeArray() { - static $MPEGaudioChannelMode = array('stereo', 'joint stereo', 'dual channel', 'mono'); - return $MPEGaudioChannelMode; - } - - public static function MPEGaudioModeExtensionArray() { - static $MPEGaudioModeExtension; - if (empty($MPEGaudioModeExtension)) { - $MPEGaudioModeExtension = array ( - 1 => array('4-31', '8-31', '12-31', '16-31'), - 2 => array('4-31', '8-31', '12-31', '16-31'), - 3 => array('', 'IS', 'MS', 'IS+MS') - ); - } - return $MPEGaudioModeExtension; - } - - public static function MPEGaudioEmphasisArray() { - static $MPEGaudioEmphasis = array('none', '50/15ms', false, 'CCIT J.17'); - return $MPEGaudioEmphasis; - } - - public static function MPEGaudioHeaderBytesValid($head4, $allowBitrate15=false) { - return self::MPEGaudioHeaderValid(self::MPEGaudioHeaderDecode($head4), false, $allowBitrate15); - } - - public static function MPEGaudioHeaderValid($rawarray, $echoerrors=false, $allowBitrate15=false) { - if (($rawarray['synch'] & 0x0FFE) != 0x0FFE) { - return false; - } - - static $MPEGaudioVersionLookup; - static $MPEGaudioLayerLookup; - static $MPEGaudioBitrateLookup; - static $MPEGaudioFrequencyLookup; - static $MPEGaudioChannelModeLookup; - static $MPEGaudioModeExtensionLookup; - static $MPEGaudioEmphasisLookup; - if (empty($MPEGaudioVersionLookup)) { - $MPEGaudioVersionLookup = self::MPEGaudioVersionArray(); - $MPEGaudioLayerLookup = self::MPEGaudioLayerArray(); - $MPEGaudioBitrateLookup = self::MPEGaudioBitrateArray(); - $MPEGaudioFrequencyLookup = self::MPEGaudioFrequencyArray(); - $MPEGaudioChannelModeLookup = self::MPEGaudioChannelModeArray(); - $MPEGaudioModeExtensionLookup = self::MPEGaudioModeExtensionArray(); - $MPEGaudioEmphasisLookup = self::MPEGaudioEmphasisArray(); - } - - if (isset($MPEGaudioVersionLookup[$rawarray['version']])) { - $decodedVersion = $MPEGaudioVersionLookup[$rawarray['version']]; - } else { - echo ($echoerrors ? "\n".'invalid Version ('.$rawarray['version'].')' : ''); - return false; - } - if (isset($MPEGaudioLayerLookup[$rawarray['layer']])) { - $decodedLayer = $MPEGaudioLayerLookup[$rawarray['layer']]; - } else { - echo ($echoerrors ? "\n".'invalid Layer ('.$rawarray['layer'].')' : ''); - return false; - } - if (!isset($MPEGaudioBitrateLookup[$decodedVersion][$decodedLayer][$rawarray['bitrate']])) { - echo ($echoerrors ? "\n".'invalid Bitrate ('.$rawarray['bitrate'].')' : ''); - if ($rawarray['bitrate'] == 15) { - // known issue in LAME 3.90 - 3.93.1 where free-format has bitrate ID of 15 instead of 0 - // let it go through here otherwise file will not be identified - if (!$allowBitrate15) { - return false; - } - } else { - return false; - } - } - if (!isset($MPEGaudioFrequencyLookup[$decodedVersion][$rawarray['sample_rate']])) { - echo ($echoerrors ? "\n".'invalid Frequency ('.$rawarray['sample_rate'].')' : ''); - return false; - } - if (!isset($MPEGaudioChannelModeLookup[$rawarray['channelmode']])) { - echo ($echoerrors ? "\n".'invalid ChannelMode ('.$rawarray['channelmode'].')' : ''); - return false; - } - if (!isset($MPEGaudioModeExtensionLookup[$decodedLayer][$rawarray['modeextension']])) { - echo ($echoerrors ? "\n".'invalid Mode Extension ('.$rawarray['modeextension'].')' : ''); - return false; - } - if (!isset($MPEGaudioEmphasisLookup[$rawarray['emphasis']])) { - echo ($echoerrors ? "\n".'invalid Emphasis ('.$rawarray['emphasis'].')' : ''); - return false; - } - // These are just either set or not set, you can't mess that up :) - // $rawarray['protection']; - // $rawarray['padding']; - // $rawarray['private']; - // $rawarray['copyright']; - // $rawarray['original']; - - return true; - } - - public static function MPEGaudioHeaderDecode($Header4Bytes) { - // AAAA AAAA AAAB BCCD EEEE FFGH IIJJ KLMM - // A - Frame sync (all bits set) - // B - MPEG Audio version ID - // C - Layer description - // D - Protection bit - // E - Bitrate index - // F - Sampling rate frequency index - // G - Padding bit - // H - Private bit - // I - Channel Mode - // J - Mode extension (Only if Joint stereo) - // K - Copyright - // L - Original - // M - Emphasis - - if (strlen($Header4Bytes) != 4) { - return false; - } - - $MPEGrawHeader['synch'] = (getid3_lib::BigEndian2Int(substr($Header4Bytes, 0, 2)) & 0xFFE0) >> 4; - $MPEGrawHeader['version'] = (ord($Header4Bytes{1}) & 0x18) >> 3; // BB - $MPEGrawHeader['layer'] = (ord($Header4Bytes{1}) & 0x06) >> 1; // CC - $MPEGrawHeader['protection'] = (ord($Header4Bytes{1}) & 0x01); // D - $MPEGrawHeader['bitrate'] = (ord($Header4Bytes{2}) & 0xF0) >> 4; // EEEE - $MPEGrawHeader['sample_rate'] = (ord($Header4Bytes{2}) & 0x0C) >> 2; // FF - $MPEGrawHeader['padding'] = (ord($Header4Bytes{2}) & 0x02) >> 1; // G - $MPEGrawHeader['private'] = (ord($Header4Bytes{2}) & 0x01); // H - $MPEGrawHeader['channelmode'] = (ord($Header4Bytes{3}) & 0xC0) >> 6; // II - $MPEGrawHeader['modeextension'] = (ord($Header4Bytes{3}) & 0x30) >> 4; // JJ - $MPEGrawHeader['copyright'] = (ord($Header4Bytes{3}) & 0x08) >> 3; // K - $MPEGrawHeader['original'] = (ord($Header4Bytes{3}) & 0x04) >> 2; // L - $MPEGrawHeader['emphasis'] = (ord($Header4Bytes{3}) & 0x03); // MM - - return $MPEGrawHeader; - } - - public static function MPEGaudioFrameLength(&$bitrate, &$version, &$layer, $padding, &$samplerate) { - static $AudioFrameLengthCache = array(); - - if (!isset($AudioFrameLengthCache[$bitrate][$version][$layer][$padding][$samplerate])) { - $AudioFrameLengthCache[$bitrate][$version][$layer][$padding][$samplerate] = false; - if ($bitrate != 'free') { - - if ($version == '1') { - - if ($layer == '1') { - - // For Layer I slot is 32 bits long - $FrameLengthCoefficient = 48; - $SlotLength = 4; - - } else { // Layer 2 / 3 - - // for Layer 2 and Layer 3 slot is 8 bits long. - $FrameLengthCoefficient = 144; - $SlotLength = 1; - - } - - } else { // MPEG-2 / MPEG-2.5 - - if ($layer == '1') { - - // For Layer I slot is 32 bits long - $FrameLengthCoefficient = 24; - $SlotLength = 4; - - } elseif ($layer == '2') { - - // for Layer 2 and Layer 3 slot is 8 bits long. - $FrameLengthCoefficient = 144; - $SlotLength = 1; - - } else { // layer 3 - - // for Layer 2 and Layer 3 slot is 8 bits long. - $FrameLengthCoefficient = 72; - $SlotLength = 1; - - } - - } - - // FrameLengthInBytes = ((Coefficient * BitRate) / SampleRate) + Padding - if ($samplerate > 0) { - $NewFramelength = ($FrameLengthCoefficient * $bitrate) / $samplerate; - $NewFramelength = floor($NewFramelength / $SlotLength) * $SlotLength; // round to next-lower multiple of SlotLength (1 byte for Layer 2/3, 4 bytes for Layer I) - if ($padding) { - $NewFramelength += $SlotLength; - } - $AudioFrameLengthCache[$bitrate][$version][$layer][$padding][$samplerate] = (int) $NewFramelength; - } - } - } - return $AudioFrameLengthCache[$bitrate][$version][$layer][$padding][$samplerate]; - } - - public static function ClosestStandardMP3Bitrate($bit_rate) { - static $standard_bit_rates = array (320000, 256000, 224000, 192000, 160000, 128000, 112000, 96000, 80000, 64000, 56000, 48000, 40000, 32000, 24000, 16000, 8000); - static $bit_rate_table = array (0=>'-'); - $round_bit_rate = intval(round($bit_rate, -3)); - if (!isset($bit_rate_table[$round_bit_rate])) { - if ($round_bit_rate > max($standard_bit_rates)) { - $bit_rate_table[$round_bit_rate] = round($bit_rate, 2 - strlen($bit_rate)); - } else { - $bit_rate_table[$round_bit_rate] = max($standard_bit_rates); - foreach ($standard_bit_rates as $standard_bit_rate) { - if ($round_bit_rate >= $standard_bit_rate + (($bit_rate_table[$round_bit_rate] - $standard_bit_rate) / 2)) { - break; - } - $bit_rate_table[$round_bit_rate] = $standard_bit_rate; - } - } - } - return $bit_rate_table[$round_bit_rate]; - } - - public static function XingVBRidOffset($version, $channelmode) { - static $XingVBRidOffsetCache = array(); - if (empty($XingVBRidOffset)) { - $XingVBRidOffset = array ( - '1' => array ('mono' => 0x15, // 4 + 17 = 21 - 'stereo' => 0x24, // 4 + 32 = 36 - 'joint stereo' => 0x24, - 'dual channel' => 0x24 - ), - - '2' => array ('mono' => 0x0D, // 4 + 9 = 13 - 'stereo' => 0x15, // 4 + 17 = 21 - 'joint stereo' => 0x15, - 'dual channel' => 0x15 - ), - - '2.5' => array ('mono' => 0x15, - 'stereo' => 0x15, - 'joint stereo' => 0x15, - 'dual channel' => 0x15 - ) - ); - } - return $XingVBRidOffset[$version][$channelmode]; - } - - public static function LAMEvbrMethodLookup($VBRmethodID) { - static $LAMEvbrMethodLookup = array( - 0x00 => 'unknown', - 0x01 => 'cbr', - 0x02 => 'abr', - 0x03 => 'vbr-old / vbr-rh', - 0x04 => 'vbr-new / vbr-mtrh', - 0x05 => 'vbr-mt', - 0x06 => 'vbr (full vbr method 4)', - 0x08 => 'cbr (constant bitrate 2 pass)', - 0x09 => 'abr (2 pass)', - 0x0F => 'reserved' - ); - return (isset($LAMEvbrMethodLookup[$VBRmethodID]) ? $LAMEvbrMethodLookup[$VBRmethodID] : ''); - } - - public static function LAMEmiscStereoModeLookup($StereoModeID) { - static $LAMEmiscStereoModeLookup = array( - 0 => 'mono', - 1 => 'stereo', - 2 => 'dual mono', - 3 => 'joint stereo', - 4 => 'forced stereo', - 5 => 'auto', - 6 => 'intensity stereo', - 7 => 'other' - ); - return (isset($LAMEmiscStereoModeLookup[$StereoModeID]) ? $LAMEmiscStereoModeLookup[$StereoModeID] : ''); - } - - public static function LAMEmiscSourceSampleFrequencyLookup($SourceSampleFrequencyID) { - static $LAMEmiscSourceSampleFrequencyLookup = array( - 0 => '<= 32 kHz', - 1 => '44.1 kHz', - 2 => '48 kHz', - 3 => '> 48kHz' - ); - return (isset($LAMEmiscSourceSampleFrequencyLookup[$SourceSampleFrequencyID]) ? $LAMEmiscSourceSampleFrequencyLookup[$SourceSampleFrequencyID] : ''); - } - - public static function LAMEsurroundInfoLookup($SurroundInfoID) { - static $LAMEsurroundInfoLookup = array( - 0 => 'no surround info', - 1 => 'DPL encoding', - 2 => 'DPL2 encoding', - 3 => 'Ambisonic encoding' - ); - return (isset($LAMEsurroundInfoLookup[$SurroundInfoID]) ? $LAMEsurroundInfoLookup[$SurroundInfoID] : 'reserved'); - } - - public static function LAMEpresetUsedLookup($LAMEtag) { - - if ($LAMEtag['preset_used_id'] == 0) { - // no preset used (LAME >=3.93) - // no preset recorded (LAME <3.93) - return ''; - } - $LAMEpresetUsedLookup = array(); - - ///// THIS PART CANNOT BE STATIC . - for ($i = 8; $i <= 320; $i++) { - switch ($LAMEtag['vbr_method']) { - case 'cbr': - $LAMEpresetUsedLookup[$i] = '--alt-preset '.$LAMEtag['vbr_method'].' '.$i; - break; - case 'abr': - default: // other VBR modes shouldn't be here(?) - $LAMEpresetUsedLookup[$i] = '--alt-preset '.$i; - break; - } - } - - // named old-style presets (studio, phone, voice, etc) are handled in GuessEncoderOptions() - - // named alt-presets - $LAMEpresetUsedLookup[1000] = '--r3mix'; - $LAMEpresetUsedLookup[1001] = '--alt-preset standard'; - $LAMEpresetUsedLookup[1002] = '--alt-preset extreme'; - $LAMEpresetUsedLookup[1003] = '--alt-preset insane'; - $LAMEpresetUsedLookup[1004] = '--alt-preset fast standard'; - $LAMEpresetUsedLookup[1005] = '--alt-preset fast extreme'; - $LAMEpresetUsedLookup[1006] = '--alt-preset medium'; - $LAMEpresetUsedLookup[1007] = '--alt-preset fast medium'; - - // LAME 3.94 additions/changes - $LAMEpresetUsedLookup[1010] = '--preset portable'; // 3.94a15 Oct 21 2003 - $LAMEpresetUsedLookup[1015] = '--preset radio'; // 3.94a15 Oct 21 2003 - - $LAMEpresetUsedLookup[320] = '--preset insane'; // 3.94a15 Nov 12 2003 - $LAMEpresetUsedLookup[410] = '-V9'; - $LAMEpresetUsedLookup[420] = '-V8'; - $LAMEpresetUsedLookup[440] = '-V6'; - $LAMEpresetUsedLookup[430] = '--preset radio'; // 3.94a15 Nov 12 2003 - $LAMEpresetUsedLookup[450] = '--preset '.(($LAMEtag['raw']['vbr_method'] == 4) ? 'fast ' : '').'portable'; // 3.94a15 Nov 12 2003 - $LAMEpresetUsedLookup[460] = '--preset '.(($LAMEtag['raw']['vbr_method'] == 4) ? 'fast ' : '').'medium'; // 3.94a15 Nov 12 2003 - $LAMEpresetUsedLookup[470] = '--r3mix'; // 3.94b1 Dec 18 2003 - $LAMEpresetUsedLookup[480] = '--preset '.(($LAMEtag['raw']['vbr_method'] == 4) ? 'fast ' : '').'standard'; // 3.94a15 Nov 12 2003 - $LAMEpresetUsedLookup[490] = '-V1'; - $LAMEpresetUsedLookup[500] = '--preset '.(($LAMEtag['raw']['vbr_method'] == 4) ? 'fast ' : '').'extreme'; // 3.94a15 Nov 12 2003 - - return (isset($LAMEpresetUsedLookup[$LAMEtag['preset_used_id']]) ? $LAMEpresetUsedLookup[$LAMEtag['preset_used_id']] : 'new/unknown preset: '.$LAMEtag['preset_used_id'].' - report to info@getid3.org'); - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.mpc.php b/src/Classes/Vendor/getid3/module.audio.mpc.php deleted file mode 100755 index 8ab421ebe..000000000 --- a/src/Classes/Vendor/getid3/module.audio.mpc.php +++ /dev/null @@ -1,506 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.mpc.php // -// module for analyzing Musepack/MPEG+ Audio files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_mpc extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - $info['mpc']['header'] = array(); - $thisfile_mpc_header = &$info['mpc']['header']; - - $info['fileformat'] = 'mpc'; - $info['audio']['dataformat'] = 'mpc'; - $info['audio']['bitrate_mode'] = 'vbr'; - $info['audio']['channels'] = 2; // up to SV7 the format appears to have been hardcoded for stereo only - $info['audio']['lossless'] = false; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $MPCheaderData = fread($this->getid3->fp, 4); - $info['mpc']['header']['preamble'] = substr($MPCheaderData, 0, 4); // should be 'MPCK' (SV8) or 'MP+' (SV7), otherwise possible stream data (SV4-SV6) - if (preg_match('#^MPCK#', $info['mpc']['header']['preamble'])) { - - // this is SV8 - return $this->ParseMPCsv8(); - - } elseif (preg_match('#^MP\+#', $info['mpc']['header']['preamble'])) { - - // this is SV7 - return $this->ParseMPCsv7(); - - } elseif (preg_match('/^[\x00\x01\x10\x11\x40\x41\x50\x51\x80\x81\x90\x91\xC0\xC1\xD0\xD1][\x20-37][\x00\x20\x40\x60\x80\xA0\xC0\xE0]/s', $MPCheaderData)) { - - // this is SV4 - SV6, handle seperately - return $this->ParseMPCsv6(); - - } else { - - $info['error'][] = 'Expecting "MP+" or "MPCK" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($MPCheaderData, 0, 4)).'"'; - unset($info['fileformat']); - unset($info['mpc']); - return false; - - } - return false; - } - - - public function ParseMPCsv8() { - // this is SV8 - // http://trac.musepack.net/trac/wiki/SV8Specification - - $info = &$this->getid3->info; - $thisfile_mpc_header = &$info['mpc']['header']; - - $keyNameSize = 2; - $maxHandledPacketLength = 9; // specs say: "n*8; 0 < n < 10" - - $offset = ftell($this->getid3->fp); - while ($offset < $info['avdataend']) { - $thisPacket = array(); - $thisPacket['offset'] = $offset; - $packet_offset = 0; - - // Size is a variable-size field, could be 1-4 bytes (possibly more?) - // read enough data in and figure out the exact size later - $MPCheaderData = fread($this->getid3->fp, $keyNameSize + $maxHandledPacketLength); - $packet_offset += $keyNameSize; - $thisPacket['key'] = substr($MPCheaderData, 0, $keyNameSize); - $thisPacket['key_name'] = $this->MPCsv8PacketName($thisPacket['key']); - if ($thisPacket['key'] == $thisPacket['key_name']) { - $info['error'][] = 'Found unexpected key value "'.$thisPacket['key'].'" at offset '.$thisPacket['offset']; - return false; - } - $packetLength = 0; - $thisPacket['packet_size'] = $this->SV8variableLengthInteger(substr($MPCheaderData, $keyNameSize), $packetLength); // includes keyname and packet_size field - if ($thisPacket['packet_size'] === false) { - $info['error'][] = 'Did not find expected packet length within '.$maxHandledPacketLength.' bytes at offset '.($thisPacket['offset'] + $keyNameSize); - return false; - } - $packet_offset += $packetLength; - $offset += $thisPacket['packet_size']; - - switch ($thisPacket['key']) { - case 'SH': // Stream Header - $moreBytesToRead = $thisPacket['packet_size'] - $keyNameSize - $maxHandledPacketLength; - if ($moreBytesToRead > 0) { - $MPCheaderData .= fread($this->getid3->fp, $moreBytesToRead); - } - $thisPacket['crc'] = getid3_lib::BigEndian2Int(substr($MPCheaderData, $packet_offset, 4)); - $packet_offset += 4; - $thisPacket['stream_version'] = getid3_lib::BigEndian2Int(substr($MPCheaderData, $packet_offset, 1)); - $packet_offset += 1; - - $packetLength = 0; - $thisPacket['sample_count'] = $this->SV8variableLengthInteger(substr($MPCheaderData, $packet_offset, $maxHandledPacketLength), $packetLength); - $packet_offset += $packetLength; - - $packetLength = 0; - $thisPacket['beginning_silence'] = $this->SV8variableLengthInteger(substr($MPCheaderData, $packet_offset, $maxHandledPacketLength), $packetLength); - $packet_offset += $packetLength; - - $otherUsefulData = getid3_lib::BigEndian2Int(substr($MPCheaderData, $packet_offset, 2)); - $packet_offset += 2; - $thisPacket['sample_frequency_raw'] = (($otherUsefulData & 0xE000) >> 13); - $thisPacket['max_bands_used'] = (($otherUsefulData & 0x1F00) >> 8); - $thisPacket['channels'] = (($otherUsefulData & 0x00F0) >> 4) + 1; - $thisPacket['ms_used'] = (bool) (($otherUsefulData & 0x0008) >> 3); - $thisPacket['audio_block_frames'] = (($otherUsefulData & 0x0007) >> 0); - $thisPacket['sample_frequency'] = $this->MPCfrequencyLookup($thisPacket['sample_frequency_raw']); - - $thisfile_mpc_header['mid_side_stereo'] = $thisPacket['ms_used']; - $thisfile_mpc_header['sample_rate'] = $thisPacket['sample_frequency']; - $thisfile_mpc_header['samples'] = $thisPacket['sample_count']; - $thisfile_mpc_header['stream_version_major'] = $thisPacket['stream_version']; - - $info['audio']['channels'] = $thisPacket['channels']; - $info['audio']['sample_rate'] = $thisPacket['sample_frequency']; - $info['playtime_seconds'] = $thisPacket['sample_count'] / $thisPacket['sample_frequency']; - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; - break; - - case 'RG': // Replay Gain - $moreBytesToRead = $thisPacket['packet_size'] - $keyNameSize - $maxHandledPacketLength; - if ($moreBytesToRead > 0) { - $MPCheaderData .= fread($this->getid3->fp, $moreBytesToRead); - } - $thisPacket['replaygain_version'] = getid3_lib::BigEndian2Int(substr($MPCheaderData, $packet_offset, 1)); - $packet_offset += 1; - $thisPacket['replaygain_title_gain'] = getid3_lib::BigEndian2Int(substr($MPCheaderData, $packet_offset, 2)); - $packet_offset += 2; - $thisPacket['replaygain_title_peak'] = getid3_lib::BigEndian2Int(substr($MPCheaderData, $packet_offset, 2)); - $packet_offset += 2; - $thisPacket['replaygain_album_gain'] = getid3_lib::BigEndian2Int(substr($MPCheaderData, $packet_offset, 2)); - $packet_offset += 2; - $thisPacket['replaygain_album_peak'] = getid3_lib::BigEndian2Int(substr($MPCheaderData, $packet_offset, 2)); - $packet_offset += 2; - - if ($thisPacket['replaygain_title_gain']) { $info['replay_gain']['title']['gain'] = $thisPacket['replaygain_title_gain']; } - if ($thisPacket['replaygain_title_peak']) { $info['replay_gain']['title']['peak'] = $thisPacket['replaygain_title_peak']; } - if ($thisPacket['replaygain_album_gain']) { $info['replay_gain']['album']['gain'] = $thisPacket['replaygain_album_gain']; } - if ($thisPacket['replaygain_album_peak']) { $info['replay_gain']['album']['peak'] = $thisPacket['replaygain_album_peak']; } - break; - - case 'EI': // Encoder Info - $moreBytesToRead = $thisPacket['packet_size'] - $keyNameSize - $maxHandledPacketLength; - if ($moreBytesToRead > 0) { - $MPCheaderData .= fread($this->getid3->fp, $moreBytesToRead); - } - $profile_pns = getid3_lib::BigEndian2Int(substr($MPCheaderData, $packet_offset, 1)); - $packet_offset += 1; - $quality_int = (($profile_pns & 0xF0) >> 4); - $quality_dec = (($profile_pns & 0x0E) >> 3); - $thisPacket['quality'] = (float) $quality_int + ($quality_dec / 8); - $thisPacket['pns_tool'] = (bool) (($profile_pns & 0x01) >> 0); - $thisPacket['version_major'] = getid3_lib::BigEndian2Int(substr($MPCheaderData, $packet_offset, 1)); - $packet_offset += 1; - $thisPacket['version_minor'] = getid3_lib::BigEndian2Int(substr($MPCheaderData, $packet_offset, 1)); - $packet_offset += 1; - $thisPacket['version_build'] = getid3_lib::BigEndian2Int(substr($MPCheaderData, $packet_offset, 1)); - $packet_offset += 1; - $thisPacket['version'] = $thisPacket['version_major'].'.'.$thisPacket['version_minor'].'.'.$thisPacket['version_build']; - - $info['audio']['encoder'] = 'MPC v'.$thisPacket['version'].' ('.(($thisPacket['version_minor'] % 2) ? 'unstable' : 'stable').')'; - $thisfile_mpc_header['encoder_version'] = $info['audio']['encoder']; - //$thisfile_mpc_header['quality'] = (float) ($thisPacket['quality'] / 1.5875); // values can range from 0.000 to 15.875, mapped to qualities of 0.0 to 10.0 - $thisfile_mpc_header['quality'] = (float) ($thisPacket['quality'] - 5); // values can range from 0.000 to 15.875, of which 0..4 are "reserved/experimental", and 5..15 are mapped to qualities of 0.0 to 10.0 - break; - - case 'SO': // Seek Table Offset - $packetLength = 0; - $thisPacket['seek_table_offset'] = $thisPacket['offset'] + $this->SV8variableLengthInteger(substr($MPCheaderData, $packet_offset, $maxHandledPacketLength), $packetLength); - $packet_offset += $packetLength; - break; - - case 'ST': // Seek Table - case 'SE': // Stream End - case 'AP': // Audio Data - // nothing useful here, just skip this packet - $thisPacket = array(); - break; - - default: - $info['error'][] = 'Found unhandled key type "'.$thisPacket['key'].'" at offset '.$thisPacket['offset']; - return false; - break; - } - if (!empty($thisPacket)) { - $info['mpc']['packets'][] = $thisPacket; - } - fseek($this->getid3->fp, $offset); - } - $thisfile_mpc_header['size'] = $offset; - return true; - } - - public function ParseMPCsv7() { - // this is SV7 - // http://www.uni-jena.de/~pfk/mpp/sv8/header.html - - $info = &$this->getid3->info; - $thisfile_mpc_header = &$info['mpc']['header']; - $offset = 0; - - $thisfile_mpc_header['size'] = 28; - $MPCheaderData = $info['mpc']['header']['preamble']; - $MPCheaderData .= fread($this->getid3->fp, $thisfile_mpc_header['size'] - strlen($info['mpc']['header']['preamble'])); - $offset = strlen('MP+'); - - $StreamVersionByte = getid3_lib::LittleEndian2Int(substr($MPCheaderData, $offset, 1)); - $offset += 1; - $thisfile_mpc_header['stream_version_major'] = ($StreamVersionByte & 0x0F) >> 0; - $thisfile_mpc_header['stream_version_minor'] = ($StreamVersionByte & 0xF0) >> 4; // should always be 0, subversions no longer exist in SV8 - $thisfile_mpc_header['frame_count'] = getid3_lib::LittleEndian2Int(substr($MPCheaderData, $offset, 4)); - $offset += 4; - - if ($thisfile_mpc_header['stream_version_major'] != 7) { - $info['error'][] = 'Only Musepack SV7 supported (this file claims to be v'.$thisfile_mpc_header['stream_version_major'].')'; - return false; - } - - $FlagsDWORD1 = getid3_lib::LittleEndian2Int(substr($MPCheaderData, $offset, 4)); - $offset += 4; - $thisfile_mpc_header['intensity_stereo'] = (bool) (($FlagsDWORD1 & 0x80000000) >> 31); - $thisfile_mpc_header['mid_side_stereo'] = (bool) (($FlagsDWORD1 & 0x40000000) >> 30); - $thisfile_mpc_header['max_subband'] = ($FlagsDWORD1 & 0x3F000000) >> 24; - $thisfile_mpc_header['raw']['profile'] = ($FlagsDWORD1 & 0x00F00000) >> 20; - $thisfile_mpc_header['begin_loud'] = (bool) (($FlagsDWORD1 & 0x00080000) >> 19); - $thisfile_mpc_header['end_loud'] = (bool) (($FlagsDWORD1 & 0x00040000) >> 18); - $thisfile_mpc_header['raw']['sample_rate'] = ($FlagsDWORD1 & 0x00030000) >> 16; - $thisfile_mpc_header['max_level'] = ($FlagsDWORD1 & 0x0000FFFF); - - $thisfile_mpc_header['raw']['title_peak'] = getid3_lib::LittleEndian2Int(substr($MPCheaderData, $offset, 2)); - $offset += 2; - $thisfile_mpc_header['raw']['title_gain'] = getid3_lib::LittleEndian2Int(substr($MPCheaderData, $offset, 2), true); - $offset += 2; - - $thisfile_mpc_header['raw']['album_peak'] = getid3_lib::LittleEndian2Int(substr($MPCheaderData, $offset, 2)); - $offset += 2; - $thisfile_mpc_header['raw']['album_gain'] = getid3_lib::LittleEndian2Int(substr($MPCheaderData, $offset, 2), true); - $offset += 2; - - $FlagsDWORD2 = getid3_lib::LittleEndian2Int(substr($MPCheaderData, $offset, 4)); - $offset += 4; - $thisfile_mpc_header['true_gapless'] = (bool) (($FlagsDWORD2 & 0x80000000) >> 31); - $thisfile_mpc_header['last_frame_length'] = ($FlagsDWORD2 & 0x7FF00000) >> 20; - - - $thisfile_mpc_header['raw']['not_sure_what'] = getid3_lib::LittleEndian2Int(substr($MPCheaderData, $offset, 3)); - $offset += 3; - $thisfile_mpc_header['raw']['encoder_version'] = getid3_lib::LittleEndian2Int(substr($MPCheaderData, $offset, 1)); - $offset += 1; - - $thisfile_mpc_header['profile'] = $this->MPCprofileNameLookup($thisfile_mpc_header['raw']['profile']); - $thisfile_mpc_header['sample_rate'] = $this->MPCfrequencyLookup($thisfile_mpc_header['raw']['sample_rate']); - if ($thisfile_mpc_header['sample_rate'] == 0) { - $info['error'][] = 'Corrupt MPC file: frequency == zero'; - return false; - } - $info['audio']['sample_rate'] = $thisfile_mpc_header['sample_rate']; - $thisfile_mpc_header['samples'] = ((($thisfile_mpc_header['frame_count'] - 1) * 1152) + $thisfile_mpc_header['last_frame_length']) * $info['audio']['channels']; - - $info['playtime_seconds'] = ($thisfile_mpc_header['samples'] / $info['audio']['channels']) / $info['audio']['sample_rate']; - if ($info['playtime_seconds'] == 0) { - $info['error'][] = 'Corrupt MPC file: playtime_seconds == zero'; - return false; - } - - // add size of file header to avdataoffset - calc bitrate correctly + MD5 data - $info['avdataoffset'] += $thisfile_mpc_header['size']; - - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; - - $thisfile_mpc_header['title_peak'] = $thisfile_mpc_header['raw']['title_peak']; - $thisfile_mpc_header['title_peak_db'] = $this->MPCpeakDBLookup($thisfile_mpc_header['title_peak']); - if ($thisfile_mpc_header['raw']['title_gain'] < 0) { - $thisfile_mpc_header['title_gain_db'] = (float) (32768 + $thisfile_mpc_header['raw']['title_gain']) / -100; - } else { - $thisfile_mpc_header['title_gain_db'] = (float) $thisfile_mpc_header['raw']['title_gain'] / 100; - } - - $thisfile_mpc_header['album_peak'] = $thisfile_mpc_header['raw']['album_peak']; - $thisfile_mpc_header['album_peak_db'] = $this->MPCpeakDBLookup($thisfile_mpc_header['album_peak']); - if ($thisfile_mpc_header['raw']['album_gain'] < 0) { - $thisfile_mpc_header['album_gain_db'] = (float) (32768 + $thisfile_mpc_header['raw']['album_gain']) / -100; - } else { - $thisfile_mpc_header['album_gain_db'] = (float) $thisfile_mpc_header['raw']['album_gain'] / 100;; - } - $thisfile_mpc_header['encoder_version'] = $this->MPCencoderVersionLookup($thisfile_mpc_header['raw']['encoder_version']); - - $info['replay_gain']['track']['adjustment'] = $thisfile_mpc_header['title_gain_db']; - $info['replay_gain']['album']['adjustment'] = $thisfile_mpc_header['album_gain_db']; - - if ($thisfile_mpc_header['title_peak'] > 0) { - $info['replay_gain']['track']['peak'] = $thisfile_mpc_header['title_peak']; - } elseif (round($thisfile_mpc_header['max_level'] * 1.18) > 0) { - $info['replay_gain']['track']['peak'] = getid3_lib::CastAsInt(round($thisfile_mpc_header['max_level'] * 1.18)); // why? I don't know - see mppdec.c - } - if ($thisfile_mpc_header['album_peak'] > 0) { - $info['replay_gain']['album']['peak'] = $thisfile_mpc_header['album_peak']; - } - - //$info['audio']['encoder'] = 'SV'.$thisfile_mpc_header['stream_version_major'].'.'.$thisfile_mpc_header['stream_version_minor'].', '.$thisfile_mpc_header['encoder_version']; - $info['audio']['encoder'] = $thisfile_mpc_header['encoder_version']; - $info['audio']['encoder_options'] = $thisfile_mpc_header['profile']; - $thisfile_mpc_header['quality'] = (float) ($thisfile_mpc_header['raw']['profile'] - 5); // values can range from 0 to 15, of which 0..4 are "reserved/experimental", and 5..15 are mapped to qualities of 0.0 to 10.0 - - return true; - } - - public function ParseMPCsv6() { - // this is SV4 - SV6 - - $info = &$this->getid3->info; - $thisfile_mpc_header = &$info['mpc']['header']; - $offset = 0; - - $thisfile_mpc_header['size'] = 8; - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $MPCheaderData = fread($this->getid3->fp, $thisfile_mpc_header['size']); - - // add size of file header to avdataoffset - calc bitrate correctly + MD5 data - $info['avdataoffset'] += $thisfile_mpc_header['size']; - - // Most of this code adapted from Jurgen Faul's MPEGplus source code - thanks Jurgen! :) - $HeaderDWORD[0] = getid3_lib::LittleEndian2Int(substr($MPCheaderData, 0, 4)); - $HeaderDWORD[1] = getid3_lib::LittleEndian2Int(substr($MPCheaderData, 4, 4)); - - - // DDDD DDDD CCCC CCCC BBBB BBBB AAAA AAAA - // aaaa aaaa abcd dddd dddd deee eeff ffff - // - // a = bitrate = anything - // b = IS = anything - // c = MS = anything - // d = streamversion = 0000000004 or 0000000005 or 0000000006 - // e = maxband = anything - // f = blocksize = 000001 for SV5+, anything(?) for SV4 - - $thisfile_mpc_header['target_bitrate'] = (($HeaderDWORD[0] & 0xFF800000) >> 23); - $thisfile_mpc_header['intensity_stereo'] = (bool) (($HeaderDWORD[0] & 0x00400000) >> 22); - $thisfile_mpc_header['mid_side_stereo'] = (bool) (($HeaderDWORD[0] & 0x00200000) >> 21); - $thisfile_mpc_header['stream_version_major'] = ($HeaderDWORD[0] & 0x001FF800) >> 11; - $thisfile_mpc_header['stream_version_minor'] = 0; // no sub-version numbers before SV7 - $thisfile_mpc_header['max_band'] = ($HeaderDWORD[0] & 0x000007C0) >> 6; // related to lowpass frequency, not sure how it translates exactly - $thisfile_mpc_header['block_size'] = ($HeaderDWORD[0] & 0x0000003F); - - switch ($thisfile_mpc_header['stream_version_major']) { - case 4: - $thisfile_mpc_header['frame_count'] = ($HeaderDWORD[1] >> 16); - break; - - case 5: - case 6: - $thisfile_mpc_header['frame_count'] = $HeaderDWORD[1]; - break; - - default: - $info['error'] = 'Expecting 4, 5 or 6 in version field, found '.$thisfile_mpc_header['stream_version_major'].' instead'; - unset($info['mpc']); - return false; - break; - } - - if (($thisfile_mpc_header['stream_version_major'] > 4) && ($thisfile_mpc_header['block_size'] != 1)) { - $info['warning'][] = 'Block size expected to be 1, actual value found: '.$thisfile_mpc_header['block_size']; - } - - $thisfile_mpc_header['sample_rate'] = 44100; // AB: used by all files up to SV7 - $info['audio']['sample_rate'] = $thisfile_mpc_header['sample_rate']; - $thisfile_mpc_header['samples'] = $thisfile_mpc_header['frame_count'] * 1152 * $info['audio']['channels']; - - if ($thisfile_mpc_header['target_bitrate'] == 0) { - $info['audio']['bitrate_mode'] = 'vbr'; - } else { - $info['audio']['bitrate_mode'] = 'cbr'; - } - - $info['mpc']['bitrate'] = ($info['avdataend'] - $info['avdataoffset']) * 8 * 44100 / $thisfile_mpc_header['frame_count'] / 1152; - $info['audio']['bitrate'] = $info['mpc']['bitrate']; - $info['audio']['encoder'] = 'SV'.$thisfile_mpc_header['stream_version_major']; - - return true; - } - - - public function MPCprofileNameLookup($profileid) { - static $MPCprofileNameLookup = array( - 0 => 'no profile', - 1 => 'Experimental', - 2 => 'unused', - 3 => 'unused', - 4 => 'unused', - 5 => 'below Telephone (q = 0.0)', - 6 => 'below Telephone (q = 1.0)', - 7 => 'Telephone (q = 2.0)', - 8 => 'Thumb (q = 3.0)', - 9 => 'Radio (q = 4.0)', - 10 => 'Standard (q = 5.0)', - 11 => 'Extreme (q = 6.0)', - 12 => 'Insane (q = 7.0)', - 13 => 'BrainDead (q = 8.0)', - 14 => 'above BrainDead (q = 9.0)', - 15 => 'above BrainDead (q = 10.0)' - ); - return (isset($MPCprofileNameLookup[$profileid]) ? $MPCprofileNameLookup[$profileid] : 'invalid'); - } - - public function MPCfrequencyLookup($frequencyid) { - static $MPCfrequencyLookup = array( - 0 => 44100, - 1 => 48000, - 2 => 37800, - 3 => 32000 - ); - return (isset($MPCfrequencyLookup[$frequencyid]) ? $MPCfrequencyLookup[$frequencyid] : 'invalid'); - } - - public function MPCpeakDBLookup($intvalue) { - if ($intvalue > 0) { - return ((log10($intvalue) / log10(2)) - 15) * 6; - } - return false; - } - - public function MPCencoderVersionLookup($encoderversion) { - //Encoder version * 100 (106 = 1.06) - //EncoderVersion % 10 == 0 Release (1.0) - //EncoderVersion % 2 == 0 Beta (1.06) - //EncoderVersion % 2 == 1 Alpha (1.05a...z) - - if ($encoderversion == 0) { - // very old version, not known exactly which - return 'Buschmann v1.7.0-v1.7.9 or Klemm v0.90-v1.05'; - } - - if (($encoderversion % 10) == 0) { - - // release version - return number_format($encoderversion / 100, 2); - - } elseif (($encoderversion % 2) == 0) { - - // beta version - return number_format($encoderversion / 100, 2).' beta'; - - } - - // alpha version - return number_format($encoderversion / 100, 2).' alpha'; - } - - public function SV8variableLengthInteger($data, &$packetLength, $maxHandledPacketLength=9) { - $packet_size = 0; - for ($packetLength = 1; $packetLength <= $maxHandledPacketLength; $packetLength++) { - // variable-length size field: - // bits, big-endian - // 0xxx xxxx - value 0 to 2^7-1 - // 1xxx xxxx 0xxx xxxx - value 0 to 2^14-1 - // 1xxx xxxx 1xxx xxxx 0xxx xxxx - value 0 to 2^21-1 - // 1xxx xxxx 1xxx xxxx 1xxx xxxx 0xxx xxxx - value 0 to 2^28-1 - // ... - $thisbyte = ord(substr($data, ($packetLength - 1), 1)); - // look through bytes until find a byte with MSB==0 - $packet_size = ($packet_size << 7); - $packet_size = ($packet_size | ($thisbyte & 0x7F)); - if (($thisbyte & 0x80) === 0) { - break; - } - if ($packetLength >= $maxHandledPacketLength) { - return false; - } - } - return $packet_size; - } - - public function MPCsv8PacketName($packetKey) { - static $MPCsv8PacketName = array(); - if (empty($MPCsv8PacketName)) { - $MPCsv8PacketName = array( - 'AP' => 'Audio Packet', - 'CT' => 'Chapter Tag', - 'EI' => 'Encoder Info', - 'RG' => 'Replay Gain', - 'SE' => 'Stream End', - 'SH' => 'Stream Header', - 'SO' => 'Seek Table Offset', - 'ST' => 'Seek Table', - ); - } - return (isset($MPCsv8PacketName[$packetKey]) ? $MPCsv8PacketName[$packetKey] : $packetKey); - } -} diff --git a/src/Classes/Vendor/getid3/module.audio.ogg.php b/src/Classes/Vendor/getid3/module.audio.ogg.php deleted file mode 100755 index a2a35aadf..000000000 --- a/src/Classes/Vendor/getid3/module.audio.ogg.php +++ /dev/null @@ -1,671 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.ogg.php // -// module for analyzing Ogg Vorbis, OggFLAC and Speex files // -// dependencies: module.audio.flac.php // -// /// -///////////////////////////////////////////////////////////////// - -getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.flac.php', __FILE__, true); - -class getid3_ogg extends getid3_handler -{ - // http://xiph.org/vorbis/doc/Vorbis_I_spec.html - public function Analyze() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'ogg'; - - // Warn about illegal tags - only vorbiscomments are allowed - if (isset($info['id3v2'])) { - $info['warning'][] = 'Illegal ID3v2 tag present.'; - } - if (isset($info['id3v1'])) { - $info['warning'][] = 'Illegal ID3v1 tag present.'; - } - if (isset($info['ape'])) { - $info['warning'][] = 'Illegal APE tag present.'; - } - - - // Page 1 - Stream Header - - $this->fseek($info['avdataoffset']); - - $oggpageinfo = $this->ParseOggPageHeader(); - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo; - - if ($this->ftell() >= $this->getid3->fread_buffer_size()) { - $info['error'][] = 'Could not find start of Ogg page in the first '.$this->getid3->fread_buffer_size().' bytes (this might not be an Ogg-Vorbis file?)'; - unset($info['fileformat']); - unset($info['ogg']); - return false; - } - - $filedata = $this->fread($oggpageinfo['page_length']); - $filedataoffset = 0; - - if (substr($filedata, 0, 4) == 'fLaC') { - - $info['audio']['dataformat'] = 'flac'; - $info['audio']['bitrate_mode'] = 'vbr'; - $info['audio']['lossless'] = true; - - } elseif (substr($filedata, 1, 6) == 'vorbis') { - - $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo); - - } elseif (substr($filedata, 0, 8) == 'Speex ') { - - // http://www.speex.org/manual/node10.html - - $info['audio']['dataformat'] = 'speex'; - $info['mime_type'] = 'audio/speex'; - $info['audio']['bitrate_mode'] = 'abr'; - $info['audio']['lossless'] = false; - - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_string'] = substr($filedata, $filedataoffset, 8); // hard-coded to 'Speex ' - $filedataoffset += 8; - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version'] = substr($filedata, $filedataoffset, 20); - $filedataoffset += 20; - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version_id'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['header_size'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode_bitstream_version'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['bitrate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['framesize'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['frames_per_packet'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['extra_headers'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved1'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['reserved2'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - - $info['speex']['speex_version'] = trim($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['speex_version']); - $info['speex']['sample_rate'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['rate']; - $info['speex']['channels'] = $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['nb_channels']; - $info['speex']['vbr'] = (bool) $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['vbr']; - $info['speex']['band_type'] = $this->SpeexBandModeLookup($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['mode']); - - $info['audio']['sample_rate'] = $info['speex']['sample_rate']; - $info['audio']['channels'] = $info['speex']['channels']; - if ($info['speex']['vbr']) { - $info['audio']['bitrate_mode'] = 'vbr'; - } - - - } elseif (substr($filedata, 0, 8) == "fishead\x00") { - - // Ogg Skeleton version 3.0 Format Specification - // http://xiph.org/ogg/doc/skeleton.html - $filedataoffset += 8; - $info['ogg']['skeleton']['fishead']['raw']['version_major'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2)); - $filedataoffset += 2; - $info['ogg']['skeleton']['fishead']['raw']['version_minor'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 2)); - $filedataoffset += 2; - $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8)); - $filedataoffset += 8; - $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8)); - $filedataoffset += 8; - $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8)); - $filedataoffset += 8; - $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8)); - $filedataoffset += 8; - $info['ogg']['skeleton']['fishead']['raw']['utc'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 20)); - $filedataoffset += 20; - - $info['ogg']['skeleton']['fishead']['version'] = $info['ogg']['skeleton']['fishead']['raw']['version_major'].'.'.$info['ogg']['skeleton']['fishead']['raw']['version_minor']; - $info['ogg']['skeleton']['fishead']['presentationtime'] = $info['ogg']['skeleton']['fishead']['raw']['presentationtime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['presentationtime_denominator']; - $info['ogg']['skeleton']['fishead']['basetime'] = $info['ogg']['skeleton']['fishead']['raw']['basetime_numerator'] / $info['ogg']['skeleton']['fishead']['raw']['basetime_denominator']; - $info['ogg']['skeleton']['fishead']['utc'] = $info['ogg']['skeleton']['fishead']['raw']['utc']; - - - $counter = 0; - do { - $oggpageinfo = $this->ParseOggPageHeader(); - $info['ogg']['pageheader'][$oggpageinfo['page_seqno'].'.'.$counter++] = $oggpageinfo; - $filedata = $this->fread($oggpageinfo['page_length']); - $this->fseek($oggpageinfo['page_end_offset']); - - if (substr($filedata, 0, 8) == "fisbone\x00") { - - $filedataoffset = 8; - $info['ogg']['skeleton']['fisbone']['raw']['message_header_offset'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['skeleton']['fisbone']['raw']['serial_number'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['skeleton']['fisbone']['raw']['number_header_packets'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['skeleton']['fisbone']['raw']['granulerate_numerator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8)); - $filedataoffset += 8; - $info['ogg']['skeleton']['fisbone']['raw']['granulerate_denominator'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8)); - $filedataoffset += 8; - $info['ogg']['skeleton']['fisbone']['raw']['basegranule'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8)); - $filedataoffset += 8; - $info['ogg']['skeleton']['fisbone']['raw']['preroll'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['skeleton']['fisbone']['raw']['granuleshift'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); - $filedataoffset += 1; - $info['ogg']['skeleton']['fisbone']['raw']['padding'] = substr($filedata, $filedataoffset, 3); - $filedataoffset += 3; - - } elseif (substr($filedata, 1, 6) == 'theora') { - - $info['video']['dataformat'] = 'theora'; - $info['error'][] = 'Ogg Theora not correctly handled in this version of getID3 ['.$this->getid3->version().']'; - //break; - - } elseif (substr($filedata, 1, 6) == 'vorbis') { - - $this->ParseVorbisPageHeader($filedata, $filedataoffset, $oggpageinfo); - - } else { - $info['error'][] = 'unexpected'; - //break; - } - //} while ($oggpageinfo['page_seqno'] == 0); - } while (($oggpageinfo['page_seqno'] == 0) && (substr($filedata, 0, 8) != "fisbone\x00")); - - $this->fseek($oggpageinfo['page_start_offset']); - - $info['error'][] = 'Ogg Skeleton not correctly handled in this version of getID3 ['.$this->getid3->version().']'; - //return false; - - } else { - - $info['error'][] = 'Expecting either "Speex " or "vorbis" identifier strings, found "'.substr($filedata, 0, 8).'"'; - unset($info['ogg']); - unset($info['mime_type']); - return false; - - } - - // Page 2 - Comment Header - $oggpageinfo = $this->ParseOggPageHeader(); - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo; - - switch ($info['audio']['dataformat']) { - case 'vorbis': - $filedata = $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']); - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int(substr($filedata, 0, 1)); - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, 1, 6); // hard-coded to 'vorbis' - - $this->ParseVorbisComments(); - break; - - case 'flac': - $flac = new getid3_flac($this->getid3); - if (!$flac->parseMETAdata()) { - $info['error'][] = 'Failed to parse FLAC headers'; - return false; - } - unset($flac); - break; - - case 'speex': - $this->fseek($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length'], SEEK_CUR); - $this->ParseVorbisComments(); - break; - } - - - // Last Page - Number of Samples - if (!getid3_lib::intValueSupported($info['avdataend'])) { - - $info['warning'][] = 'Unable to parse Ogg end chunk file (PHP does not support file operations beyond '.round(PHP_INT_MAX / 1073741824).'GB)'; - - } else { - - $this->fseek(max($info['avdataend'] - $this->getid3->fread_buffer_size(), 0)); - $LastChunkOfOgg = strrev($this->fread($this->getid3->fread_buffer_size())); - if ($LastOggSpostion = strpos($LastChunkOfOgg, 'SggO')) { - $this->fseek($info['avdataend'] - ($LastOggSpostion + strlen('SggO'))); - $info['avdataend'] = $this->ftell(); - $info['ogg']['pageheader']['eos'] = $this->ParseOggPageHeader(); - $info['ogg']['samples'] = $info['ogg']['pageheader']['eos']['pcm_abs_position']; - if ($info['ogg']['samples'] == 0) { - $info['error'][] = 'Corrupt Ogg file: eos.number of samples == zero'; - return false; - } - if (!empty($info['audio']['sample_rate'])) { - $info['ogg']['bitrate_average'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($info['ogg']['samples'] / $info['audio']['sample_rate']); - } - } - - } - - if (!empty($info['ogg']['bitrate_average'])) { - $info['audio']['bitrate'] = $info['ogg']['bitrate_average']; - } elseif (!empty($info['ogg']['bitrate_nominal'])) { - $info['audio']['bitrate'] = $info['ogg']['bitrate_nominal']; - } elseif (!empty($info['ogg']['bitrate_min']) && !empty($info['ogg']['bitrate_max'])) { - $info['audio']['bitrate'] = ($info['ogg']['bitrate_min'] + $info['ogg']['bitrate_max']) / 2; - } - if (isset($info['audio']['bitrate']) && !isset($info['playtime_seconds'])) { - if ($info['audio']['bitrate'] == 0) { - $info['error'][] = 'Corrupt Ogg file: bitrate_audio == zero'; - return false; - } - $info['playtime_seconds'] = (float) ((($info['avdataend'] - $info['avdataoffset']) * 8) / $info['audio']['bitrate']); - } - - if (isset($info['ogg']['vendor'])) { - $info['audio']['encoder'] = preg_replace('/^Encoded with /', '', $info['ogg']['vendor']); - - // Vorbis only - if ($info['audio']['dataformat'] == 'vorbis') { - - // Vorbis 1.0 starts with Xiph.Org - if (preg_match('/^Xiph.Org/', $info['audio']['encoder'])) { - - if ($info['audio']['bitrate_mode'] == 'abr') { - - // Set -b 128 on abr files - $info['audio']['encoder_options'] = '-b '.round($info['ogg']['bitrate_nominal'] / 1000); - - } elseif (($info['audio']['bitrate_mode'] == 'vbr') && ($info['audio']['channels'] == 2) && ($info['audio']['sample_rate'] >= 44100) && ($info['audio']['sample_rate'] <= 48000)) { - // Set -q N on vbr files - $info['audio']['encoder_options'] = '-q '.$this->get_quality_from_nominal_bitrate($info['ogg']['bitrate_nominal']); - - } - } - - if (empty($info['audio']['encoder_options']) && !empty($info['ogg']['bitrate_nominal'])) { - $info['audio']['encoder_options'] = 'Nominal bitrate: '.intval(round($info['ogg']['bitrate_nominal'] / 1000)).'kbps'; - } - } - } - - return true; - } - - public function ParseVorbisPageHeader(&$filedata, &$filedataoffset, &$oggpageinfo) { - $info = &$this->getid3->info; - $info['audio']['dataformat'] = 'vorbis'; - $info['audio']['lossless'] = false; - - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['packet_type'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); - $filedataoffset += 1; - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['stream_type'] = substr($filedata, $filedataoffset, 6); // hard-coded to 'vorbis' - $filedataoffset += 6; - $info['ogg']['bitstreamversion'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['numberofchannels'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); - $filedataoffset += 1; - $info['audio']['channels'] = $info['ogg']['numberofchannels']; - $info['ogg']['samplerate'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - if ($info['ogg']['samplerate'] == 0) { - $info['error'][] = 'Corrupt Ogg file: sample rate == zero'; - return false; - } - $info['audio']['sample_rate'] = $info['ogg']['samplerate']; - $info['ogg']['samples'] = 0; // filled in later - $info['ogg']['bitrate_average'] = 0; // filled in later - $info['ogg']['bitrate_max'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['bitrate_nominal'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['bitrate_min'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $info['ogg']['blocksize_small'] = pow(2, getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0x0F); - $info['ogg']['blocksize_large'] = pow(2, (getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)) & 0xF0) >> 4); - $info['ogg']['stop_bit'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); // must be 1, marks end of packet - - $info['audio']['bitrate_mode'] = 'vbr'; // overridden if actually abr - if ($info['ogg']['bitrate_max'] == 0xFFFFFFFF) { - unset($info['ogg']['bitrate_max']); - $info['audio']['bitrate_mode'] = 'abr'; - } - if ($info['ogg']['bitrate_nominal'] == 0xFFFFFFFF) { - unset($info['ogg']['bitrate_nominal']); - } - if ($info['ogg']['bitrate_min'] == 0xFFFFFFFF) { - unset($info['ogg']['bitrate_min']); - $info['audio']['bitrate_mode'] = 'abr'; - } - return true; - } - - public function ParseOggPageHeader() { - // http://xiph.org/ogg/vorbis/doc/framing.html - $oggheader['page_start_offset'] = $this->ftell(); // where we started from in the file - - $filedata = $this->fread($this->getid3->fread_buffer_size()); - $filedataoffset = 0; - while ((substr($filedata, $filedataoffset++, 4) != 'OggS')) { - if (($this->ftell() - $oggheader['page_start_offset']) >= $this->getid3->fread_buffer_size()) { - // should be found before here - return false; - } - if ((($filedataoffset + 28) > strlen($filedata)) || (strlen($filedata) < 28)) { - if ($this->feof() || (($filedata .= $this->fread($this->getid3->fread_buffer_size())) === false)) { - // get some more data, unless eof, in which case fail - return false; - } - } - } - $filedataoffset += strlen('OggS') - 1; // page, delimited by 'OggS' - - $oggheader['stream_structver'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); - $filedataoffset += 1; - $oggheader['flags_raw'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); - $filedataoffset += 1; - $oggheader['flags']['fresh'] = (bool) ($oggheader['flags_raw'] & 0x01); // fresh packet - $oggheader['flags']['bos'] = (bool) ($oggheader['flags_raw'] & 0x02); // first page of logical bitstream (bos) - $oggheader['flags']['eos'] = (bool) ($oggheader['flags_raw'] & 0x04); // last page of logical bitstream (eos) - - $oggheader['pcm_abs_position'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 8)); - $filedataoffset += 8; - $oggheader['stream_serialno'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $oggheader['page_seqno'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $oggheader['page_checksum'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 4)); - $filedataoffset += 4; - $oggheader['page_segments'] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); - $filedataoffset += 1; - $oggheader['page_length'] = 0; - for ($i = 0; $i < $oggheader['page_segments']; $i++) { - $oggheader['segment_table'][$i] = getid3_lib::LittleEndian2Int(substr($filedata, $filedataoffset, 1)); - $filedataoffset += 1; - $oggheader['page_length'] += $oggheader['segment_table'][$i]; - } - $oggheader['header_end_offset'] = $oggheader['page_start_offset'] + $filedataoffset; - $oggheader['page_end_offset'] = $oggheader['header_end_offset'] + $oggheader['page_length']; - $this->fseek($oggheader['header_end_offset']); - - return $oggheader; - } - - // http://xiph.org/vorbis/doc/Vorbis_I_spec.html#x1-810005 - public function ParseVorbisComments() { - $info = &$this->getid3->info; - - $OriginalOffset = $this->ftell(); - $commentdataoffset = 0; - $VorbisCommentPage = 1; - - switch ($info['audio']['dataformat']) { - case 'vorbis': - case 'speex': - $CommentStartOffset = $info['ogg']['pageheader'][$VorbisCommentPage]['page_start_offset']; // Second Ogg page, after header block - $this->fseek($CommentStartOffset); - $commentdataoffset = 27 + $info['ogg']['pageheader'][$VorbisCommentPage]['page_segments']; - $commentdata = $this->fread(self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1) + $commentdataoffset); - - if ($info['audio']['dataformat'] == 'vorbis') { - $commentdataoffset += (strlen('vorbis') + 1); - } - break; - - case 'flac': - $CommentStartOffset = $info['flac']['VORBIS_COMMENT']['raw']['offset'] + 4; - $this->fseek($CommentStartOffset); - $commentdata = $this->fread($info['flac']['VORBIS_COMMENT']['raw']['block_length']); - break; - - default: - return false; - } - - $VendorSize = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4)); - $commentdataoffset += 4; - - $info['ogg']['vendor'] = substr($commentdata, $commentdataoffset, $VendorSize); - $commentdataoffset += $VendorSize; - - $CommentsCount = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4)); - $commentdataoffset += 4; - $info['avdataoffset'] = $CommentStartOffset + $commentdataoffset; - - $basicfields = array('TITLE', 'ARTIST', 'ALBUM', 'TRACKNUMBER', 'GENRE', 'DATE', 'DESCRIPTION', 'COMMENT'); - $ThisFileInfo_ogg_comments_raw = &$info['ogg']['comments_raw']; - for ($i = 0; $i < $CommentsCount; $i++) { - - $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] = $CommentStartOffset + $commentdataoffset; - - if ($this->ftell() < ($ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + 4)) { - if ($oggpageinfo = $this->ParseOggPageHeader()) { - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo; - - $VorbisCommentPage++; - - // First, save what we haven't read yet - $AsYetUnusedData = substr($commentdata, $commentdataoffset); - - // Then take that data off the end - $commentdata = substr($commentdata, 0, $commentdataoffset); - - // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct - $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']); - $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']); - - // Finally, stick the unused data back on the end - $commentdata .= $AsYetUnusedData; - - //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']); - $commentdata .= $this->fread($this->OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1)); - } - - } - $ThisFileInfo_ogg_comments_raw[$i]['size'] = getid3_lib::LittleEndian2Int(substr($commentdata, $commentdataoffset, 4)); - - // replace avdataoffset with position just after the last vorbiscomment - $info['avdataoffset'] = $ThisFileInfo_ogg_comments_raw[$i]['dataoffset'] + $ThisFileInfo_ogg_comments_raw[$i]['size'] + 4; - - $commentdataoffset += 4; - while ((strlen($commentdata) - $commentdataoffset) < $ThisFileInfo_ogg_comments_raw[$i]['size']) { - if (($ThisFileInfo_ogg_comments_raw[$i]['size'] > $info['avdataend']) || ($ThisFileInfo_ogg_comments_raw[$i]['size'] < 0)) { - $info['warning'][] = 'Invalid Ogg comment size (comment #'.$i.', claims to be '.number_format($ThisFileInfo_ogg_comments_raw[$i]['size']).' bytes) - aborting reading comments'; - break 2; - } - - $VorbisCommentPage++; - - $oggpageinfo = $this->ParseOggPageHeader(); - $info['ogg']['pageheader'][$oggpageinfo['page_seqno']] = $oggpageinfo; - - // First, save what we haven't read yet - $AsYetUnusedData = substr($commentdata, $commentdataoffset); - - // Then take that data off the end - $commentdata = substr($commentdata, 0, $commentdataoffset); - - // Add [headerlength] bytes of dummy data for the Ogg Page Header, just to keep absolute offsets correct - $commentdata .= str_repeat("\x00", 27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']); - $commentdataoffset += (27 + $info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_segments']); - - // Finally, stick the unused data back on the end - $commentdata .= $AsYetUnusedData; - - //$commentdata .= $this->fread($info['ogg']['pageheader'][$oggpageinfo['page_seqno']]['page_length']); - if (!isset($info['ogg']['pageheader'][$VorbisCommentPage])) { - $info['warning'][] = 'undefined Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell(); - break; - } - $readlength = self::OggPageSegmentLength($info['ogg']['pageheader'][$VorbisCommentPage], 1); - if ($readlength <= 0) { - $info['warning'][] = 'invalid length Vorbis Comment page "'.$VorbisCommentPage.'" at offset '.$this->ftell(); - break; - } - $commentdata .= $this->fread($readlength); - - //$filebaseoffset += $oggpageinfo['header_end_offset'] - $oggpageinfo['page_start_offset']; - } - $ThisFileInfo_ogg_comments_raw[$i]['offset'] = $commentdataoffset; - $commentstring = substr($commentdata, $commentdataoffset, $ThisFileInfo_ogg_comments_raw[$i]['size']); - $commentdataoffset += $ThisFileInfo_ogg_comments_raw[$i]['size']; - - if (!$commentstring) { - - // no comment? - $info['warning'][] = 'Blank Ogg comment ['.$i.']'; - - } elseif (strstr($commentstring, '=')) { - - $commentexploded = explode('=', $commentstring, 2); - $ThisFileInfo_ogg_comments_raw[$i]['key'] = strtoupper($commentexploded[0]); - $ThisFileInfo_ogg_comments_raw[$i]['value'] = (isset($commentexploded[1]) ? $commentexploded[1] : ''); - - if ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'METADATA_BLOCK_PICTURE') { - - // http://wiki.xiph.org/VorbisComment#METADATA_BLOCK_PICTURE - // The unencoded format is that of the FLAC picture block. The fields are stored in big endian order as in FLAC, picture data is stored according to the relevant standard. - // http://flac.sourceforge.net/format.html#metadata_block_picture - $flac = new getid3_flac($this->getid3); - $flac->setStringMode(base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value'])); - $flac->parsePICTURE(); - $info['ogg']['comments']['picture'][] = $flac->getid3->info['flac']['PICTURE'][0]; - unset($flac); - - } elseif ($ThisFileInfo_ogg_comments_raw[$i]['key'] == 'COVERART') { - - $data = base64_decode($ThisFileInfo_ogg_comments_raw[$i]['value']); - $this->notice('Found deprecated COVERART tag, it should be replaced in honor of METADATA_BLOCK_PICTURE structure'); - /** @todo use 'coverartmime' where available */ - $imageinfo = getid3_lib::GetDataImageSize($data); - if ($imageinfo === false || !isset($imageinfo['mime'])) { - $this->warning('COVERART vorbiscomment tag contains invalid image'); - continue; - } - - $ogg = new self($this->getid3); - $ogg->setStringMode($data); - $info['ogg']['comments']['picture'][] = array( - 'image_mime' => $imageinfo['mime'], - 'data' => $ogg->saveAttachment('coverart', 0, strlen($data), $imageinfo['mime']), - ); - unset($ogg); - - } else { - - $info['ogg']['comments'][strtolower($ThisFileInfo_ogg_comments_raw[$i]['key'])][] = $ThisFileInfo_ogg_comments_raw[$i]['value']; - - } - - } else { - - $info['warning'][] = '[known problem with CDex >= v1.40, < v1.50b7] Invalid Ogg comment name/value pair ['.$i.']: '.$commentstring; - - } - unset($ThisFileInfo_ogg_comments_raw[$i]); - } - unset($ThisFileInfo_ogg_comments_raw); - - - // Replay Gain Adjustment - // http://privatewww.essex.ac.uk/~djmrob/replaygain/ - if (isset($info['ogg']['comments']) && is_array($info['ogg']['comments'])) { - foreach ($info['ogg']['comments'] as $index => $commentvalue) { - switch ($index) { - case 'rg_audiophile': - case 'replaygain_album_gain': - $info['replay_gain']['album']['adjustment'] = (double) $commentvalue[0]; - unset($info['ogg']['comments'][$index]); - break; - - case 'rg_radio': - case 'replaygain_track_gain': - $info['replay_gain']['track']['adjustment'] = (double) $commentvalue[0]; - unset($info['ogg']['comments'][$index]); - break; - - case 'replaygain_album_peak': - $info['replay_gain']['album']['peak'] = (double) $commentvalue[0]; - unset($info['ogg']['comments'][$index]); - break; - - case 'rg_peak': - case 'replaygain_track_peak': - $info['replay_gain']['track']['peak'] = (double) $commentvalue[0]; - unset($info['ogg']['comments'][$index]); - break; - - case 'replaygain_reference_loudness': - $info['replay_gain']['reference_volume'] = (double) $commentvalue[0]; - unset($info['ogg']['comments'][$index]); - break; - - default: - // do nothing - break; - } - } - } - - $this->fseek($OriginalOffset); - - return true; - } - - public static function SpeexBandModeLookup($mode) { - static $SpeexBandModeLookup = array(); - if (empty($SpeexBandModeLookup)) { - $SpeexBandModeLookup[0] = 'narrow'; - $SpeexBandModeLookup[1] = 'wide'; - $SpeexBandModeLookup[2] = 'ultra-wide'; - } - return (isset($SpeexBandModeLookup[$mode]) ? $SpeexBandModeLookup[$mode] : null); - } - - - public static function OggPageSegmentLength($OggInfoArray, $SegmentNumber=1) { - for ($i = 0; $i < $SegmentNumber; $i++) { - $segmentlength = 0; - foreach ($OggInfoArray['segment_table'] as $key => $value) { - $segmentlength += $value; - if ($value < 255) { - break; - } - } - } - return $segmentlength; - } - - - public static function get_quality_from_nominal_bitrate($nominal_bitrate) { - - // decrease precision - $nominal_bitrate = $nominal_bitrate / 1000; - - if ($nominal_bitrate < 128) { - // q-1 to q4 - $qval = ($nominal_bitrate - 64) / 16; - } elseif ($nominal_bitrate < 256) { - // q4 to q8 - $qval = $nominal_bitrate / 32; - } elseif ($nominal_bitrate < 320) { - // q8 to q9 - $qval = ($nominal_bitrate + 256) / 64; - } else { - // q9 to q10 - $qval = ($nominal_bitrate + 1300) / 180; - } - //return $qval; // 5.031324 - //return intval($qval); // 5 - return round($qval, 1); // 5 or 4.9 - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.optimfrog.php b/src/Classes/Vendor/getid3/module.audio.optimfrog.php deleted file mode 100755 index 5df74b7b6..000000000 --- a/src/Classes/Vendor/getid3/module.audio.optimfrog.php +++ /dev/null @@ -1,426 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.optimfrog.php // -// module for analyzing OptimFROG audio files // -// dependencies: module.audio.riff.php // -// /// -///////////////////////////////////////////////////////////////// - -getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); - -class getid3_optimfrog extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'ofr'; - $info['audio']['dataformat'] = 'ofr'; - $info['audio']['bitrate_mode'] = 'vbr'; - $info['audio']['lossless'] = true; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $OFRheader = fread($this->getid3->fp, 8); - if (substr($OFRheader, 0, 5) == '*RIFF') { - - return $this->ParseOptimFROGheader42(); - - } elseif (substr($OFRheader, 0, 3) == 'OFR') { - - return $this->ParseOptimFROGheader45(); - - } - - $info['error'][] = 'Expecting "*RIFF" or "OFR " at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($OFRheader).'"'; - unset($info['fileformat']); - return false; - } - - - public function ParseOptimFROGheader42() { - // for fileformat of v4.21 and older - - $info = &$this->getid3->info; - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $OptimFROGheaderData = fread($this->getid3->fp, 45); - $info['avdataoffset'] = 45; - - $OptimFROGencoderVersion_raw = getid3_lib::LittleEndian2Int(substr($OptimFROGheaderData, 0, 1)); - $OptimFROGencoderVersion_major = floor($OptimFROGencoderVersion_raw / 10); - $OptimFROGencoderVersion_minor = $OptimFROGencoderVersion_raw - ($OptimFROGencoderVersion_major * 10); - $RIFFdata = substr($OptimFROGheaderData, 1, 44); - $OrignalRIFFheaderSize = getid3_lib::LittleEndian2Int(substr($RIFFdata, 4, 4)) + 8; - $OrignalRIFFdataSize = getid3_lib::LittleEndian2Int(substr($RIFFdata, 40, 4)) + 44; - - if ($OrignalRIFFheaderSize > $OrignalRIFFdataSize) { - $info['avdataend'] -= ($OrignalRIFFheaderSize - $OrignalRIFFdataSize); - fseek($this->getid3->fp, $info['avdataend'], SEEK_SET); - $RIFFdata .= fread($this->getid3->fp, $OrignalRIFFheaderSize - $OrignalRIFFdataSize); - } - - // move the data chunk after all other chunks (if any) - // so that the RIFF parser doesn't see EOF when trying - // to skip over the data chunk - $RIFFdata = substr($RIFFdata, 0, 36).substr($RIFFdata, 44).substr($RIFFdata, 36, 8); - - $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); - $getid3_temp->info['avdataoffset'] = $info['avdataoffset']; - $getid3_temp->info['avdataend'] = $info['avdataend']; - $getid3_riff = new getid3_riff($getid3_temp); - $getid3_riff->ParseRIFFdata($RIFFdata); - $info['riff'] = $getid3_temp->info['riff']; - - $info['audio']['encoder'] = 'OptimFROG '.$OptimFROGencoderVersion_major.'.'.$OptimFROGencoderVersion_minor; - $info['audio']['channels'] = $info['riff']['audio'][0]['channels']; - $info['audio']['sample_rate'] = $info['riff']['audio'][0]['sample_rate']; - $info['audio']['bits_per_sample'] = $info['riff']['audio'][0]['bits_per_sample']; - $info['playtime_seconds'] = $OrignalRIFFdataSize / ($info['audio']['channels'] * $info['audio']['sample_rate'] * ($info['audio']['bits_per_sample'] / 8)); - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; - - unset($getid3_riff, $getid3_temp, $RIFFdata); - - return true; - } - - - public function ParseOptimFROGheader45() { - // for fileformat of v4.50a and higher - - $info = &$this->getid3->info; - $RIFFdata = ''; - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - while (!feof($this->getid3->fp) && (ftell($this->getid3->fp) < $info['avdataend'])) { - $BlockOffset = ftell($this->getid3->fp); - $BlockData = fread($this->getid3->fp, 8); - $offset = 8; - $BlockName = substr($BlockData, 0, 4); - $BlockSize = getid3_lib::LittleEndian2Int(substr($BlockData, 4, 4)); - - if ($BlockName == 'OFRX') { - $BlockName = 'OFR '; - } - if (!isset($info['ofr'][$BlockName])) { - $info['ofr'][$BlockName] = array(); - } - $thisfile_ofr_thisblock = &$info['ofr'][$BlockName]; - - switch ($BlockName) { - case 'OFR ': - - // shortcut - $thisfile_ofr_thisblock['offset'] = $BlockOffset; - $thisfile_ofr_thisblock['size'] = $BlockSize; - - $info['audio']['encoder'] = 'OptimFROG 4.50 alpha'; - switch ($BlockSize) { - case 12: - case 15: - // good - break; - - default: - $info['warning'][] = '"'.$BlockName.'" contains more data than expected (expected 12 or 15 bytes, found '.$BlockSize.' bytes)'; - break; - } - $BlockData .= fread($this->getid3->fp, $BlockSize); - - $thisfile_ofr_thisblock['total_samples'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 6)); - $offset += 6; - $thisfile_ofr_thisblock['raw']['sample_type'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 1)); - $thisfile_ofr_thisblock['sample_type'] = $this->OptimFROGsampleTypeLookup($thisfile_ofr_thisblock['raw']['sample_type']); - $offset += 1; - $thisfile_ofr_thisblock['channel_config'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 1)); - $thisfile_ofr_thisblock['channels'] = $thisfile_ofr_thisblock['channel_config']; - $offset += 1; - $thisfile_ofr_thisblock['sample_rate'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 4)); - $offset += 4; - - if ($BlockSize > 12) { - - // OFR 4.504b or higher - $thisfile_ofr_thisblock['channels'] = $this->OptimFROGchannelConfigNumChannelsLookup($thisfile_ofr_thisblock['channel_config']); - $thisfile_ofr_thisblock['raw']['encoder_id'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 2)); - $thisfile_ofr_thisblock['encoder'] = $this->OptimFROGencoderNameLookup($thisfile_ofr_thisblock['raw']['encoder_id']); - $offset += 2; - $thisfile_ofr_thisblock['raw']['compression'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 1)); - $thisfile_ofr_thisblock['compression'] = $this->OptimFROGcompressionLookup($thisfile_ofr_thisblock['raw']['compression']); - $thisfile_ofr_thisblock['speedup'] = $this->OptimFROGspeedupLookup($thisfile_ofr_thisblock['raw']['compression']); - $offset += 1; - - $info['audio']['encoder'] = 'OptimFROG '.$thisfile_ofr_thisblock['encoder']; - $info['audio']['encoder_options'] = '--mode '.$thisfile_ofr_thisblock['compression']; - - if ((($thisfile_ofr_thisblock['raw']['encoder_id'] & 0xF0) >> 4) == 7) { // v4.507 - if (strtolower(getid3_lib::fileextension($info['filename'])) == 'ofs') { - // OptimFROG DualStream format is lossy, but as of v4.507 there is no way to tell the difference - // between lossless and lossy other than the file extension. - $info['audio']['dataformat'] = 'ofs'; - $info['audio']['lossless'] = true; - } - } - - } - - $info['audio']['channels'] = $thisfile_ofr_thisblock['channels']; - $info['audio']['sample_rate'] = $thisfile_ofr_thisblock['sample_rate']; - $info['audio']['bits_per_sample'] = $this->OptimFROGbitsPerSampleTypeLookup($thisfile_ofr_thisblock['raw']['sample_type']); - break; - - - case 'COMP': - // unlike other block types, there CAN be multiple COMP blocks - - $COMPdata['offset'] = $BlockOffset; - $COMPdata['size'] = $BlockSize; - - if ($info['avdataoffset'] == 0) { - $info['avdataoffset'] = $BlockOffset; - } - - // Only interested in first 14 bytes (only first 12 needed for v4.50 alpha), not actual audio data - $BlockData .= fread($this->getid3->fp, 14); - fseek($this->getid3->fp, $BlockSize - 14, SEEK_CUR); - - $COMPdata['crc_32'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 4)); - $offset += 4; - $COMPdata['sample_count'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 4)); - $offset += 4; - $COMPdata['raw']['sample_type'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 1)); - $COMPdata['sample_type'] = $this->OptimFROGsampleTypeLookup($COMPdata['raw']['sample_type']); - $offset += 1; - $COMPdata['raw']['channel_configuration'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 1)); - $COMPdata['channel_configuration'] = $this->OptimFROGchannelConfigurationLookup($COMPdata['raw']['channel_configuration']); - $offset += 1; - $COMPdata['raw']['algorithm_id'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 2)); - //$COMPdata['algorithm'] = OptimFROGalgorithmNameLookup($COMPdata['raw']['algorithm_id']); - $offset += 2; - - if ($info['ofr']['OFR ']['size'] > 12) { - - // OFR 4.504b or higher - $COMPdata['raw']['encoder_id'] = getid3_lib::LittleEndian2Int(substr($BlockData, $offset, 2)); - $COMPdata['encoder'] = $this->OptimFROGencoderNameLookup($COMPdata['raw']['encoder_id']); - $offset += 2; - - } - - if ($COMPdata['crc_32'] == 0x454E4F4E) { - // ASCII value of 'NONE' - placeholder value in v4.50a - $COMPdata['crc_32'] = false; - } - - $thisfile_ofr_thisblock[] = $COMPdata; - break; - - case 'HEAD': - $thisfile_ofr_thisblock['offset'] = $BlockOffset; - $thisfile_ofr_thisblock['size'] = $BlockSize; - - $RIFFdata .= fread($this->getid3->fp, $BlockSize); - break; - - case 'TAIL': - $thisfile_ofr_thisblock['offset'] = $BlockOffset; - $thisfile_ofr_thisblock['size'] = $BlockSize; - - if ($BlockSize > 0) { - $RIFFdata .= fread($this->getid3->fp, $BlockSize); - } - break; - - case 'RECV': - // block contains no useful meta data - simply note and skip - - $thisfile_ofr_thisblock['offset'] = $BlockOffset; - $thisfile_ofr_thisblock['size'] = $BlockSize; - - fseek($this->getid3->fp, $BlockSize, SEEK_CUR); - break; - - - case 'APET': - // APEtag v2 - - $thisfile_ofr_thisblock['offset'] = $BlockOffset; - $thisfile_ofr_thisblock['size'] = $BlockSize; - $info['warning'][] = 'APEtag processing inside OptimFROG not supported in this version ('.$this->getid3->version().') of getID3()'; - - fseek($this->getid3->fp, $BlockSize, SEEK_CUR); - break; - - - case 'MD5 ': - // APEtag v2 - - $thisfile_ofr_thisblock['offset'] = $BlockOffset; - $thisfile_ofr_thisblock['size'] = $BlockSize; - - if ($BlockSize == 16) { - - $thisfile_ofr_thisblock['md5_binary'] = fread($this->getid3->fp, $BlockSize); - $thisfile_ofr_thisblock['md5_string'] = getid3_lib::PrintHexBytes($thisfile_ofr_thisblock['md5_binary'], true, false, false); - $info['md5_data_source'] = $thisfile_ofr_thisblock['md5_string']; - - } else { - - $info['warning'][] = 'Expecting block size of 16 in "MD5 " chunk, found '.$BlockSize.' instead'; - fseek($this->getid3->fp, $BlockSize, SEEK_CUR); - - } - break; - - - default: - $thisfile_ofr_thisblock['offset'] = $BlockOffset; - $thisfile_ofr_thisblock['size'] = $BlockSize; - - $info['warning'][] = 'Unhandled OptimFROG block type "'.$BlockName.'" at offset '.$thisfile_ofr_thisblock['offset']; - fseek($this->getid3->fp, $BlockSize, SEEK_CUR); - break; - } - } - if (isset($info['ofr']['TAIL']['offset'])) { - $info['avdataend'] = $info['ofr']['TAIL']['offset']; - } - - $info['playtime_seconds'] = (float) $info['ofr']['OFR ']['total_samples'] / ($info['audio']['channels'] * $info['audio']['sample_rate']); - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; - - // move the data chunk after all other chunks (if any) - // so that the RIFF parser doesn't see EOF when trying - // to skip over the data chunk - $RIFFdata = substr($RIFFdata, 0, 36).substr($RIFFdata, 44).substr($RIFFdata, 36, 8); - - $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); - $getid3_temp->info['avdataoffset'] = $info['avdataoffset']; - $getid3_temp->info['avdataend'] = $info['avdataend']; - $getid3_riff = new getid3_riff($getid3_temp); - $getid3_riff->ParseRIFFdata($RIFFdata); - $info['riff'] = $getid3_temp->info['riff']; - - unset($getid3_riff, $getid3_temp, $RIFFdata); - - return true; - } - - - public static function OptimFROGsampleTypeLookup($SampleType) { - static $OptimFROGsampleTypeLookup = array( - 0 => 'unsigned int (8-bit)', - 1 => 'signed int (8-bit)', - 2 => 'unsigned int (16-bit)', - 3 => 'signed int (16-bit)', - 4 => 'unsigned int (24-bit)', - 5 => 'signed int (24-bit)', - 6 => 'unsigned int (32-bit)', - 7 => 'signed int (32-bit)', - 8 => 'float 0.24 (32-bit)', - 9 => 'float 16.8 (32-bit)', - 10 => 'float 24.0 (32-bit)' - ); - return (isset($OptimFROGsampleTypeLookup[$SampleType]) ? $OptimFROGsampleTypeLookup[$SampleType] : false); - } - - public static function OptimFROGbitsPerSampleTypeLookup($SampleType) { - static $OptimFROGbitsPerSampleTypeLookup = array( - 0 => 8, - 1 => 8, - 2 => 16, - 3 => 16, - 4 => 24, - 5 => 24, - 6 => 32, - 7 => 32, - 8 => 32, - 9 => 32, - 10 => 32 - ); - return (isset($OptimFROGbitsPerSampleTypeLookup[$SampleType]) ? $OptimFROGbitsPerSampleTypeLookup[$SampleType] : false); - } - - public static function OptimFROGchannelConfigurationLookup($ChannelConfiguration) { - static $OptimFROGchannelConfigurationLookup = array( - 0 => 'mono', - 1 => 'stereo' - ); - return (isset($OptimFROGchannelConfigurationLookup[$ChannelConfiguration]) ? $OptimFROGchannelConfigurationLookup[$ChannelConfiguration] : false); - } - - public static function OptimFROGchannelConfigNumChannelsLookup($ChannelConfiguration) { - static $OptimFROGchannelConfigNumChannelsLookup = array( - 0 => 1, - 1 => 2 - ); - return (isset($OptimFROGchannelConfigNumChannelsLookup[$ChannelConfiguration]) ? $OptimFROGchannelConfigNumChannelsLookup[$ChannelConfiguration] : false); - } - - - - // static function OptimFROGalgorithmNameLookup($AlgorithID) { - // static $OptimFROGalgorithmNameLookup = array(); - // return (isset($OptimFROGalgorithmNameLookup[$AlgorithID]) ? $OptimFROGalgorithmNameLookup[$AlgorithID] : false); - // } - - - public static function OptimFROGencoderNameLookup($EncoderID) { - // version = (encoderID >> 4) + 4500 - // system = encoderID & 0xF - - $EncoderVersion = number_format(((($EncoderID & 0xF0) >> 4) + 4500) / 1000, 3); - $EncoderSystemID = ($EncoderID & 0x0F); - - static $OptimFROGencoderSystemLookup = array( - 0x00 => 'Windows console', - 0x01 => 'Linux console', - 0x0F => 'unknown' - ); - return $EncoderVersion.' ('.(isset($OptimFROGencoderSystemLookup[$EncoderSystemID]) ? $OptimFROGencoderSystemLookup[$EncoderSystemID] : 'undefined encoder type (0x'.dechex($EncoderSystemID).')').')'; - } - - public static function OptimFROGcompressionLookup($CompressionID) { - // mode = compression >> 3 - // speedup = compression & 0x07 - - $CompressionModeID = ($CompressionID & 0xF8) >> 3; - //$CompressionSpeedupID = ($CompressionID & 0x07); - - static $OptimFROGencoderModeLookup = array( - 0x00 => 'fast', - 0x01 => 'normal', - 0x02 => 'high', - 0x03 => 'extra', // extranew (some versions) - 0x04 => 'best', // bestnew (some versions) - 0x05 => 'ultra', - 0x06 => 'insane', - 0x07 => 'highnew', - 0x08 => 'extranew', - 0x09 => 'bestnew' - ); - return (isset($OptimFROGencoderModeLookup[$CompressionModeID]) ? $OptimFROGencoderModeLookup[$CompressionModeID] : 'undefined mode (0x'.str_pad(dechex($CompressionModeID), 2, '0', STR_PAD_LEFT).')'); - } - - public static function OptimFROGspeedupLookup($CompressionID) { - // mode = compression >> 3 - // speedup = compression & 0x07 - - //$CompressionModeID = ($CompressionID & 0xF8) >> 3; - $CompressionSpeedupID = ($CompressionID & 0x07); - - static $OptimFROGencoderSpeedupLookup = array( - 0x00 => '1x', - 0x01 => '2x', - 0x02 => '4x' - ); - return (isset($OptimFROGencoderSpeedupLookup[$CompressionSpeedupID]) ? $OptimFROGencoderSpeedupLookup[$CompressionSpeedupID] : 'undefined mode (0x'.dechex($CompressionSpeedupID)); - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.rkau.php b/src/Classes/Vendor/getid3/module.audio.rkau.php deleted file mode 100755 index 0ea051bc6..000000000 --- a/src/Classes/Vendor/getid3/module.audio.rkau.php +++ /dev/null @@ -1,92 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.shorten.php // -// module for analyzing Shorten Audio files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_rkau extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $RKAUHeader = fread($this->getid3->fp, 20); - $magic = 'RKA'; - if (substr($RKAUHeader, 0, 3) != $magic) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($RKAUHeader, 0, 3)).'"'; - return false; - } - - $info['fileformat'] = 'rkau'; - $info['audio']['dataformat'] = 'rkau'; - $info['audio']['bitrate_mode'] = 'vbr'; - - $info['rkau']['raw']['version'] = getid3_lib::LittleEndian2Int(substr($RKAUHeader, 3, 1)); - $info['rkau']['version'] = '1.'.str_pad($info['rkau']['raw']['version'] & 0x0F, 2, '0', STR_PAD_LEFT); - if (($info['rkau']['version'] > 1.07) || ($info['rkau']['version'] < 1.06)) { - $info['error'][] = 'This version of getID3() ['.$this->getid3->version().'] can only parse RKAU files v1.06 and 1.07 (this file is v'.$info['rkau']['version'].')'; - unset($info['rkau']); - return false; - } - - $info['rkau']['source_bytes'] = getid3_lib::LittleEndian2Int(substr($RKAUHeader, 4, 4)); - $info['rkau']['sample_rate'] = getid3_lib::LittleEndian2Int(substr($RKAUHeader, 8, 4)); - $info['rkau']['channels'] = getid3_lib::LittleEndian2Int(substr($RKAUHeader, 12, 1)); - $info['rkau']['bits_per_sample'] = getid3_lib::LittleEndian2Int(substr($RKAUHeader, 13, 1)); - - $info['rkau']['raw']['quality'] = getid3_lib::LittleEndian2Int(substr($RKAUHeader, 14, 1)); - $this->RKAUqualityLookup($info['rkau']); - - $info['rkau']['raw']['flags'] = getid3_lib::LittleEndian2Int(substr($RKAUHeader, 15, 1)); - $info['rkau']['flags']['joint_stereo'] = (bool) (!($info['rkau']['raw']['flags'] & 0x01)); - $info['rkau']['flags']['streaming'] = (bool) ($info['rkau']['raw']['flags'] & 0x02); - $info['rkau']['flags']['vrq_lossy_mode'] = (bool) ($info['rkau']['raw']['flags'] & 0x04); - - if ($info['rkau']['flags']['streaming']) { - $info['avdataoffset'] += 20; - $info['rkau']['compressed_bytes'] = getid3_lib::LittleEndian2Int(substr($RKAUHeader, 16, 4)); - } else { - $info['avdataoffset'] += 16; - $info['rkau']['compressed_bytes'] = $info['avdataend'] - $info['avdataoffset'] - 1; - } - // Note: compressed_bytes does not always equal what appears to be the actual number of compressed bytes, - // sometimes it's more, sometimes less. No idea why(?) - - $info['audio']['lossless'] = $info['rkau']['lossless']; - $info['audio']['channels'] = $info['rkau']['channels']; - $info['audio']['bits_per_sample'] = $info['rkau']['bits_per_sample']; - $info['audio']['sample_rate'] = $info['rkau']['sample_rate']; - - $info['playtime_seconds'] = $info['rkau']['source_bytes'] / ($info['rkau']['sample_rate'] * $info['rkau']['channels'] * ($info['rkau']['bits_per_sample'] / 8)); - $info['audio']['bitrate'] = ($info['rkau']['compressed_bytes'] * 8) / $info['playtime_seconds']; - - return true; - - } - - - public function RKAUqualityLookup(&$RKAUdata) { - $level = ($RKAUdata['raw']['quality'] & 0xF0) >> 4; - $quality = $RKAUdata['raw']['quality'] & 0x0F; - - $RKAUdata['lossless'] = (($quality == 0) ? true : false); - $RKAUdata['compression_level'] = $level + 1; - if (!$RKAUdata['lossless']) { - $RKAUdata['quality_setting'] = $quality; - } - - return true; - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.shorten.php b/src/Classes/Vendor/getid3/module.audio.shorten.php deleted file mode 100755 index a047f16f1..000000000 --- a/src/Classes/Vendor/getid3/module.audio.shorten.php +++ /dev/null @@ -1,181 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.shorten.php // -// module for analyzing Shorten Audio files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_shorten extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - - $ShortenHeader = fread($this->getid3->fp, 8); - $magic = 'ajkg'; - if (substr($ShortenHeader, 0, 4) != $magic) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($ShortenHeader, 0, 4)).'"'; - return false; - } - $info['fileformat'] = 'shn'; - $info['audio']['dataformat'] = 'shn'; - $info['audio']['lossless'] = true; - $info['audio']['bitrate_mode'] = 'vbr'; - - $info['shn']['version'] = getid3_lib::LittleEndian2Int(substr($ShortenHeader, 4, 1)); - - fseek($this->getid3->fp, $info['avdataend'] - 12, SEEK_SET); - $SeekTableSignatureTest = fread($this->getid3->fp, 12); - $info['shn']['seektable']['present'] = (bool) (substr($SeekTableSignatureTest, 4, 8) == 'SHNAMPSK'); - if ($info['shn']['seektable']['present']) { - $info['shn']['seektable']['length'] = getid3_lib::LittleEndian2Int(substr($SeekTableSignatureTest, 0, 4)); - $info['shn']['seektable']['offset'] = $info['avdataend'] - $info['shn']['seektable']['length']; - fseek($this->getid3->fp, $info['shn']['seektable']['offset'], SEEK_SET); - $SeekTableMagic = fread($this->getid3->fp, 4); - $magic = 'SEEK'; - if ($SeekTableMagic != $magic) { - - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" at offset '.$info['shn']['seektable']['offset'].', found "'.getid3_lib::PrintHexBytes($SeekTableMagic).'"'; - return false; - - } else { - - // typedef struct tag_TSeekEntry - // { - // unsigned long SampleNumber; - // unsigned long SHNFileByteOffset; - // unsigned long SHNLastBufferReadPosition; - // unsigned short SHNByteGet; - // unsigned short SHNBufferOffset; - // unsigned short SHNFileBitOffset; - // unsigned long SHNGBuffer; - // unsigned short SHNBitShift; - // long CBuf0[3]; - // long CBuf1[3]; - // long Offset0[4]; - // long Offset1[4]; - // }TSeekEntry; - - $SeekTableData = fread($this->getid3->fp, $info['shn']['seektable']['length'] - 16); - $info['shn']['seektable']['entry_count'] = floor(strlen($SeekTableData) / 80); - //$info['shn']['seektable']['entries'] = array(); - //$SeekTableOffset = 0; - //for ($i = 0; $i < $info['shn']['seektable']['entry_count']; $i++) { - // $SeekTableEntry['sample_number'] = getid3_lib::LittleEndian2Int(substr($SeekTableData, $SeekTableOffset, 4)); - // $SeekTableOffset += 4; - // $SeekTableEntry['shn_file_byte_offset'] = getid3_lib::LittleEndian2Int(substr($SeekTableData, $SeekTableOffset, 4)); - // $SeekTableOffset += 4; - // $SeekTableEntry['shn_last_buffer_read_position'] = getid3_lib::LittleEndian2Int(substr($SeekTableData, $SeekTableOffset, 4)); - // $SeekTableOffset += 4; - // $SeekTableEntry['shn_byte_get'] = getid3_lib::LittleEndian2Int(substr($SeekTableData, $SeekTableOffset, 2)); - // $SeekTableOffset += 2; - // $SeekTableEntry['shn_buffer_offset'] = getid3_lib::LittleEndian2Int(substr($SeekTableData, $SeekTableOffset, 2)); - // $SeekTableOffset += 2; - // $SeekTableEntry['shn_file_bit_offset'] = getid3_lib::LittleEndian2Int(substr($SeekTableData, $SeekTableOffset, 2)); - // $SeekTableOffset += 2; - // $SeekTableEntry['shn_gbuffer'] = getid3_lib::LittleEndian2Int(substr($SeekTableData, $SeekTableOffset, 4)); - // $SeekTableOffset += 4; - // $SeekTableEntry['shn_bit_shift'] = getid3_lib::LittleEndian2Int(substr($SeekTableData, $SeekTableOffset, 2)); - // $SeekTableOffset += 2; - // for ($j = 0; $j < 3; $j++) { - // $SeekTableEntry['cbuf0'][$j] = getid3_lib::LittleEndian2Int(substr($SeekTableData, $SeekTableOffset, 4)); - // $SeekTableOffset += 4; - // } - // for ($j = 0; $j < 3; $j++) { - // $SeekTableEntry['cbuf1'][$j] = getid3_lib::LittleEndian2Int(substr($SeekTableData, $SeekTableOffset, 4)); - // $SeekTableOffset += 4; - // } - // for ($j = 0; $j < 4; $j++) { - // $SeekTableEntry['offset0'][$j] = getid3_lib::LittleEndian2Int(substr($SeekTableData, $SeekTableOffset, 4)); - // $SeekTableOffset += 4; - // } - // for ($j = 0; $j < 4; $j++) { - // $SeekTableEntry['offset1'][$j] = getid3_lib::LittleEndian2Int(substr($SeekTableData, $SeekTableOffset, 4)); - // $SeekTableOffset += 4; - // } - // - // $info['shn']['seektable']['entries'][] = $SeekTableEntry; - //} - - } - - } - - if (preg_match('#(1|ON)#i', ini_get('safe_mode'))) { - $info['error'][] = 'PHP running in Safe Mode - backtick operator not available, cannot run shntool to analyze Shorten files'; - return false; - } - - if (GETID3_OS_ISWINDOWS) { - - $RequiredFiles = array('shorten.exe', 'cygwin1.dll', 'head.exe'); - foreach ($RequiredFiles as $required_file) { - if (!is_readable(GETID3_HELPERAPPSDIR.$required_file)) { - $info['error'][] = GETID3_HELPERAPPSDIR.$required_file.' does not exist'; - return false; - } - } - $commandline = GETID3_HELPERAPPSDIR.'shorten.exe -x "'.$info['filenamepath'].'" - | '.GETID3_HELPERAPPSDIR.'head.exe -c 64'; - $commandline = str_replace('/', '\\', $commandline); - - } else { - - static $shorten_present; - if (!isset($shorten_present)) { - $shorten_present = file_exists('/usr/local/bin/shorten') || `which shorten`; - } - if (!$shorten_present) { - $info['error'][] = 'shorten binary was not found in path or /usr/local/bin'; - return false; - } - $commandline = (file_exists('/usr/local/bin/shorten') ? '/usr/local/bin/' : '' ) . 'shorten -x '.escapeshellarg($info['filenamepath']).' - | head -c 64'; - - } - - $output = `$commandline`; - - if (!empty($output) && (substr($output, 12, 4) == 'fmt ')) { - - getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); - - $fmt_size = getid3_lib::LittleEndian2Int(substr($output, 16, 4)); - $DecodedWAVFORMATEX = getid3_riff::parseWAVEFORMATex(substr($output, 20, $fmt_size)); - $info['audio']['channels'] = $DecodedWAVFORMATEX['channels']; - $info['audio']['bits_per_sample'] = $DecodedWAVFORMATEX['bits_per_sample']; - $info['audio']['sample_rate'] = $DecodedWAVFORMATEX['sample_rate']; - - if (substr($output, 20 + $fmt_size, 4) == 'data') { - - $info['playtime_seconds'] = getid3_lib::LittleEndian2Int(substr($output, 20 + 4 + $fmt_size, 4)) / $DecodedWAVFORMATEX['raw']['nAvgBytesPerSec']; - - } else { - - $info['error'][] = 'shorten failed to decode DATA chunk to expected location, cannot determine playtime'; - return false; - - } - - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) / $info['playtime_seconds']) * 8; - - } else { - - $info['error'][] = 'shorten failed to decode file to WAV for parsing'; - return false; - - } - - return true; - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.tta.php b/src/Classes/Vendor/getid3/module.audio.tta.php deleted file mode 100755 index a3056db69..000000000 --- a/src/Classes/Vendor/getid3/module.audio.tta.php +++ /dev/null @@ -1,106 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.tta.php // -// module for analyzing TTA Audio files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_tta extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'tta'; - $info['audio']['dataformat'] = 'tta'; - $info['audio']['lossless'] = true; - $info['audio']['bitrate_mode'] = 'vbr'; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $ttaheader = fread($this->getid3->fp, 26); - - $info['tta']['magic'] = substr($ttaheader, 0, 3); - $magic = 'TTA'; - if ($info['tta']['magic'] != $magic) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($info['tta']['magic']).'"'; - unset($info['fileformat']); - unset($info['audio']); - unset($info['tta']); - return false; - } - - switch ($ttaheader{3}) { - case "\x01": // TTA v1.x - case "\x02": // TTA v1.x - case "\x03": // TTA v1.x - // "It was the demo-version of the TTA encoder. There is no released format with such header. TTA encoder v1 is not supported about a year." - $info['tta']['major_version'] = 1; - $info['avdataoffset'] += 16; - - $info['tta']['compression_level'] = ord($ttaheader{3}); - $info['tta']['channels'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 4, 2)); - $info['tta']['bits_per_sample'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 6, 2)); - $info['tta']['sample_rate'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 8, 4)); - $info['tta']['samples_per_channel'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 12, 4)); - - $info['audio']['encoder_options'] = '-e'.$info['tta']['compression_level']; - $info['playtime_seconds'] = $info['tta']['samples_per_channel'] / $info['tta']['sample_rate']; - break; - - case '2': // TTA v2.x - // "I have hurried to release the TTA 2.0 encoder. Format documentation is removed from our site. This format still in development. Please wait the TTA2 format, encoder v4." - $info['tta']['major_version'] = 2; - $info['avdataoffset'] += 20; - - $info['tta']['compression_level'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 4, 2)); - $info['tta']['audio_format'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 6, 2)); - $info['tta']['channels'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 8, 2)); - $info['tta']['bits_per_sample'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 10, 2)); - $info['tta']['sample_rate'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 12, 4)); - $info['tta']['data_length'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 16, 4)); - - $info['audio']['encoder_options'] = '-e'.$info['tta']['compression_level']; - $info['playtime_seconds'] = $info['tta']['data_length'] / $info['tta']['sample_rate']; - break; - - case '1': // TTA v3.x - // "This is a first stable release of the TTA format. It will be supported by the encoders v3 or higher." - $info['tta']['major_version'] = 3; - $info['avdataoffset'] += 26; - - $info['tta']['audio_format'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 4, 2)); // getid3_riff::wFormatTagLookup() - $info['tta']['channels'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 6, 2)); - $info['tta']['bits_per_sample'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 8, 2)); - $info['tta']['sample_rate'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 10, 4)); - $info['tta']['data_length'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 14, 4)); - $info['tta']['crc32_footer'] = substr($ttaheader, 18, 4); - $info['tta']['seek_point'] = getid3_lib::LittleEndian2Int(substr($ttaheader, 22, 4)); - - $info['playtime_seconds'] = $info['tta']['data_length'] / $info['tta']['sample_rate']; - break; - - default: - $info['error'][] = 'This version of getID3() ['.$this->getid3->version().'] only knows how to handle TTA v1 and v2 - it may not work correctly with this file which appears to be TTA v'.$ttaheader{3}; - return false; - break; - } - - $info['audio']['encoder'] = 'TTA v'.$info['tta']['major_version']; - $info['audio']['bits_per_sample'] = $info['tta']['bits_per_sample']; - $info['audio']['sample_rate'] = $info['tta']['sample_rate']; - $info['audio']['channels'] = $info['tta']['channels']; - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; - - return true; - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.voc.php b/src/Classes/Vendor/getid3/module.audio.voc.php deleted file mode 100755 index e38fa482b..000000000 --- a/src/Classes/Vendor/getid3/module.audio.voc.php +++ /dev/null @@ -1,204 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.voc.php // -// module for analyzing Creative VOC Audio files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_voc extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - $OriginalAVdataOffset = $info['avdataoffset']; - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $VOCheader = fread($this->getid3->fp, 26); - - $magic = 'Creative Voice File'; - if (substr($VOCheader, 0, 19) != $magic) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($VOCheader, 0, 19)).'"'; - return false; - } - - // shortcuts - $thisfile_audio = &$info['audio']; - $info['voc'] = array(); - $thisfile_voc = &$info['voc']; - - $info['fileformat'] = 'voc'; - $thisfile_audio['dataformat'] = 'voc'; - $thisfile_audio['bitrate_mode'] = 'cbr'; - $thisfile_audio['lossless'] = true; - $thisfile_audio['channels'] = 1; // might be overriden below - $thisfile_audio['bits_per_sample'] = 8; // might be overriden below - - // byte # Description - // ------ ------------------------------------------ - // 00-12 'Creative Voice File' - // 13 1A (eof to abort printing of file) - // 14-15 Offset of first datablock in .voc file (std 1A 00 in Intel Notation) - // 16-17 Version number (minor,major) (VOC-HDR puts 0A 01) - // 18-19 2's Comp of Ver. # + 1234h (VOC-HDR puts 29 11) - - $thisfile_voc['header']['datablock_offset'] = getid3_lib::LittleEndian2Int(substr($VOCheader, 20, 2)); - $thisfile_voc['header']['minor_version'] = getid3_lib::LittleEndian2Int(substr($VOCheader, 22, 1)); - $thisfile_voc['header']['major_version'] = getid3_lib::LittleEndian2Int(substr($VOCheader, 23, 1)); - - do { - - $BlockOffset = ftell($this->getid3->fp); - $BlockData = fread($this->getid3->fp, 4); - $BlockType = ord($BlockData{0}); - $BlockSize = getid3_lib::LittleEndian2Int(substr($BlockData, 1, 3)); - $ThisBlock = array(); - - getid3_lib::safe_inc($thisfile_voc['blocktypes'][$BlockType], 1); - switch ($BlockType) { - case 0: // Terminator - // do nothing, we'll break out of the loop down below - break; - - case 1: // Sound data - $BlockData .= fread($this->getid3->fp, 2); - if ($info['avdataoffset'] <= $OriginalAVdataOffset) { - $info['avdataoffset'] = ftell($this->getid3->fp); - } - fseek($this->getid3->fp, $BlockSize - 2, SEEK_CUR); - - $ThisBlock['sample_rate_id'] = getid3_lib::LittleEndian2Int(substr($BlockData, 4, 1)); - $ThisBlock['compression_type'] = getid3_lib::LittleEndian2Int(substr($BlockData, 5, 1)); - - $ThisBlock['compression_name'] = $this->VOCcompressionTypeLookup($ThisBlock['compression_type']); - if ($ThisBlock['compression_type'] <= 3) { - $thisfile_voc['compressed_bits_per_sample'] = getid3_lib::CastAsInt(str_replace('-bit', '', $ThisBlock['compression_name'])); - } - - // Less accurate sample_rate calculation than the Extended block (#8) data (but better than nothing if Extended Block is not available) - if (empty($thisfile_audio['sample_rate'])) { - // SR byte = 256 - (1000000 / sample_rate) - $thisfile_audio['sample_rate'] = getid3_lib::trunc((1000000 / (256 - $ThisBlock['sample_rate_id'])) / $thisfile_audio['channels']); - } - break; - - case 2: // Sound continue - case 3: // Silence - case 4: // Marker - case 6: // Repeat - case 7: // End repeat - // nothing useful, just skip - fseek($this->getid3->fp, $BlockSize, SEEK_CUR); - break; - - case 8: // Extended - $BlockData .= fread($this->getid3->fp, 4); - - //00-01 Time Constant: - // Mono: 65536 - (256000000 / sample_rate) - // Stereo: 65536 - (256000000 / (sample_rate * 2)) - $ThisBlock['time_constant'] = getid3_lib::LittleEndian2Int(substr($BlockData, 4, 2)); - $ThisBlock['pack_method'] = getid3_lib::LittleEndian2Int(substr($BlockData, 6, 1)); - $ThisBlock['stereo'] = (bool) getid3_lib::LittleEndian2Int(substr($BlockData, 7, 1)); - - $thisfile_audio['channels'] = ($ThisBlock['stereo'] ? 2 : 1); - $thisfile_audio['sample_rate'] = getid3_lib::trunc((256000000 / (65536 - $ThisBlock['time_constant'])) / $thisfile_audio['channels']); - break; - - case 9: // data block that supersedes blocks 1 and 8. Used for stereo, 16 bit - $BlockData .= fread($this->getid3->fp, 12); - if ($info['avdataoffset'] <= $OriginalAVdataOffset) { - $info['avdataoffset'] = ftell($this->getid3->fp); - } - fseek($this->getid3->fp, $BlockSize - 12, SEEK_CUR); - - $ThisBlock['sample_rate'] = getid3_lib::LittleEndian2Int(substr($BlockData, 4, 4)); - $ThisBlock['bits_per_sample'] = getid3_lib::LittleEndian2Int(substr($BlockData, 8, 1)); - $ThisBlock['channels'] = getid3_lib::LittleEndian2Int(substr($BlockData, 9, 1)); - $ThisBlock['wFormat'] = getid3_lib::LittleEndian2Int(substr($BlockData, 10, 2)); - - $ThisBlock['compression_name'] = $this->VOCwFormatLookup($ThisBlock['wFormat']); - if ($this->VOCwFormatActualBitsPerSampleLookup($ThisBlock['wFormat'])) { - $thisfile_voc['compressed_bits_per_sample'] = $this->VOCwFormatActualBitsPerSampleLookup($ThisBlock['wFormat']); - } - - $thisfile_audio['sample_rate'] = $ThisBlock['sample_rate']; - $thisfile_audio['bits_per_sample'] = $ThisBlock['bits_per_sample']; - $thisfile_audio['channels'] = $ThisBlock['channels']; - break; - - default: - $info['warning'][] = 'Unhandled block type "'.$BlockType.'" at offset '.$BlockOffset; - fseek($this->getid3->fp, $BlockSize, SEEK_CUR); - break; - } - - if (!empty($ThisBlock)) { - $ThisBlock['block_offset'] = $BlockOffset; - $ThisBlock['block_size'] = $BlockSize; - $ThisBlock['block_type_id'] = $BlockType; - $thisfile_voc['blocks'][] = $ThisBlock; - } - - } while (!feof($this->getid3->fp) && ($BlockType != 0)); - - // Terminator block doesn't have size field, so seek back 3 spaces - fseek($this->getid3->fp, -3, SEEK_CUR); - - ksort($thisfile_voc['blocktypes']); - - if (!empty($thisfile_voc['compressed_bits_per_sample'])) { - $info['playtime_seconds'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / ($thisfile_voc['compressed_bits_per_sample'] * $thisfile_audio['channels'] * $thisfile_audio['sample_rate']); - $thisfile_audio['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; - } - - return true; - } - - public function VOCcompressionTypeLookup($index) { - static $VOCcompressionTypeLookup = array( - 0 => '8-bit', - 1 => '4-bit', - 2 => '2.6-bit', - 3 => '2-bit' - ); - return (isset($VOCcompressionTypeLookup[$index]) ? $VOCcompressionTypeLookup[$index] : 'Multi DAC ('.($index - 3).') channels'); - } - - public function VOCwFormatLookup($index) { - static $VOCwFormatLookup = array( - 0x0000 => '8-bit unsigned PCM', - 0x0001 => 'Creative 8-bit to 4-bit ADPCM', - 0x0002 => 'Creative 8-bit to 3-bit ADPCM', - 0x0003 => 'Creative 8-bit to 2-bit ADPCM', - 0x0004 => '16-bit signed PCM', - 0x0006 => 'CCITT a-Law', - 0x0007 => 'CCITT u-Law', - 0x2000 => 'Creative 16-bit to 4-bit ADPCM' - ); - return (isset($VOCwFormatLookup[$index]) ? $VOCwFormatLookup[$index] : false); - } - - public function VOCwFormatActualBitsPerSampleLookup($index) { - static $VOCwFormatLookup = array( - 0x0000 => 8, - 0x0001 => 4, - 0x0002 => 3, - 0x0003 => 2, - 0x0004 => 16, - 0x0006 => 8, - 0x0007 => 8, - 0x2000 => 4 - ); - return (isset($VOCwFormatLookup[$index]) ? $VOCwFormatLookup[$index] : false); - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.vqf.php b/src/Classes/Vendor/getid3/module.audio.vqf.php deleted file mode 100755 index b963c6ece..000000000 --- a/src/Classes/Vendor/getid3/module.audio.vqf.php +++ /dev/null @@ -1,159 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.vqf.php // -// module for analyzing VQF audio files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_vqf extends getid3_handler -{ - public function Analyze() { - $info = &$this->getid3->info; - - // based loosely on code from TTwinVQ by Jurgen Faul - // http://jfaul.de/atl or http://j-faul.virtualave.net/atl/atl.html - - $info['fileformat'] = 'vqf'; - $info['audio']['dataformat'] = 'vqf'; - $info['audio']['bitrate_mode'] = 'cbr'; - $info['audio']['lossless'] = false; - - // shortcut - $info['vqf']['raw'] = array(); - $thisfile_vqf = &$info['vqf']; - $thisfile_vqf_raw = &$thisfile_vqf['raw']; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $VQFheaderData = fread($this->getid3->fp, 16); - - $offset = 0; - $thisfile_vqf_raw['header_tag'] = substr($VQFheaderData, $offset, 4); - $magic = 'TWIN'; - if ($thisfile_vqf_raw['header_tag'] != $magic) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($thisfile_vqf_raw['header_tag']).'"'; - unset($info['vqf']); - unset($info['fileformat']); - return false; - } - $offset += 4; - $thisfile_vqf_raw['version'] = substr($VQFheaderData, $offset, 8); - $offset += 8; - $thisfile_vqf_raw['size'] = getid3_lib::BigEndian2Int(substr($VQFheaderData, $offset, 4)); - $offset += 4; - - while (ftell($this->getid3->fp) < $info['avdataend']) { - - $ChunkBaseOffset = ftell($this->getid3->fp); - $chunkoffset = 0; - $ChunkData = fread($this->getid3->fp, 8); - $ChunkName = substr($ChunkData, $chunkoffset, 4); - if ($ChunkName == 'DATA') { - $info['avdataoffset'] = $ChunkBaseOffset; - break; - } - $chunkoffset += 4; - $ChunkSize = getid3_lib::BigEndian2Int(substr($ChunkData, $chunkoffset, 4)); - $chunkoffset += 4; - if ($ChunkSize > ($info['avdataend'] - ftell($this->getid3->fp))) { - $info['error'][] = 'Invalid chunk size ('.$ChunkSize.') for chunk "'.$ChunkName.'" at offset '.$ChunkBaseOffset; - break; - } - if ($ChunkSize > 0) { - $ChunkData .= fread($this->getid3->fp, $ChunkSize); - } - - switch ($ChunkName) { - case 'COMM': - // shortcut - $thisfile_vqf['COMM'] = array(); - $thisfile_vqf_COMM = &$thisfile_vqf['COMM']; - - $thisfile_vqf_COMM['channel_mode'] = getid3_lib::BigEndian2Int(substr($ChunkData, $chunkoffset, 4)); - $chunkoffset += 4; - $thisfile_vqf_COMM['bitrate'] = getid3_lib::BigEndian2Int(substr($ChunkData, $chunkoffset, 4)); - $chunkoffset += 4; - $thisfile_vqf_COMM['sample_rate'] = getid3_lib::BigEndian2Int(substr($ChunkData, $chunkoffset, 4)); - $chunkoffset += 4; - $thisfile_vqf_COMM['security_level'] = getid3_lib::BigEndian2Int(substr($ChunkData, $chunkoffset, 4)); - $chunkoffset += 4; - - $info['audio']['channels'] = $thisfile_vqf_COMM['channel_mode'] + 1; - $info['audio']['sample_rate'] = $this->VQFchannelFrequencyLookup($thisfile_vqf_COMM['sample_rate']); - $info['audio']['bitrate'] = $thisfile_vqf_COMM['bitrate'] * 1000; - $info['audio']['encoder_options'] = 'CBR' . ceil($info['audio']['bitrate']/1000); - - if ($info['audio']['bitrate'] == 0) { - $info['error'][] = 'Corrupt VQF file: bitrate_audio == zero'; - return false; - } - break; - - case 'NAME': - case 'AUTH': - case '(c) ': - case 'FILE': - case 'COMT': - case 'ALBM': - $thisfile_vqf['comments'][$this->VQFcommentNiceNameLookup($ChunkName)][] = trim(substr($ChunkData, 8)); - break; - - case 'DSIZ': - $thisfile_vqf['DSIZ'] = getid3_lib::BigEndian2Int(substr($ChunkData, 8, 4)); - break; - - default: - $info['warning'][] = 'Unhandled chunk type "'.$ChunkName.'" at offset '.$ChunkBaseOffset; - break; - } - } - - $info['playtime_seconds'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['audio']['bitrate']; - - if (isset($thisfile_vqf['DSIZ']) && (($thisfile_vqf['DSIZ'] != ($info['avdataend'] - $info['avdataoffset'] - strlen('DATA'))))) { - switch ($thisfile_vqf['DSIZ']) { - case 0: - case 1: - $info['warning'][] = 'Invalid DSIZ value "'.$thisfile_vqf['DSIZ'].'". This is known to happen with VQF files encoded by Ahead Nero, and seems to be its way of saying this is TwinVQF v'.($thisfile_vqf['DSIZ'] + 1).'.0'; - $info['audio']['encoder'] = 'Ahead Nero'; - break; - - default: - $info['warning'][] = 'Probable corrupted file - should be '.$thisfile_vqf['DSIZ'].' bytes, actually '.($info['avdataend'] - $info['avdataoffset'] - strlen('DATA')); - break; - } - } - - return true; - } - - public function VQFchannelFrequencyLookup($frequencyid) { - static $VQFchannelFrequencyLookup = array( - 11 => 11025, - 22 => 22050, - 44 => 44100 - ); - return (isset($VQFchannelFrequencyLookup[$frequencyid]) ? $VQFchannelFrequencyLookup[$frequencyid] : $frequencyid * 1000); - } - - public function VQFcommentNiceNameLookup($shortname) { - static $VQFcommentNiceNameLookup = array( - 'NAME' => 'title', - 'AUTH' => 'artist', - '(c) ' => 'copyright', - 'FILE' => 'filename', - 'COMT' => 'comment', - 'ALBM' => 'album' - ); - return (isset($VQFcommentNiceNameLookup[$shortname]) ? $VQFcommentNiceNameLookup[$shortname] : $shortname); - } - -} diff --git a/src/Classes/Vendor/getid3/module.audio.wavpack.php b/src/Classes/Vendor/getid3/module.audio.wavpack.php deleted file mode 100755 index 8daf60346..000000000 --- a/src/Classes/Vendor/getid3/module.audio.wavpack.php +++ /dev/null @@ -1,397 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.audio.wavpack.php // -// module for analyzing WavPack v4.0+ Audio files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_wavpack extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - - while (true) { - - $wavpackheader = fread($this->getid3->fp, 32); - - if (ftell($this->getid3->fp) >= $info['avdataend']) { - break; - } elseif (feof($this->getid3->fp)) { - break; - } elseif ( - isset($info['wavpack']['blockheader']['total_samples']) && - isset($info['wavpack']['blockheader']['block_samples']) && - ($info['wavpack']['blockheader']['total_samples'] > 0) && - ($info['wavpack']['blockheader']['block_samples'] > 0) && - (!isset($info['wavpack']['riff_trailer_size']) || ($info['wavpack']['riff_trailer_size'] <= 0)) && - ((isset($info['wavpack']['config_flags']['md5_checksum']) && ($info['wavpack']['config_flags']['md5_checksum'] === false)) || !empty($info['md5_data_source']))) { - break; - } - - $blockheader_offset = ftell($this->getid3->fp) - 32; - $blockheader_magic = substr($wavpackheader, 0, 4); - $blockheader_size = getid3_lib::LittleEndian2Int(substr($wavpackheader, 4, 4)); - - $magic = 'wvpk'; - if ($blockheader_magic != $magic) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" at offset '.$blockheader_offset.', found "'.getid3_lib::PrintHexBytes($blockheader_magic).'"'; - switch (isset($info['audio']['dataformat']) ? $info['audio']['dataformat'] : '') { - case 'wavpack': - case 'wvc': - break; - default: - unset($info['fileformat']); - unset($info['audio']); - unset($info['wavpack']); - break; - } - return false; - } - - if (empty($info['wavpack']['blockheader']['block_samples']) || - empty($info['wavpack']['blockheader']['total_samples']) || - ($info['wavpack']['blockheader']['block_samples'] <= 0) || - ($info['wavpack']['blockheader']['total_samples'] <= 0)) { - // Also, it is possible that the first block might not have - // any samples (block_samples == 0) and in this case you should skip blocks - // until you find one with samples because the other information (like - // total_samples) are not guaranteed to be correct until (block_samples > 0) - - // Finally, I have defined a format for files in which the length is not known - // (for example when raw files are created using pipes). In these cases - // total_samples will be -1 and you must seek to the final block to determine - // the total number of samples. - - - $info['audio']['dataformat'] = 'wavpack'; - $info['fileformat'] = 'wavpack'; - $info['audio']['lossless'] = true; - $info['audio']['bitrate_mode'] = 'vbr'; - - $info['wavpack']['blockheader']['offset'] = $blockheader_offset; - $info['wavpack']['blockheader']['magic'] = $blockheader_magic; - $info['wavpack']['blockheader']['size'] = $blockheader_size; - - if ($info['wavpack']['blockheader']['size'] >= 0x100000) { - $info['error'][] = 'Expecting WavPack block size less than "0x100000", found "'.$info['wavpack']['blockheader']['size'].'" at offset '.$info['wavpack']['blockheader']['offset']; - switch (isset($info['audio']['dataformat']) ? $info['audio']['dataformat'] : '') { - case 'wavpack': - case 'wvc': - break; - default: - unset($info['fileformat']); - unset($info['audio']); - unset($info['wavpack']); - break; - } - return false; - } - - $info['wavpack']['blockheader']['minor_version'] = ord($wavpackheader{8}); - $info['wavpack']['blockheader']['major_version'] = ord($wavpackheader{9}); - - if (($info['wavpack']['blockheader']['major_version'] != 4) || - (($info['wavpack']['blockheader']['minor_version'] < 4) && - ($info['wavpack']['blockheader']['minor_version'] > 16))) { - $info['error'][] = 'Expecting WavPack version between "4.2" and "4.16", found version "'.$info['wavpack']['blockheader']['major_version'].'.'.$info['wavpack']['blockheader']['minor_version'].'" at offset '.$info['wavpack']['blockheader']['offset']; - switch (isset($info['audio']['dataformat']) ? $info['audio']['dataformat'] : '') { - case 'wavpack': - case 'wvc': - break; - default: - unset($info['fileformat']); - unset($info['audio']); - unset($info['wavpack']); - break; - } - return false; - } - - $info['wavpack']['blockheader']['track_number'] = ord($wavpackheader{10}); // unused - $info['wavpack']['blockheader']['index_number'] = ord($wavpackheader{11}); // unused - $info['wavpack']['blockheader']['total_samples'] = getid3_lib::LittleEndian2Int(substr($wavpackheader, 12, 4)); - $info['wavpack']['blockheader']['block_index'] = getid3_lib::LittleEndian2Int(substr($wavpackheader, 16, 4)); - $info['wavpack']['blockheader']['block_samples'] = getid3_lib::LittleEndian2Int(substr($wavpackheader, 20, 4)); - $info['wavpack']['blockheader']['flags_raw'] = getid3_lib::LittleEndian2Int(substr($wavpackheader, 24, 4)); - $info['wavpack']['blockheader']['crc'] = getid3_lib::LittleEndian2Int(substr($wavpackheader, 28, 4)); - - $info['wavpack']['blockheader']['flags']['bytes_per_sample'] = 1 + ($info['wavpack']['blockheader']['flags_raw'] & 0x00000003); - $info['wavpack']['blockheader']['flags']['mono'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000004); - $info['wavpack']['blockheader']['flags']['hybrid'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000008); - $info['wavpack']['blockheader']['flags']['joint_stereo'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000010); - $info['wavpack']['blockheader']['flags']['cross_decorrelation'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000020); - $info['wavpack']['blockheader']['flags']['hybrid_noiseshape'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000040); - $info['wavpack']['blockheader']['flags']['ieee_32bit_float'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000080); - $info['wavpack']['blockheader']['flags']['int_32bit'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000100); - $info['wavpack']['blockheader']['flags']['hybrid_bitrate_noise'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000200); - $info['wavpack']['blockheader']['flags']['hybrid_balance_noise'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000400); - $info['wavpack']['blockheader']['flags']['multichannel_initial'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00000800); - $info['wavpack']['blockheader']['flags']['multichannel_final'] = (bool) ($info['wavpack']['blockheader']['flags_raw'] & 0x00001000); - - $info['audio']['lossless'] = !$info['wavpack']['blockheader']['flags']['hybrid']; - } - - while (!feof($this->getid3->fp) && (ftell($this->getid3->fp) < ($blockheader_offset + $blockheader_size + 8))) { - - $metablock = array('offset'=>ftell($this->getid3->fp)); - $metablockheader = fread($this->getid3->fp, 2); - if (feof($this->getid3->fp)) { - break; - } - $metablock['id'] = ord($metablockheader{0}); - $metablock['function_id'] = ($metablock['id'] & 0x3F); - $metablock['function_name'] = $this->WavPackMetablockNameLookup($metablock['function_id']); - - // The 0x20 bit in the id of the meta subblocks (which is defined as - // ID_OPTIONAL_DATA) is a permanent part of the id. The idea is that - // if a decoder encounters an id that it does not know about, it uses - // that "ID_OPTIONAL_DATA" flag to determine what to do. If it is set - // then the decoder simply ignores the metadata, but if it is zero - // then the decoder should quit because it means that an understanding - // of the metadata is required to correctly decode the audio. - $metablock['non_decoder'] = (bool) ($metablock['id'] & 0x20); - - $metablock['padded_data'] = (bool) ($metablock['id'] & 0x40); - $metablock['large_block'] = (bool) ($metablock['id'] & 0x80); - if ($metablock['large_block']) { - $metablockheader .= fread($this->getid3->fp, 2); - } - $metablock['size'] = getid3_lib::LittleEndian2Int(substr($metablockheader, 1)) * 2; // size is stored in words - $metablock['data'] = null; - - if ($metablock['size'] > 0) { - - switch ($metablock['function_id']) { - case 0x21: // ID_RIFF_HEADER - case 0x22: // ID_RIFF_TRAILER - case 0x23: // ID_REPLAY_GAIN - case 0x24: // ID_CUESHEET - case 0x25: // ID_CONFIG_BLOCK - case 0x26: // ID_MD5_CHECKSUM - $metablock['data'] = fread($this->getid3->fp, $metablock['size']); - - if ($metablock['padded_data']) { - // padded to the nearest even byte - $metablock['size']--; - $metablock['data'] = substr($metablock['data'], 0, -1); - } - break; - - case 0x00: // ID_DUMMY - case 0x01: // ID_ENCODER_INFO - case 0x02: // ID_DECORR_TERMS - case 0x03: // ID_DECORR_WEIGHTS - case 0x04: // ID_DECORR_SAMPLES - case 0x05: // ID_ENTROPY_VARS - case 0x06: // ID_HYBRID_PROFILE - case 0x07: // ID_SHAPING_WEIGHTS - case 0x08: // ID_FLOAT_INFO - case 0x09: // ID_INT32_INFO - case 0x0A: // ID_WV_BITSTREAM - case 0x0B: // ID_WVC_BITSTREAM - case 0x0C: // ID_WVX_BITSTREAM - case 0x0D: // ID_CHANNEL_INFO - fseek($this->getid3->fp, $metablock['offset'] + ($metablock['large_block'] ? 4 : 2) + $metablock['size'], SEEK_SET); - break; - - default: - $info['warning'][] = 'Unexpected metablock type "0x'.str_pad(dechex($metablock['function_id']), 2, '0', STR_PAD_LEFT).'" at offset '.$metablock['offset']; - fseek($this->getid3->fp, $metablock['offset'] + ($metablock['large_block'] ? 4 : 2) + $metablock['size'], SEEK_SET); - break; - } - - switch ($metablock['function_id']) { - case 0x21: // ID_RIFF_HEADER - getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); - $original_wav_filesize = getid3_lib::LittleEndian2Int(substr($metablock['data'], 4, 4)); - - $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); - $getid3_riff = new getid3_riff($getid3_temp); - $getid3_riff->ParseRIFFdata($metablock['data']); - $metablock['riff'] = $getid3_temp->info['riff']; - $info['audio']['sample_rate'] = $getid3_temp->info['riff']['raw']['fmt ']['nSamplesPerSec']; - unset($getid3_riff, $getid3_temp); - - $metablock['riff']['original_filesize'] = $original_wav_filesize; - $info['wavpack']['riff_trailer_size'] = $original_wav_filesize - $metablock['riff']['WAVE']['data'][0]['size'] - $metablock['riff']['header_size']; - $info['playtime_seconds'] = $info['wavpack']['blockheader']['total_samples'] / $info['audio']['sample_rate']; - - // Safe RIFF header in case there's a RIFF footer later - $metablockRIFFheader = $metablock['data']; - break; - - - case 0x22: // ID_RIFF_TRAILER - $metablockRIFFfooter = $metablockRIFFheader.$metablock['data']; - getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio-video.riff.php', __FILE__, true); - - $startoffset = $metablock['offset'] + ($metablock['large_block'] ? 4 : 2); - $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); - $getid3_temp->info['avdataend'] = $info['avdataend']; - $getid3_temp->info['fileformat'] = 'riff'; - $getid3_riff = new getid3_riff($getid3_temp); - $metablock['riff'] = $getid3_riff->ParseRIFF($startoffset, $startoffset + $metablock['size']); - - if (!empty($metablock['riff']['INFO'])) { - getid3_riff::parseComments($metablock['riff']['INFO'], $metablock['comments']); - $info['tags']['riff'] = $metablock['comments']; - } - unset($getid3_temp, $getid3_riff); - break; - - - case 0x23: // ID_REPLAY_GAIN - $info['warning'][] = 'WavPack "Replay Gain" contents not yet handled by getID3() in metablock at offset '.$metablock['offset']; - break; - - - case 0x24: // ID_CUESHEET - $info['warning'][] = 'WavPack "Cuesheet" contents not yet handled by getID3() in metablock at offset '.$metablock['offset']; - break; - - - case 0x25: // ID_CONFIG_BLOCK - $metablock['flags_raw'] = getid3_lib::LittleEndian2Int(substr($metablock['data'], 0, 3)); - - $metablock['flags']['adobe_mode'] = (bool) ($metablock['flags_raw'] & 0x000001); // "adobe" mode for 32-bit floats - $metablock['flags']['fast_flag'] = (bool) ($metablock['flags_raw'] & 0x000002); // fast mode - $metablock['flags']['very_fast_flag'] = (bool) ($metablock['flags_raw'] & 0x000004); // double fast - $metablock['flags']['high_flag'] = (bool) ($metablock['flags_raw'] & 0x000008); // high quality mode - $metablock['flags']['very_high_flag'] = (bool) ($metablock['flags_raw'] & 0x000010); // double high (not used yet) - $metablock['flags']['bitrate_kbps'] = (bool) ($metablock['flags_raw'] & 0x000020); // bitrate is kbps, not bits / sample - $metablock['flags']['auto_shaping'] = (bool) ($metablock['flags_raw'] & 0x000040); // automatic noise shaping - $metablock['flags']['shape_override'] = (bool) ($metablock['flags_raw'] & 0x000080); // shaping mode specified - $metablock['flags']['joint_override'] = (bool) ($metablock['flags_raw'] & 0x000100); // joint-stereo mode specified - $metablock['flags']['copy_time'] = (bool) ($metablock['flags_raw'] & 0x000200); // copy file-time from source - $metablock['flags']['create_exe'] = (bool) ($metablock['flags_raw'] & 0x000400); // create executable - $metablock['flags']['create_wvc'] = (bool) ($metablock['flags_raw'] & 0x000800); // create correction file - $metablock['flags']['optimize_wvc'] = (bool) ($metablock['flags_raw'] & 0x001000); // maximize bybrid compression - $metablock['flags']['quality_mode'] = (bool) ($metablock['flags_raw'] & 0x002000); // psychoacoustic quality mode - $metablock['flags']['raw_flag'] = (bool) ($metablock['flags_raw'] & 0x004000); // raw mode (not implemented yet) - $metablock['flags']['calc_noise'] = (bool) ($metablock['flags_raw'] & 0x008000); // calc noise in hybrid mode - $metablock['flags']['lossy_mode'] = (bool) ($metablock['flags_raw'] & 0x010000); // obsolete (for information) - $metablock['flags']['extra_mode'] = (bool) ($metablock['flags_raw'] & 0x020000); // extra processing mode - $metablock['flags']['skip_wvx'] = (bool) ($metablock['flags_raw'] & 0x040000); // no wvx stream w/ floats & big ints - $metablock['flags']['md5_checksum'] = (bool) ($metablock['flags_raw'] & 0x080000); // compute & store MD5 signature - $metablock['flags']['quiet_mode'] = (bool) ($metablock['flags_raw'] & 0x100000); // don't report progress % - - $info['wavpack']['config_flags'] = $metablock['flags']; - - - $info['audio']['encoder_options'] = ''; - if ($info['wavpack']['blockheader']['flags']['hybrid']) { - $info['audio']['encoder_options'] .= ' -b???'; - } - $info['audio']['encoder_options'] .= ($metablock['flags']['adobe_mode'] ? ' -a' : ''); - $info['audio']['encoder_options'] .= ($metablock['flags']['optimize_wvc'] ? ' -cc' : ''); - $info['audio']['encoder_options'] .= ($metablock['flags']['create_exe'] ? ' -e' : ''); - $info['audio']['encoder_options'] .= ($metablock['flags']['fast_flag'] ? ' -f' : ''); - $info['audio']['encoder_options'] .= ($metablock['flags']['joint_override'] ? ' -j?' : ''); - $info['audio']['encoder_options'] .= ($metablock['flags']['high_flag'] ? ' -h' : ''); - $info['audio']['encoder_options'] .= ($metablock['flags']['md5_checksum'] ? ' -m' : ''); - $info['audio']['encoder_options'] .= ($metablock['flags']['calc_noise'] ? ' -n' : ''); - $info['audio']['encoder_options'] .= ($metablock['flags']['shape_override'] ? ' -s?' : ''); - $info['audio']['encoder_options'] .= ($metablock['flags']['extra_mode'] ? ' -x?' : ''); - if (!empty($info['audio']['encoder_options'])) { - $info['audio']['encoder_options'] = trim($info['audio']['encoder_options']); - } elseif (isset($info['audio']['encoder_options'])) { - unset($info['audio']['encoder_options']); - } - break; - - - case 0x26: // ID_MD5_CHECKSUM - if (strlen($metablock['data']) == 16) { - $info['md5_data_source'] = strtolower(getid3_lib::PrintHexBytes($metablock['data'], true, false, false)); - } else { - $info['warning'][] = 'Expecting 16 bytes of WavPack "MD5 Checksum" in metablock at offset '.$metablock['offset'].', but found '.strlen($metablock['data']).' bytes'; - } - break; - - - case 0x00: // ID_DUMMY - case 0x01: // ID_ENCODER_INFO - case 0x02: // ID_DECORR_TERMS - case 0x03: // ID_DECORR_WEIGHTS - case 0x04: // ID_DECORR_SAMPLES - case 0x05: // ID_ENTROPY_VARS - case 0x06: // ID_HYBRID_PROFILE - case 0x07: // ID_SHAPING_WEIGHTS - case 0x08: // ID_FLOAT_INFO - case 0x09: // ID_INT32_INFO - case 0x0A: // ID_WV_BITSTREAM - case 0x0B: // ID_WVC_BITSTREAM - case 0x0C: // ID_WVX_BITSTREAM - case 0x0D: // ID_CHANNEL_INFO - unset($metablock); - break; - } - - } - if (!empty($metablock)) { - $info['wavpack']['metablocks'][] = $metablock; - } - - } - - } - - $info['audio']['encoder'] = 'WavPack v'.$info['wavpack']['blockheader']['major_version'].'.'.str_pad($info['wavpack']['blockheader']['minor_version'], 2, '0', STR_PAD_LEFT); - $info['audio']['bits_per_sample'] = $info['wavpack']['blockheader']['flags']['bytes_per_sample'] * 8; - $info['audio']['channels'] = ($info['wavpack']['blockheader']['flags']['mono'] ? 1 : 2); - - if (!empty($info['playtime_seconds'])) { - - $info['audio']['bitrate'] = (($info['avdataend'] - $info['avdataoffset']) * 8) / $info['playtime_seconds']; - - } else { - - $info['audio']['dataformat'] = 'wvc'; - - } - - return true; - } - - - public function WavPackMetablockNameLookup(&$id) { - static $WavPackMetablockNameLookup = array( - 0x00 => 'Dummy', - 0x01 => 'Encoder Info', - 0x02 => 'Decorrelation Terms', - 0x03 => 'Decorrelation Weights', - 0x04 => 'Decorrelation Samples', - 0x05 => 'Entropy Variables', - 0x06 => 'Hybrid Profile', - 0x07 => 'Shaping Weights', - 0x08 => 'Float Info', - 0x09 => 'Int32 Info', - 0x0A => 'WV Bitstream', - 0x0B => 'WVC Bitstream', - 0x0C => 'WVX Bitstream', - 0x0D => 'Channel Info', - 0x21 => 'RIFF header', - 0x22 => 'RIFF trailer', - 0x23 => 'Replay Gain', - 0x24 => 'Cuesheet', - 0x25 => 'Config Block', - 0x26 => 'MD5 Checksum', - ); - return (isset($WavPackMetablockNameLookup[$id]) ? $WavPackMetablockNameLookup[$id] : ''); - } - -} diff --git a/src/Classes/Vendor/getid3/module.graphic.bmp.php b/src/Classes/Vendor/getid3/module.graphic.bmp.php deleted file mode 100755 index 5dabd9b07..000000000 --- a/src/Classes/Vendor/getid3/module.graphic.bmp.php +++ /dev/null @@ -1,687 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.graphic.bmp.php // -// module for analyzing BMP Image files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_bmp extends getid3_handler -{ - public $ExtractPalette = false; - public $ExtractData = false; - - public function Analyze() { - $info = &$this->getid3->info; - - // shortcuts - $info['bmp']['header']['raw'] = array(); - $thisfile_bmp = &$info['bmp']; - $thisfile_bmp_header = &$thisfile_bmp['header']; - $thisfile_bmp_header_raw = &$thisfile_bmp_header['raw']; - - // BITMAPFILEHEADER [14 bytes] - http://msdn.microsoft.com/library/en-us/gdi/bitmaps_62uq.asp - // all versions - // WORD bfType; - // DWORD bfSize; - // WORD bfReserved1; - // WORD bfReserved2; - // DWORD bfOffBits; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $offset = 0; - $BMPheader = fread($this->getid3->fp, 14 + 40); - - $thisfile_bmp_header_raw['identifier'] = substr($BMPheader, $offset, 2); - $offset += 2; - - $magic = 'BM'; - if ($thisfile_bmp_header_raw['identifier'] != $magic) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($thisfile_bmp_header_raw['identifier']).'"'; - unset($info['fileformat']); - unset($info['bmp']); - return false; - } - - $thisfile_bmp_header_raw['filesize'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['reserved1'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 2)); - $offset += 2; - $thisfile_bmp_header_raw['reserved2'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 2)); - $offset += 2; - $thisfile_bmp_header_raw['data_offset'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['header_size'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - - - // check if the hardcoded-to-1 "planes" is at offset 22 or 26 - $planes22 = getid3_lib::LittleEndian2Int(substr($BMPheader, 22, 2)); - $planes26 = getid3_lib::LittleEndian2Int(substr($BMPheader, 26, 2)); - if (($planes22 == 1) && ($planes26 != 1)) { - $thisfile_bmp['type_os'] = 'OS/2'; - $thisfile_bmp['type_version'] = 1; - } elseif (($planes26 == 1) && ($planes22 != 1)) { - $thisfile_bmp['type_os'] = 'Windows'; - $thisfile_bmp['type_version'] = 1; - } elseif ($thisfile_bmp_header_raw['header_size'] == 12) { - $thisfile_bmp['type_os'] = 'OS/2'; - $thisfile_bmp['type_version'] = 1; - } elseif ($thisfile_bmp_header_raw['header_size'] == 40) { - $thisfile_bmp['type_os'] = 'Windows'; - $thisfile_bmp['type_version'] = 1; - } elseif ($thisfile_bmp_header_raw['header_size'] == 84) { - $thisfile_bmp['type_os'] = 'Windows'; - $thisfile_bmp['type_version'] = 4; - } elseif ($thisfile_bmp_header_raw['header_size'] == 100) { - $thisfile_bmp['type_os'] = 'Windows'; - $thisfile_bmp['type_version'] = 5; - } else { - $info['error'][] = 'Unknown BMP subtype (or not a BMP file)'; - unset($info['fileformat']); - unset($info['bmp']); - return false; - } - - $info['fileformat'] = 'bmp'; - $info['video']['dataformat'] = 'bmp'; - $info['video']['lossless'] = true; - $info['video']['pixel_aspect_ratio'] = (float) 1; - - if ($thisfile_bmp['type_os'] == 'OS/2') { - - // OS/2-format BMP - // http://netghost.narod.ru/gff/graphics/summary/os2bmp.htm - - // DWORD Size; /* Size of this structure in bytes */ - // DWORD Width; /* Bitmap width in pixels */ - // DWORD Height; /* Bitmap height in pixel */ - // WORD NumPlanes; /* Number of bit planes (color depth) */ - // WORD BitsPerPixel; /* Number of bits per pixel per plane */ - - $thisfile_bmp_header_raw['width'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 2)); - $offset += 2; - $thisfile_bmp_header_raw['height'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 2)); - $offset += 2; - $thisfile_bmp_header_raw['planes'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 2)); - $offset += 2; - $thisfile_bmp_header_raw['bits_per_pixel'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 2)); - $offset += 2; - - $info['video']['resolution_x'] = $thisfile_bmp_header_raw['width']; - $info['video']['resolution_y'] = $thisfile_bmp_header_raw['height']; - $info['video']['codec'] = 'BI_RGB '.$thisfile_bmp_header_raw['bits_per_pixel'].'-bit'; - $info['video']['bits_per_sample'] = $thisfile_bmp_header_raw['bits_per_pixel']; - - if ($thisfile_bmp['type_version'] >= 2) { - // DWORD Compression; /* Bitmap compression scheme */ - // DWORD ImageDataSize; /* Size of bitmap data in bytes */ - // DWORD XResolution; /* X resolution of display device */ - // DWORD YResolution; /* Y resolution of display device */ - // DWORD ColorsUsed; /* Number of color table indices used */ - // DWORD ColorsImportant; /* Number of important color indices */ - // WORD Units; /* Type of units used to measure resolution */ - // WORD Reserved; /* Pad structure to 4-byte boundary */ - // WORD Recording; /* Recording algorithm */ - // WORD Rendering; /* Halftoning algorithm used */ - // DWORD Size1; /* Reserved for halftoning algorithm use */ - // DWORD Size2; /* Reserved for halftoning algorithm use */ - // DWORD ColorEncoding; /* Color model used in bitmap */ - // DWORD Identifier; /* Reserved for application use */ - - $thisfile_bmp_header_raw['compression'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['bmp_data_size'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['resolution_h'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['resolution_v'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['colors_used'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['colors_important'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['resolution_units'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 2)); - $offset += 2; - $thisfile_bmp_header_raw['reserved1'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 2)); - $offset += 2; - $thisfile_bmp_header_raw['recording'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 2)); - $offset += 2; - $thisfile_bmp_header_raw['rendering'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 2)); - $offset += 2; - $thisfile_bmp_header_raw['size1'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['size2'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['color_encoding'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['identifier'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - - $thisfile_bmp_header['compression'] = $this->BMPcompressionOS2Lookup($thisfile_bmp_header_raw['compression']); - - $info['video']['codec'] = $thisfile_bmp_header['compression'].' '.$thisfile_bmp_header_raw['bits_per_pixel'].'-bit'; - } - - } elseif ($thisfile_bmp['type_os'] == 'Windows') { - - // Windows-format BMP - - // BITMAPINFOHEADER - [40 bytes] http://msdn.microsoft.com/library/en-us/gdi/bitmaps_1rw2.asp - // all versions - // DWORD biSize; - // LONG biWidth; - // LONG biHeight; - // WORD biPlanes; - // WORD biBitCount; - // DWORD biCompression; - // DWORD biSizeImage; - // LONG biXPelsPerMeter; - // LONG biYPelsPerMeter; - // DWORD biClrUsed; - // DWORD biClrImportant; - - // possibly integrate this section and module.audio-video.riff.php::ParseBITMAPINFOHEADER() ? - - $thisfile_bmp_header_raw['width'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4), true); - $offset += 4; - $thisfile_bmp_header_raw['height'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4), true); - $offset += 4; - $thisfile_bmp_header_raw['planes'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 2)); - $offset += 2; - $thisfile_bmp_header_raw['bits_per_pixel'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 2)); - $offset += 2; - $thisfile_bmp_header_raw['compression'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['bmp_data_size'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['resolution_h'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4), true); - $offset += 4; - $thisfile_bmp_header_raw['resolution_v'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4), true); - $offset += 4; - $thisfile_bmp_header_raw['colors_used'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['colors_important'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - - $thisfile_bmp_header['compression'] = $this->BMPcompressionWindowsLookup($thisfile_bmp_header_raw['compression']); - $info['video']['resolution_x'] = $thisfile_bmp_header_raw['width']; - $info['video']['resolution_y'] = $thisfile_bmp_header_raw['height']; - $info['video']['codec'] = $thisfile_bmp_header['compression'].' '.$thisfile_bmp_header_raw['bits_per_pixel'].'-bit'; - $info['video']['bits_per_sample'] = $thisfile_bmp_header_raw['bits_per_pixel']; - - if (($thisfile_bmp['type_version'] >= 4) || ($thisfile_bmp_header_raw['compression'] == 3)) { - // should only be v4+, but BMPs with type_version==1 and BI_BITFIELDS compression have been seen - $BMPheader .= fread($this->getid3->fp, 44); - - // BITMAPV4HEADER - [44 bytes] - http://msdn.microsoft.com/library/en-us/gdi/bitmaps_2k1e.asp - // Win95+, WinNT4.0+ - // DWORD bV4RedMask; - // DWORD bV4GreenMask; - // DWORD bV4BlueMask; - // DWORD bV4AlphaMask; - // DWORD bV4CSType; - // CIEXYZTRIPLE bV4Endpoints; - // DWORD bV4GammaRed; - // DWORD bV4GammaGreen; - // DWORD bV4GammaBlue; - $thisfile_bmp_header_raw['red_mask'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['green_mask'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['blue_mask'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['alpha_mask'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['cs_type'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['ciexyz_red'] = substr($BMPheader, $offset, 4); - $offset += 4; - $thisfile_bmp_header_raw['ciexyz_green'] = substr($BMPheader, $offset, 4); - $offset += 4; - $thisfile_bmp_header_raw['ciexyz_blue'] = substr($BMPheader, $offset, 4); - $offset += 4; - $thisfile_bmp_header_raw['gamma_red'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['gamma_green'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['gamma_blue'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - - $thisfile_bmp_header['ciexyz_red'] = getid3_lib::FixedPoint2_30(strrev($thisfile_bmp_header_raw['ciexyz_red'])); - $thisfile_bmp_header['ciexyz_green'] = getid3_lib::FixedPoint2_30(strrev($thisfile_bmp_header_raw['ciexyz_green'])); - $thisfile_bmp_header['ciexyz_blue'] = getid3_lib::FixedPoint2_30(strrev($thisfile_bmp_header_raw['ciexyz_blue'])); - } - - if ($thisfile_bmp['type_version'] >= 5) { - $BMPheader .= fread($this->getid3->fp, 16); - - // BITMAPV5HEADER - [16 bytes] - http://msdn.microsoft.com/library/en-us/gdi/bitmaps_7c36.asp - // Win98+, Win2000+ - // DWORD bV5Intent; - // DWORD bV5ProfileData; - // DWORD bV5ProfileSize; - // DWORD bV5Reserved; - $thisfile_bmp_header_raw['intent'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['profile_data_offset'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['profile_data_size'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - $thisfile_bmp_header_raw['reserved3'] = getid3_lib::LittleEndian2Int(substr($BMPheader, $offset, 4)); - $offset += 4; - } - - } else { - - $info['error'][] = 'Unknown BMP format in header.'; - return false; - - } - - - if ($this->ExtractPalette || $this->ExtractData) { - $PaletteEntries = 0; - if ($thisfile_bmp_header_raw['bits_per_pixel'] < 16) { - $PaletteEntries = pow(2, $thisfile_bmp_header_raw['bits_per_pixel']); - } elseif (isset($thisfile_bmp_header_raw['colors_used']) && ($thisfile_bmp_header_raw['colors_used'] > 0) && ($thisfile_bmp_header_raw['colors_used'] <= 256)) { - $PaletteEntries = $thisfile_bmp_header_raw['colors_used']; - } - if ($PaletteEntries > 0) { - $BMPpalette = fread($this->getid3->fp, 4 * $PaletteEntries); - $paletteoffset = 0; - for ($i = 0; $i < $PaletteEntries; $i++) { - // RGBQUAD - http://msdn.microsoft.com/library/en-us/gdi/bitmaps_5f8y.asp - // BYTE rgbBlue; - // BYTE rgbGreen; - // BYTE rgbRed; - // BYTE rgbReserved; - $blue = getid3_lib::LittleEndian2Int(substr($BMPpalette, $paletteoffset++, 1)); - $green = getid3_lib::LittleEndian2Int(substr($BMPpalette, $paletteoffset++, 1)); - $red = getid3_lib::LittleEndian2Int(substr($BMPpalette, $paletteoffset++, 1)); - if (($thisfile_bmp['type_os'] == 'OS/2') && ($thisfile_bmp['type_version'] == 1)) { - // no padding byte - } else { - $paletteoffset++; // padding byte - } - $thisfile_bmp['palette'][$i] = (($red << 16) | ($green << 8) | $blue); - } - } - } - - if ($this->ExtractData) { - fseek($this->getid3->fp, $thisfile_bmp_header_raw['data_offset'], SEEK_SET); - $RowByteLength = ceil(($thisfile_bmp_header_raw['width'] * ($thisfile_bmp_header_raw['bits_per_pixel'] / 8)) / 4) * 4; // round up to nearest DWORD boundry - $BMPpixelData = fread($this->getid3->fp, $thisfile_bmp_header_raw['height'] * $RowByteLength); - $pixeldataoffset = 0; - $thisfile_bmp_header_raw['compression'] = (isset($thisfile_bmp_header_raw['compression']) ? $thisfile_bmp_header_raw['compression'] : ''); - switch ($thisfile_bmp_header_raw['compression']) { - - case 0: // BI_RGB - switch ($thisfile_bmp_header_raw['bits_per_pixel']) { - case 1: - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col = $col) { - $paletteindexbyte = ord($BMPpixelData{$pixeldataoffset++}); - for ($i = 7; $i >= 0; $i--) { - $paletteindex = ($paletteindexbyte & (0x01 << $i)) >> $i; - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; - $col++; - } - } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; - } - } - break; - - case 4: - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col = $col) { - $paletteindexbyte = ord($BMPpixelData{$pixeldataoffset++}); - for ($i = 1; $i >= 0; $i--) { - $paletteindex = ($paletteindexbyte & (0x0F << (4 * $i))) >> (4 * $i); - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; - $col++; - } - } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; - } - } - break; - - case 8: - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { - $paletteindex = ord($BMPpixelData{$pixeldataoffset++}); - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; - } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; - } - } - break; - - case 24: - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { - $thisfile_bmp['data'][$row][$col] = (ord($BMPpixelData{$pixeldataoffset+2}) << 16) | (ord($BMPpixelData{$pixeldataoffset+1}) << 8) | ord($BMPpixelData{$pixeldataoffset}); - $pixeldataoffset += 3; - } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; - } - } - break; - - case 32: - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { - $thisfile_bmp['data'][$row][$col] = (ord($BMPpixelData{$pixeldataoffset+3}) << 24) | (ord($BMPpixelData{$pixeldataoffset+2}) << 16) | (ord($BMPpixelData{$pixeldataoffset+1}) << 8) | ord($BMPpixelData{$pixeldataoffset}); - $pixeldataoffset += 4; - } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; - } - } - break; - - case 16: - // ? - break; - - default: - $info['error'][] = 'Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'; - break; - } - break; - - - case 1: // BI_RLE8 - http://msdn.microsoft.com/library/en-us/gdi/bitmaps_6x0u.asp - switch ($thisfile_bmp_header_raw['bits_per_pixel']) { - case 8: - $pixelcounter = 0; - while ($pixeldataoffset < strlen($BMPpixelData)) { - $firstbyte = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $secondbyte = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - if ($firstbyte == 0) { - - // escaped/absolute mode - the first byte of the pair can be set to zero to - // indicate an escape character that denotes the end of a line, the end of - // a bitmap, or a delta, depending on the value of the second byte. - switch ($secondbyte) { - case 0: - // end of line - // no need for special processing, just ignore - break; - - case 1: - // end of bitmap - $pixeldataoffset = strlen($BMPpixelData); // force to exit loop just in case - break; - - case 2: - // delta - The 2 bytes following the escape contain unsigned values - // indicating the horizontal and vertical offsets of the next pixel - // from the current position. - $colincrement = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $rowincrement = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $col = ($pixelcounter % $thisfile_bmp_header_raw['width']) + $colincrement; - $row = ($thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width'])) - $rowincrement; - $pixelcounter = ($row * $thisfile_bmp_header_raw['width']) + $col; - break; - - default: - // In absolute mode, the first byte is zero and the second byte is a - // value in the range 03H through FFH. The second byte represents the - // number of bytes that follow, each of which contains the color index - // of a single pixel. Each run must be aligned on a word boundary. - for ($i = 0; $i < $secondbyte; $i++) { - $paletteindex = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $col = $pixelcounter % $thisfile_bmp_header_raw['width']; - $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; - $pixelcounter++; - } - while (($pixeldataoffset % 2) != 0) { - // Each run must be aligned on a word boundary. - $pixeldataoffset++; - } - break; - } - - } else { - - // encoded mode - the first byte specifies the number of consecutive pixels - // to be drawn using the color index contained in the second byte. - for ($i = 0; $i < $firstbyte; $i++) { - $col = $pixelcounter % $thisfile_bmp_header_raw['width']; - $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$secondbyte]; - $pixelcounter++; - } - - } - } - break; - - default: - $info['error'][] = 'Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'; - break; - } - break; - - - - case 2: // BI_RLE4 - http://msdn.microsoft.com/library/en-us/gdi/bitmaps_6x0u.asp - switch ($thisfile_bmp_header_raw['bits_per_pixel']) { - case 4: - $pixelcounter = 0; - while ($pixeldataoffset < strlen($BMPpixelData)) { - $firstbyte = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $secondbyte = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - if ($firstbyte == 0) { - - // escaped/absolute mode - the first byte of the pair can be set to zero to - // indicate an escape character that denotes the end of a line, the end of - // a bitmap, or a delta, depending on the value of the second byte. - switch ($secondbyte) { - case 0: - // end of line - // no need for special processing, just ignore - break; - - case 1: - // end of bitmap - $pixeldataoffset = strlen($BMPpixelData); // force to exit loop just in case - break; - - case 2: - // delta - The 2 bytes following the escape contain unsigned values - // indicating the horizontal and vertical offsets of the next pixel - // from the current position. - $colincrement = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $rowincrement = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $col = ($pixelcounter % $thisfile_bmp_header_raw['width']) + $colincrement; - $row = ($thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width'])) - $rowincrement; - $pixelcounter = ($row * $thisfile_bmp_header_raw['width']) + $col; - break; - - default: - // In absolute mode, the first byte is zero. The second byte contains the number - // of color indexes that follow. Subsequent bytes contain color indexes in their - // high- and low-order 4 bits, one color index for each pixel. In absolute mode, - // each run must be aligned on a word boundary. - unset($paletteindexes); - for ($i = 0; $i < ceil($secondbyte / 2); $i++) { - $paletteindexbyte = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset++, 1)); - $paletteindexes[] = ($paletteindexbyte & 0xF0) >> 4; - $paletteindexes[] = ($paletteindexbyte & 0x0F); - } - while (($pixeldataoffset % 2) != 0) { - // Each run must be aligned on a word boundary. - $pixeldataoffset++; - } - - foreach ($paletteindexes as $paletteindex) { - $col = $pixelcounter % $thisfile_bmp_header_raw['width']; - $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindex]; - $pixelcounter++; - } - break; - } - - } else { - - // encoded mode - the first byte of the pair contains the number of pixels to be - // drawn using the color indexes in the second byte. The second byte contains two - // color indexes, one in its high-order 4 bits and one in its low-order 4 bits. - // The first of the pixels is drawn using the color specified by the high-order - // 4 bits, the second is drawn using the color in the low-order 4 bits, the third - // is drawn using the color in the high-order 4 bits, and so on, until all the - // pixels specified by the first byte have been drawn. - $paletteindexes[0] = ($secondbyte & 0xF0) >> 4; - $paletteindexes[1] = ($secondbyte & 0x0F); - for ($i = 0; $i < $firstbyte; $i++) { - $col = $pixelcounter % $thisfile_bmp_header_raw['width']; - $row = $thisfile_bmp_header_raw['height'] - 1 - (($pixelcounter - $col) / $thisfile_bmp_header_raw['width']); - $thisfile_bmp['data'][$row][$col] = $thisfile_bmp['palette'][$paletteindexes[($i % 2)]]; - $pixelcounter++; - } - - } - } - break; - - default: - $info['error'][] = 'Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'; - break; - } - break; - - - case 3: // BI_BITFIELDS - switch ($thisfile_bmp_header_raw['bits_per_pixel']) { - case 16: - case 32: - $redshift = 0; - $greenshift = 0; - $blueshift = 0; - while ((($thisfile_bmp_header_raw['red_mask'] >> $redshift) & 0x01) == 0) { - $redshift++; - } - while ((($thisfile_bmp_header_raw['green_mask'] >> $greenshift) & 0x01) == 0) { - $greenshift++; - } - while ((($thisfile_bmp_header_raw['blue_mask'] >> $blueshift) & 0x01) == 0) { - $blueshift++; - } - for ($row = ($thisfile_bmp_header_raw['height'] - 1); $row >= 0; $row--) { - for ($col = 0; $col < $thisfile_bmp_header_raw['width']; $col++) { - $pixelvalue = getid3_lib::LittleEndian2Int(substr($BMPpixelData, $pixeldataoffset, $thisfile_bmp_header_raw['bits_per_pixel'] / 8)); - $pixeldataoffset += $thisfile_bmp_header_raw['bits_per_pixel'] / 8; - - $red = intval(round(((($pixelvalue & $thisfile_bmp_header_raw['red_mask']) >> $redshift) / ($thisfile_bmp_header_raw['red_mask'] >> $redshift)) * 255)); - $green = intval(round(((($pixelvalue & $thisfile_bmp_header_raw['green_mask']) >> $greenshift) / ($thisfile_bmp_header_raw['green_mask'] >> $greenshift)) * 255)); - $blue = intval(round(((($pixelvalue & $thisfile_bmp_header_raw['blue_mask']) >> $blueshift) / ($thisfile_bmp_header_raw['blue_mask'] >> $blueshift)) * 255)); - $thisfile_bmp['data'][$row][$col] = (($red << 16) | ($green << 8) | ($blue)); - } - while (($pixeldataoffset % 4) != 0) { - // lines are padded to nearest DWORD - $pixeldataoffset++; - } - } - break; - - default: - $info['error'][] = 'Unknown bits-per-pixel value ('.$thisfile_bmp_header_raw['bits_per_pixel'].') - cannot read pixel data'; - break; - } - break; - - - default: // unhandled compression type - $info['error'][] = 'Unknown/unhandled compression type value ('.$thisfile_bmp_header_raw['compression'].') - cannot decompress pixel data'; - break; - } - } - - return true; - } - - - public function PlotBMP(&$BMPinfo) { - $starttime = time(); - if (!isset($BMPinfo['bmp']['data']) || !is_array($BMPinfo['bmp']['data'])) { - echo 'ERROR: no pixel data
    '; - return false; - } - set_time_limit(intval(round($BMPinfo['resolution_x'] * $BMPinfo['resolution_y'] / 10000))); - if ($im = ImageCreateTrueColor($BMPinfo['resolution_x'], $BMPinfo['resolution_y'])) { - for ($row = 0; $row < $BMPinfo['resolution_y']; $row++) { - for ($col = 0; $col < $BMPinfo['resolution_x']; $col++) { - if (isset($BMPinfo['bmp']['data'][$row][$col])) { - $red = ($BMPinfo['bmp']['data'][$row][$col] & 0x00FF0000) >> 16; - $green = ($BMPinfo['bmp']['data'][$row][$col] & 0x0000FF00) >> 8; - $blue = ($BMPinfo['bmp']['data'][$row][$col] & 0x000000FF); - $pixelcolor = ImageColorAllocate($im, $red, $green, $blue); - ImageSetPixel($im, $col, $row, $pixelcolor); - } else { - //echo 'ERROR: no data for pixel '.$row.' x '.$col.'
    '; - //return false; - } - } - } - if (headers_sent()) { - echo 'plotted '.($BMPinfo['resolution_x'] * $BMPinfo['resolution_y']).' pixels in '.(time() - $starttime).' seconds
    '; - ImageDestroy($im); - exit; - } else { - header('Content-type: image/png'); - ImagePNG($im); - ImageDestroy($im); - return true; - } - } - return false; - } - - public function BMPcompressionWindowsLookup($compressionid) { - static $BMPcompressionWindowsLookup = array( - 0 => 'BI_RGB', - 1 => 'BI_RLE8', - 2 => 'BI_RLE4', - 3 => 'BI_BITFIELDS', - 4 => 'BI_JPEG', - 5 => 'BI_PNG' - ); - return (isset($BMPcompressionWindowsLookup[$compressionid]) ? $BMPcompressionWindowsLookup[$compressionid] : 'invalid'); - } - - public function BMPcompressionOS2Lookup($compressionid) { - static $BMPcompressionOS2Lookup = array( - 0 => 'BI_RGB', - 1 => 'BI_RLE8', - 2 => 'BI_RLE4', - 3 => 'Huffman 1D', - 4 => 'BI_RLE24', - ); - return (isset($BMPcompressionOS2Lookup[$compressionid]) ? $BMPcompressionOS2Lookup[$compressionid] : 'invalid'); - } - -} diff --git a/src/Classes/Vendor/getid3/module.graphic.efax.php b/src/Classes/Vendor/getid3/module.graphic.efax.php deleted file mode 100755 index dfedf6e6f..000000000 --- a/src/Classes/Vendor/getid3/module.graphic.efax.php +++ /dev/null @@ -1,50 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.archive.efax.php // -// module for analyzing eFax files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_efax extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $efaxheader = fread($this->getid3->fp, 1024); - - $info['efax']['header']['magic'] = substr($efaxheader, 0, 2); - if ($info['efax']['header']['magic'] != "\xDC\xFE") { - $info['error'][] = 'Invalid eFax byte order identifier (expecting DC FE, found '.getid3_lib::PrintHexBytes($info['efax']['header']['magic']).') at offset '.$info['avdataoffset']; - return false; - } - $info['fileformat'] = 'efax'; - - $info['efax']['header']['filesize'] = getid3_lib::LittleEndian2Int(substr($efaxheader, 2, 4)); - if ($info['efax']['header']['filesize'] != $info['filesize']) { - $info['error'][] = 'Probable '.(($info['efax']['header']['filesize'] > $info['filesize']) ? 'truncated' : 'corrupt').' file, expecting '.$info['efax']['header']['filesize'].' bytes, found '.$info['filesize'].' bytes'; - } - $info['efax']['header']['software1'] = rtrim(substr($efaxheader, 26, 32), "\x00"); - $info['efax']['header']['software2'] = rtrim(substr($efaxheader, 58, 32), "\x00"); - $info['efax']['header']['software3'] = rtrim(substr($efaxheader, 90, 32), "\x00"); - - $info['efax']['header']['pages'] = getid3_lib::LittleEndian2Int(substr($efaxheader, 198, 2)); - $info['efax']['header']['data_bytes'] = getid3_lib::LittleEndian2Int(substr($efaxheader, 202, 4)); - -$info['error'][] = 'eFax parsing not enabled in this version of getID3() ['.$this->getid3->version().']'; -return false; - - return true; - } - -} diff --git a/src/Classes/Vendor/getid3/module.graphic.gif.php b/src/Classes/Vendor/getid3/module.graphic.gif.php deleted file mode 100755 index cd8457e33..000000000 --- a/src/Classes/Vendor/getid3/module.graphic.gif.php +++ /dev/null @@ -1,181 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.graphic.gif.php // -// module for analyzing GIF Image files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_gif extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'gif'; - $info['video']['dataformat'] = 'gif'; - $info['video']['lossless'] = true; - $info['video']['pixel_aspect_ratio'] = (float) 1; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $GIFheader = fread($this->getid3->fp, 13); - $offset = 0; - - $info['gif']['header']['raw']['identifier'] = substr($GIFheader, $offset, 3); - $offset += 3; - - $magic = 'GIF'; - if ($info['gif']['header']['raw']['identifier'] != $magic) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes($info['gif']['header']['raw']['identifier']).'"'; - unset($info['fileformat']); - unset($info['gif']); - return false; - } - - $info['gif']['header']['raw']['version'] = substr($GIFheader, $offset, 3); - $offset += 3; - $info['gif']['header']['raw']['width'] = getid3_lib::LittleEndian2Int(substr($GIFheader, $offset, 2)); - $offset += 2; - $info['gif']['header']['raw']['height'] = getid3_lib::LittleEndian2Int(substr($GIFheader, $offset, 2)); - $offset += 2; - $info['gif']['header']['raw']['flags'] = getid3_lib::LittleEndian2Int(substr($GIFheader, $offset, 1)); - $offset += 1; - $info['gif']['header']['raw']['bg_color_index'] = getid3_lib::LittleEndian2Int(substr($GIFheader, $offset, 1)); - $offset += 1; - $info['gif']['header']['raw']['aspect_ratio'] = getid3_lib::LittleEndian2Int(substr($GIFheader, $offset, 1)); - $offset += 1; - - $info['video']['resolution_x'] = $info['gif']['header']['raw']['width']; - $info['video']['resolution_y'] = $info['gif']['header']['raw']['height']; - $info['gif']['version'] = $info['gif']['header']['raw']['version']; - $info['gif']['header']['flags']['global_color_table'] = (bool) ($info['gif']['header']['raw']['flags'] & 0x80); - if ($info['gif']['header']['raw']['flags'] & 0x80) { - // Number of bits per primary color available to the original image, minus 1 - $info['gif']['header']['bits_per_pixel'] = 3 * ((($info['gif']['header']['raw']['flags'] & 0x70) >> 4) + 1); - } else { - $info['gif']['header']['bits_per_pixel'] = 0; - } - $info['gif']['header']['flags']['global_color_sorted'] = (bool) ($info['gif']['header']['raw']['flags'] & 0x40); - if ($info['gif']['header']['flags']['global_color_table']) { - // the number of bytes contained in the Global Color Table. To determine that - // actual size of the color table, raise 2 to [the value of the field + 1] - $info['gif']['header']['global_color_size'] = pow(2, ($info['gif']['header']['raw']['flags'] & 0x07) + 1); - $info['video']['bits_per_sample'] = ($info['gif']['header']['raw']['flags'] & 0x07) + 1; - } else { - $info['gif']['header']['global_color_size'] = 0; - } - if ($info['gif']['header']['raw']['aspect_ratio'] != 0) { - // Aspect Ratio = (Pixel Aspect Ratio + 15) / 64 - $info['gif']['header']['aspect_ratio'] = ($info['gif']['header']['raw']['aspect_ratio'] + 15) / 64; - } - -// if ($info['gif']['header']['flags']['global_color_table']) { -// $GIFcolorTable = fread($this->getid3->fp, 3 * $info['gif']['header']['global_color_size']); -// $offset = 0; -// for ($i = 0; $i < $info['gif']['header']['global_color_size']; $i++) { -// $red = getid3_lib::LittleEndian2Int(substr($GIFcolorTable, $offset++, 1)); -// $green = getid3_lib::LittleEndian2Int(substr($GIFcolorTable, $offset++, 1)); -// $blue = getid3_lib::LittleEndian2Int(substr($GIFcolorTable, $offset++, 1)); -// $info['gif']['global_color_table'][$i] = (($red << 16) | ($green << 8) | ($blue)); -// } -// } -// -// // Image Descriptor -// while (!feof($this->getid3->fp)) { -// $NextBlockTest = fread($this->getid3->fp, 1); -// switch ($NextBlockTest) { -// -// case ',': // ',' - Image separator character -// -// $ImageDescriptorData = $NextBlockTest.fread($this->getid3->fp, 9); -// $ImageDescriptor = array(); -// $ImageDescriptor['image_left'] = getid3_lib::LittleEndian2Int(substr($ImageDescriptorData, 1, 2)); -// $ImageDescriptor['image_top'] = getid3_lib::LittleEndian2Int(substr($ImageDescriptorData, 3, 2)); -// $ImageDescriptor['image_width'] = getid3_lib::LittleEndian2Int(substr($ImageDescriptorData, 5, 2)); -// $ImageDescriptor['image_height'] = getid3_lib::LittleEndian2Int(substr($ImageDescriptorData, 7, 2)); -// $ImageDescriptor['flags_raw'] = getid3_lib::LittleEndian2Int(substr($ImageDescriptorData, 9, 1)); -// $ImageDescriptor['flags']['use_local_color_map'] = (bool) ($ImageDescriptor['flags_raw'] & 0x80); -// $ImageDescriptor['flags']['image_interlaced'] = (bool) ($ImageDescriptor['flags_raw'] & 0x40); -// $info['gif']['image_descriptor'][] = $ImageDescriptor; -// -// if ($ImageDescriptor['flags']['use_local_color_map']) { -// -// $info['warning'][] = 'This version of getID3() cannot parse local color maps for GIFs'; -// return true; -// -// } -//echo 'Start of raster data: '.ftell($this->getid3->fp).'
    '; -// $RasterData = array(); -// $RasterData['code_size'] = getid3_lib::LittleEndian2Int(fread($this->getid3->fp, 1)); -// $RasterData['block_byte_count'] = getid3_lib::LittleEndian2Int(fread($this->getid3->fp, 1)); -// $info['gif']['raster_data'][count($info['gif']['image_descriptor']) - 1] = $RasterData; -// -// $CurrentCodeSize = $RasterData['code_size'] + 1; -// for ($i = 0; $i < pow(2, $RasterData['code_size']); $i++) { -// $DefaultDataLookupTable[$i] = chr($i); -// } -// $DefaultDataLookupTable[pow(2, $RasterData['code_size']) + 0] = ''; // Clear Code -// $DefaultDataLookupTable[pow(2, $RasterData['code_size']) + 1] = ''; // End Of Image Code -// -// -// $NextValue = $this->GetLSBits($CurrentCodeSize); -// echo 'Clear Code: '.$NextValue.'
    '; -// -// $NextValue = $this->GetLSBits($CurrentCodeSize); -// echo 'First Color: '.$NextValue.'
    '; -// -// $Prefix = $NextValue; -//$i = 0; -// while ($i++ < 20) { -// $NextValue = $this->GetLSBits($CurrentCodeSize); -// echo $NextValue.'
    '; -// } -//return true; -// break; -// -// case '!': -// // GIF Extension Block -// $ExtensionBlockData = $NextBlockTest.fread($this->getid3->fp, 2); -// $ExtensionBlock = array(); -// $ExtensionBlock['function_code'] = getid3_lib::LittleEndian2Int(substr($ExtensionBlockData, 1, 1)); -// $ExtensionBlock['byte_length'] = getid3_lib::LittleEndian2Int(substr($ExtensionBlockData, 2, 1)); -// $ExtensionBlock['data'] = fread($this->getid3->fp, $ExtensionBlock['byte_length']); -// $info['gif']['extension_blocks'][] = $ExtensionBlock; -// break; -// -// case ';': -// $info['gif']['terminator_offset'] = ftell($this->getid3->fp) - 1; -// // GIF Terminator -// break; -// -// default: -// break; -// -// -// } -// } - - return true; - } - - - public function GetLSBits($bits) { - static $bitbuffer = ''; - while (strlen($bitbuffer) < $bits) { - $bitbuffer = str_pad(decbin(ord(fread($this->getid3->fp, 1))), 8, '0', STR_PAD_LEFT).$bitbuffer; - } - $value = bindec(substr($bitbuffer, 0 - $bits)); - $bitbuffer = substr($bitbuffer, 0, 0 - $bits); - - return $value; - } - -} diff --git a/src/Classes/Vendor/getid3/module.graphic.jpg.php b/src/Classes/Vendor/getid3/module.graphic.jpg.php deleted file mode 100755 index 3db654382..000000000 --- a/src/Classes/Vendor/getid3/module.graphic.jpg.php +++ /dev/null @@ -1,344 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.graphic.jpg.php // -// module for analyzing JPEG Image files // -// dependencies: PHP compiled with --enable-exif (optional) // -// module.tag.xmp.php (optional) // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_jpg extends getid3_handler -{ - - - public function Analyze() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'jpg'; - $info['video']['dataformat'] = 'jpg'; - $info['video']['lossless'] = false; - $info['video']['bits_per_sample'] = 24; - $info['video']['pixel_aspect_ratio'] = (float) 1; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - - $imageinfo = array(); - //list($width, $height, $type) = getid3_lib::GetDataImageSize(fread($this->getid3->fp, $info['filesize']), $imageinfo); - list($width, $height, $type) = getimagesize($info['filenamepath'], $imageinfo); // http://www.getid3.org/phpBB3/viewtopic.php?t=1474 - - - if (isset($imageinfo['APP13'])) { - // http://php.net/iptcparse - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/IPTC.html - $iptc_parsed = iptcparse($imageinfo['APP13']); - if (is_array($iptc_parsed)) { - foreach ($iptc_parsed as $iptc_key_raw => $iptc_values) { - list($iptc_record, $iptc_tagkey) = explode('#', $iptc_key_raw); - $iptc_tagkey = intval(ltrim($iptc_tagkey, '0')); - foreach ($iptc_values as $key => $value) { - $IPTCrecordName = $this->IPTCrecordName($iptc_record); - $IPTCrecordTagName = $this->IPTCrecordTagName($iptc_record, $iptc_tagkey); - if (isset($info['iptc'][$IPTCrecordName][$IPTCrecordTagName])) { - $info['iptc'][$IPTCrecordName][$IPTCrecordTagName][] = $value; - } else { - $info['iptc'][$IPTCrecordName][$IPTCrecordTagName] = array($value); - } - } - } - } - } - - $returnOK = false; - switch ($type) { - case IMG_JPG: - $info['video']['resolution_x'] = $width; - $info['video']['resolution_y'] = $height; - - if (isset($imageinfo['APP1'])) { - if (function_exists('exif_read_data')) { - if (substr($imageinfo['APP1'], 0, 4) == 'Exif') { -//$info['warning'][] = 'known issue: https://bugs.php.net/bug.php?id=62523'; -//return false; - $info['jpg']['exif'] = exif_read_data($info['filenamepath'], null, true, false); - } else { - $info['warning'][] = 'exif_read_data() cannot parse non-EXIF data in APP1 (expected "Exif", found "'.substr($imageinfo['APP1'], 0, 4).'")'; - } - } else { - $info['warning'][] = 'EXIF parsing only available when '.(GETID3_OS_ISWINDOWS ? 'php_exif.dll enabled' : 'compiled with --enable-exif'); - } - } - $returnOK = true; - break; - - default: - break; - } - - - $cast_as_appropriate_keys = array('EXIF', 'IFD0', 'THUMBNAIL'); - foreach ($cast_as_appropriate_keys as $exif_key) { - if (isset($info['jpg']['exif'][$exif_key])) { - foreach ($info['jpg']['exif'][$exif_key] as $key => $value) { - $info['jpg']['exif'][$exif_key][$key] = $this->CastAsAppropriate($value); - } - } - } - - - if (isset($info['jpg']['exif']['GPS'])) { - - if (isset($info['jpg']['exif']['GPS']['GPSVersion'])) { - for ($i = 0; $i < 4; $i++) { - $version_subparts[$i] = ord(substr($info['jpg']['exif']['GPS']['GPSVersion'], $i, 1)); - } - $info['jpg']['exif']['GPS']['computed']['version'] = 'v'.implode('.', $version_subparts); - } - - if (isset($info['jpg']['exif']['GPS']['GPSDateStamp'])) { - $explodedGPSDateStamp = explode(':', $info['jpg']['exif']['GPS']['GPSDateStamp']); - $computed_time[5] = (isset($explodedGPSDateStamp[0]) ? $explodedGPSDateStamp[0] : ''); - $computed_time[3] = (isset($explodedGPSDateStamp[1]) ? $explodedGPSDateStamp[1] : ''); - $computed_time[4] = (isset($explodedGPSDateStamp[2]) ? $explodedGPSDateStamp[2] : ''); - - if (function_exists('date_default_timezone_set')) { - date_default_timezone_set('UTC'); - } else { - ini_set('date.timezone', 'UTC'); - } - - $computed_time = array(0=>0, 1=>0, 2=>0, 3=>0, 4=>0, 5=>0); - if (isset($info['jpg']['exif']['GPS']['GPSTimeStamp']) && is_array($info['jpg']['exif']['GPS']['GPSTimeStamp'])) { - foreach ($info['jpg']['exif']['GPS']['GPSTimeStamp'] as $key => $value) { - $computed_time[$key] = getid3_lib::DecimalizeFraction($value); - } - } - $info['jpg']['exif']['GPS']['computed']['timestamp'] = mktime($computed_time[0], $computed_time[1], $computed_time[2], $computed_time[3], $computed_time[4], $computed_time[5]); - } - - if (isset($info['jpg']['exif']['GPS']['GPSLatitude']) && is_array($info['jpg']['exif']['GPS']['GPSLatitude'])) { - $direction_multiplier = ((isset($info['jpg']['exif']['GPS']['GPSLatitudeRef']) && ($info['jpg']['exif']['GPS']['GPSLatitudeRef'] == 'S')) ? -1 : 1); - foreach ($info['jpg']['exif']['GPS']['GPSLatitude'] as $key => $value) { - $computed_latitude[$key] = getid3_lib::DecimalizeFraction($value); - } - $info['jpg']['exif']['GPS']['computed']['latitude'] = $direction_multiplier * ($computed_latitude[0] + ($computed_latitude[1] / 60) + ($computed_latitude[2] / 3600)); - } - - if (isset($info['jpg']['exif']['GPS']['GPSLongitude']) && is_array($info['jpg']['exif']['GPS']['GPSLongitude'])) { - $direction_multiplier = ((isset($info['jpg']['exif']['GPS']['GPSLongitudeRef']) && ($info['jpg']['exif']['GPS']['GPSLongitudeRef'] == 'W')) ? -1 : 1); - foreach ($info['jpg']['exif']['GPS']['GPSLongitude'] as $key => $value) { - $computed_longitude[$key] = getid3_lib::DecimalizeFraction($value); - } - $info['jpg']['exif']['GPS']['computed']['longitude'] = $direction_multiplier * ($computed_longitude[0] + ($computed_longitude[1] / 60) + ($computed_longitude[2] / 3600)); - } - if (isset($info['jpg']['exif']['GPS']['GPSAltitudeRef'])) { - $info['jpg']['exif']['GPS']['GPSAltitudeRef'] = ord($info['jpg']['exif']['GPS']['GPSAltitudeRef']); // 0 = above sea level; 1 = below sea level - } - if (isset($info['jpg']['exif']['GPS']['GPSAltitude'])) { - $direction_multiplier = (!empty($info['jpg']['exif']['GPS']['GPSAltitudeRef']) ? -1 : 1); // 0 = above sea level; 1 = below sea level - $info['jpg']['exif']['GPS']['computed']['altitude'] = $direction_multiplier * getid3_lib::DecimalizeFraction($info['jpg']['exif']['GPS']['GPSAltitude']); - } - - } - - - if (getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.xmp.php', __FILE__, false)) { - if (isset($info['filenamepath'])) { - $image_xmp = new Image_XMP($info['filenamepath']); - $xmp_raw = $image_xmp->getAllTags(); - foreach ($xmp_raw as $key => $value) { - if (strpos($key, ':')) { - list($subsection, $tagname) = explode(':', $key); - $info['xmp'][$subsection][$tagname] = $this->CastAsAppropriate($value); - } else { - $info['warning'][] = 'XMP: expecting ":", found "'.$key.'"'; - } - } - } - } - - if (!$returnOK) { - unset($info['fileformat']); - return false; - } - return true; - } - - - public function CastAsAppropriate($value) { - if (is_array($value)) { - return $value; - } elseif (preg_match('#^[0-9]+/[0-9]+$#', $value)) { - return getid3_lib::DecimalizeFraction($value); - } elseif (preg_match('#^[0-9]+$#', $value)) { - return getid3_lib::CastAsInt($value); - } elseif (preg_match('#^[0-9\.]+$#', $value)) { - return (float) $value; - } - return $value; - } - - - public function IPTCrecordName($iptc_record) { - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/IPTC.html - static $IPTCrecordName = array(); - if (empty($IPTCrecordName)) { - $IPTCrecordName = array( - 1 => 'IPTCEnvelope', - 2 => 'IPTCApplication', - 3 => 'IPTCNewsPhoto', - 7 => 'IPTCPreObjectData', - 8 => 'IPTCObjectData', - 9 => 'IPTCPostObjectData', - ); - } - return (isset($IPTCrecordName[$iptc_record]) ? $IPTCrecordName[$iptc_record] : ''); - } - - - public function IPTCrecordTagName($iptc_record, $iptc_tagkey) { - // http://www.sno.phy.queensu.ca/~phil/exiftool/TagNames/IPTC.html - static $IPTCrecordTagName = array(); - if (empty($IPTCrecordTagName)) { - $IPTCrecordTagName = array( - 1 => array( // IPTC EnvelopeRecord Tags - 0 => 'EnvelopeRecordVersion', - 5 => 'Destination', - 20 => 'FileFormat', - 22 => 'FileVersion', - 30 => 'ServiceIdentifier', - 40 => 'EnvelopeNumber', - 50 => 'ProductID', - 60 => 'EnvelopePriority', - 70 => 'DateSent', - 80 => 'TimeSent', - 90 => 'CodedCharacterSet', - 100 => 'UniqueObjectName', - 120 => 'ARMIdentifier', - 122 => 'ARMVersion', - ), - 2 => array( // IPTC ApplicationRecord Tags - 0 => 'ApplicationRecordVersion', - 3 => 'ObjectTypeReference', - 4 => 'ObjectAttributeReference', - 5 => 'ObjectName', - 7 => 'EditStatus', - 8 => 'EditorialUpdate', - 10 => 'Urgency', - 12 => 'SubjectReference', - 15 => 'Category', - 20 => 'SupplementalCategories', - 22 => 'FixtureIdentifier', - 25 => 'Keywords', - 26 => 'ContentLocationCode', - 27 => 'ContentLocationName', - 30 => 'ReleaseDate', - 35 => 'ReleaseTime', - 37 => 'ExpirationDate', - 38 => 'ExpirationTime', - 40 => 'SpecialInstructions', - 42 => 'ActionAdvised', - 45 => 'ReferenceService', - 47 => 'ReferenceDate', - 50 => 'ReferenceNumber', - 55 => 'DateCreated', - 60 => 'TimeCreated', - 62 => 'DigitalCreationDate', - 63 => 'DigitalCreationTime', - 65 => 'OriginatingProgram', - 70 => 'ProgramVersion', - 75 => 'ObjectCycle', - 80 => 'By-line', - 85 => 'By-lineTitle', - 90 => 'City', - 92 => 'Sub-location', - 95 => 'Province-State', - 100 => 'Country-PrimaryLocationCode', - 101 => 'Country-PrimaryLocationName', - 103 => 'OriginalTransmissionReference', - 105 => 'Headline', - 110 => 'Credit', - 115 => 'Source', - 116 => 'CopyrightNotice', - 118 => 'Contact', - 120 => 'Caption-Abstract', - 121 => 'LocalCaption', - 122 => 'Writer-Editor', - 125 => 'RasterizedCaption', - 130 => 'ImageType', - 131 => 'ImageOrientation', - 135 => 'LanguageIdentifier', - 150 => 'AudioType', - 151 => 'AudioSamplingRate', - 152 => 'AudioSamplingResolution', - 153 => 'AudioDuration', - 154 => 'AudioOutcue', - 184 => 'JobID', - 185 => 'MasterDocumentID', - 186 => 'ShortDocumentID', - 187 => 'UniqueDocumentID', - 188 => 'OwnerID', - 200 => 'ObjectPreviewFileFormat', - 201 => 'ObjectPreviewFileVersion', - 202 => 'ObjectPreviewData', - 221 => 'Prefs', - 225 => 'ClassifyState', - 228 => 'SimilarityIndex', - 230 => 'DocumentNotes', - 231 => 'DocumentHistory', - 232 => 'ExifCameraInfo', - ), - 3 => array( // IPTC NewsPhoto Tags - 0 => 'NewsPhotoVersion', - 10 => 'IPTCPictureNumber', - 20 => 'IPTCImageWidth', - 30 => 'IPTCImageHeight', - 40 => 'IPTCPixelWidth', - 50 => 'IPTCPixelHeight', - 55 => 'SupplementalType', - 60 => 'ColorRepresentation', - 64 => 'InterchangeColorSpace', - 65 => 'ColorSequence', - 66 => 'ICC_Profile', - 70 => 'ColorCalibrationMatrix', - 80 => 'LookupTable', - 84 => 'NumIndexEntries', - 85 => 'ColorPalette', - 86 => 'IPTCBitsPerSample', - 90 => 'SampleStructure', - 100 => 'ScanningDirection', - 102 => 'IPTCImageRotation', - 110 => 'DataCompressionMethod', - 120 => 'QuantizationMethod', - 125 => 'EndPoints', - 130 => 'ExcursionTolerance', - 135 => 'BitsPerComponent', - 140 => 'MaximumDensityRange', - 145 => 'GammaCompensatedValue', - ), - 7 => array( // IPTC PreObjectData Tags - 10 => 'SizeMode', - 20 => 'MaxSubfileSize', - 90 => 'ObjectSizeAnnounced', - 95 => 'MaximumObjectSize', - ), - 8 => array( // IPTC ObjectData Tags - 10 => 'SubFile', - ), - 9 => array( // IPTC PostObjectData Tags - 10 => 'ConfirmedObjectSize', - ), - ); - - } - return (isset($IPTCrecordTagName[$iptc_record][$iptc_tagkey]) ? $IPTCrecordTagName[$iptc_record][$iptc_tagkey] : $iptc_tagkey); - } - -} diff --git a/src/Classes/Vendor/getid3/module.graphic.pcd.php b/src/Classes/Vendor/getid3/module.graphic.pcd.php deleted file mode 100755 index 2ea2a45b9..000000000 --- a/src/Classes/Vendor/getid3/module.graphic.pcd.php +++ /dev/null @@ -1,132 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.graphic.pcd.php // -// module for analyzing PhotoCD (PCD) Image files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_pcd extends getid3_handler -{ - public $ExtractData = 0; - - public function Analyze() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'pcd'; - $info['video']['dataformat'] = 'pcd'; - $info['video']['lossless'] = false; - - - fseek($this->getid3->fp, $info['avdataoffset'] + 72, SEEK_SET); - - $PCDflags = fread($this->getid3->fp, 1); - $PCDisVertical = ((ord($PCDflags) & 0x01) ? true : false); - - - if ($PCDisVertical) { - $info['video']['resolution_x'] = 3072; - $info['video']['resolution_y'] = 2048; - } else { - $info['video']['resolution_x'] = 2048; - $info['video']['resolution_y'] = 3072; - } - - - if ($this->ExtractData > 3) { - - $info['error'][] = 'Cannot extract PSD image data for detail levels above BASE (level-3) because encrypted with Kodak-proprietary compression/encryption.'; - - } elseif ($this->ExtractData > 0) { - - $PCD_levels[1] = array( 192, 128, 0x02000); // BASE/16 - $PCD_levels[2] = array( 384, 256, 0x0B800); // BASE/4 - $PCD_levels[3] = array( 768, 512, 0x30000); // BASE - //$PCD_levels[4] = array(1536, 1024, ??); // BASE*4 - encrypted with Kodak-proprietary compression/encryption - //$PCD_levels[5] = array(3072, 2048, ??); // BASE*16 - encrypted with Kodak-proprietary compression/encryption - //$PCD_levels[6] = array(6144, 4096, ??); // BASE*64 - encrypted with Kodak-proprietary compression/encryption; PhotoCD-Pro only - - list($PCD_width, $PCD_height, $PCD_dataOffset) = $PCD_levels[3]; - - fseek($this->getid3->fp, $info['avdataoffset'] + $PCD_dataOffset, SEEK_SET); - - for ($y = 0; $y < $PCD_height; $y += 2) { - // The image-data of these subtypes start at the respective offsets of 02000h, 0b800h and 30000h. - // To decode the YcbYr to the more usual RGB-code, three lines of data have to be read, each - // consisting of ‘w’ bytes, where ‘w’ is the width of the image-subtype. The first ‘w’ bytes and - // the first half of the third ‘w’ bytes contain data for the first RGB-line, the second ‘w’ bytes - // and the second half of the third ‘w’ bytes contain data for a second RGB-line. - - $PCD_data_Y1 = fread($this->getid3->fp, $PCD_width); - $PCD_data_Y2 = fread($this->getid3->fp, $PCD_width); - $PCD_data_Cb = fread($this->getid3->fp, intval(round($PCD_width / 2))); - $PCD_data_Cr = fread($this->getid3->fp, intval(round($PCD_width / 2))); - - for ($x = 0; $x < $PCD_width; $x++) { - if ($PCDisVertical) { - $info['pcd']['data'][$PCD_width - $x][$y] = $this->YCbCr2RGB(ord($PCD_data_Y1{$x}), ord($PCD_data_Cb{floor($x / 2)}), ord($PCD_data_Cr{floor($x / 2)})); - $info['pcd']['data'][$PCD_width - $x][$y + 1] = $this->YCbCr2RGB(ord($PCD_data_Y2{$x}), ord($PCD_data_Cb{floor($x / 2)}), ord($PCD_data_Cr{floor($x / 2)})); - } else { - $info['pcd']['data'][$y][$x] = $this->YCbCr2RGB(ord($PCD_data_Y1{$x}), ord($PCD_data_Cb{floor($x / 2)}), ord($PCD_data_Cr{floor($x / 2)})); - $info['pcd']['data'][$y + 1][$x] = $this->YCbCr2RGB(ord($PCD_data_Y2{$x}), ord($PCD_data_Cb{floor($x / 2)}), ord($PCD_data_Cr{floor($x / 2)})); - } - } - } - - // Example for plotting extracted data - //getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.audio.ac3.php', __FILE__, true); - //if ($PCDisVertical) { - // $BMPinfo['resolution_x'] = $PCD_height; - // $BMPinfo['resolution_y'] = $PCD_width; - //} else { - // $BMPinfo['resolution_x'] = $PCD_width; - // $BMPinfo['resolution_y'] = $PCD_height; - //} - //$BMPinfo['bmp']['data'] = $info['pcd']['data']; - //getid3_bmp::PlotBMP($BMPinfo); - //exit; - - } - - } - - public function YCbCr2RGB($Y, $Cb, $Cr) { - static $YCbCr_constants = array(); - if (empty($YCbCr_constants)) { - $YCbCr_constants['red']['Y'] = 0.0054980 * 256; - $YCbCr_constants['red']['Cb'] = 0.0000000 * 256; - $YCbCr_constants['red']['Cr'] = 0.0051681 * 256; - $YCbCr_constants['green']['Y'] = 0.0054980 * 256; - $YCbCr_constants['green']['Cb'] = -0.0015446 * 256; - $YCbCr_constants['green']['Cr'] = -0.0026325 * 256; - $YCbCr_constants['blue']['Y'] = 0.0054980 * 256; - $YCbCr_constants['blue']['Cb'] = 0.0079533 * 256; - $YCbCr_constants['blue']['Cr'] = 0.0000000 * 256; - } - - $RGBcolor = array('red'=>0, 'green'=>0, 'blue'=>0); - foreach ($RGBcolor as $rgbname => $dummy) { - $RGBcolor[$rgbname] = max(0, - min(255, - intval( - round( - ($YCbCr_constants[$rgbname]['Y'] * $Y) + - ($YCbCr_constants[$rgbname]['Cb'] * ($Cb - 156)) + - ($YCbCr_constants[$rgbname]['Cr'] * ($Cr - 137)) - ) - ) - ) - ); - } - return (($RGBcolor['red'] * 65536) + ($RGBcolor['green'] * 256) + $RGBcolor['blue']); - } - -} diff --git a/src/Classes/Vendor/getid3/module.graphic.png.php b/src/Classes/Vendor/getid3/module.graphic.png.php deleted file mode 100755 index 250ca1b10..000000000 --- a/src/Classes/Vendor/getid3/module.graphic.png.php +++ /dev/null @@ -1,517 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.graphic.png.php // -// module for analyzing PNG Image files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_png extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - // shortcut - $info['png'] = array(); - $thisfile_png = &$info['png']; - - $info['fileformat'] = 'png'; - $info['video']['dataformat'] = 'png'; - $info['video']['lossless'] = false; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $PNGfiledata = fread($this->getid3->fp, $this->getid3->fread_buffer_size()); - $offset = 0; - - $PNGidentifier = substr($PNGfiledata, $offset, 8); // $89 $50 $4E $47 $0D $0A $1A $0A - $offset += 8; - - if ($PNGidentifier != "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A") { - $info['error'][] = 'First 8 bytes of file ('.getid3_lib::PrintHexBytes($PNGidentifier).') did not match expected PNG identifier'; - unset($info['fileformat']); - return false; - } - - while (((ftell($this->getid3->fp) - (strlen($PNGfiledata) - $offset)) < $info['filesize'])) { - $chunk['data_length'] = getid3_lib::BigEndian2Int(substr($PNGfiledata, $offset, 4)); - $offset += 4; - while (((strlen($PNGfiledata) - $offset) < ($chunk['data_length'] + 4)) && (ftell($this->getid3->fp) < $info['filesize'])) { - $PNGfiledata .= fread($this->getid3->fp, $this->getid3->fread_buffer_size()); - } - $chunk['type_text'] = substr($PNGfiledata, $offset, 4); - $offset += 4; - $chunk['type_raw'] = getid3_lib::BigEndian2Int($chunk['type_text']); - $chunk['data'] = substr($PNGfiledata, $offset, $chunk['data_length']); - $offset += $chunk['data_length']; - $chunk['crc'] = getid3_lib::BigEndian2Int(substr($PNGfiledata, $offset, 4)); - $offset += 4; - - $chunk['flags']['ancilliary'] = (bool) ($chunk['type_raw'] & 0x20000000); - $chunk['flags']['private'] = (bool) ($chunk['type_raw'] & 0x00200000); - $chunk['flags']['reserved'] = (bool) ($chunk['type_raw'] & 0x00002000); - $chunk['flags']['safe_to_copy'] = (bool) ($chunk['type_raw'] & 0x00000020); - - // shortcut - $thisfile_png[$chunk['type_text']] = array(); - $thisfile_png_chunk_type_text = &$thisfile_png[$chunk['type_text']]; - - switch ($chunk['type_text']) { - - case 'IHDR': // Image Header - $thisfile_png_chunk_type_text['header'] = $chunk; - $thisfile_png_chunk_type_text['width'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 0, 4)); - $thisfile_png_chunk_type_text['height'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 4, 4)); - $thisfile_png_chunk_type_text['raw']['bit_depth'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 8, 1)); - $thisfile_png_chunk_type_text['raw']['color_type'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 9, 1)); - $thisfile_png_chunk_type_text['raw']['compression_method'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 10, 1)); - $thisfile_png_chunk_type_text['raw']['filter_method'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 11, 1)); - $thisfile_png_chunk_type_text['raw']['interlace_method'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 12, 1)); - - $thisfile_png_chunk_type_text['compression_method_text'] = $this->PNGcompressionMethodLookup($thisfile_png_chunk_type_text['raw']['compression_method']); - $thisfile_png_chunk_type_text['color_type']['palette'] = (bool) ($thisfile_png_chunk_type_text['raw']['color_type'] & 0x01); - $thisfile_png_chunk_type_text['color_type']['true_color'] = (bool) ($thisfile_png_chunk_type_text['raw']['color_type'] & 0x02); - $thisfile_png_chunk_type_text['color_type']['alpha'] = (bool) ($thisfile_png_chunk_type_text['raw']['color_type'] & 0x04); - - $info['video']['resolution_x'] = $thisfile_png_chunk_type_text['width']; - $info['video']['resolution_y'] = $thisfile_png_chunk_type_text['height']; - - $info['video']['bits_per_sample'] = $this->IHDRcalculateBitsPerSample($thisfile_png_chunk_type_text['raw']['color_type'], $thisfile_png_chunk_type_text['raw']['bit_depth']); - break; - - - case 'PLTE': // Palette - $thisfile_png_chunk_type_text['header'] = $chunk; - $paletteoffset = 0; - for ($i = 0; $i <= 255; $i++) { - //$thisfile_png_chunk_type_text['red'][$i] = getid3_lib::BigEndian2Int(substr($chunk['data'], $paletteoffset++, 1)); - //$thisfile_png_chunk_type_text['green'][$i] = getid3_lib::BigEndian2Int(substr($chunk['data'], $paletteoffset++, 1)); - //$thisfile_png_chunk_type_text['blue'][$i] = getid3_lib::BigEndian2Int(substr($chunk['data'], $paletteoffset++, 1)); - $red = getid3_lib::BigEndian2Int(substr($chunk['data'], $paletteoffset++, 1)); - $green = getid3_lib::BigEndian2Int(substr($chunk['data'], $paletteoffset++, 1)); - $blue = getid3_lib::BigEndian2Int(substr($chunk['data'], $paletteoffset++, 1)); - $thisfile_png_chunk_type_text[$i] = (($red << 16) | ($green << 8) | ($blue)); - } - break; - - - case 'tRNS': // Transparency - $thisfile_png_chunk_type_text['header'] = $chunk; - switch ($thisfile_png['IHDR']['raw']['color_type']) { - case 0: - $thisfile_png_chunk_type_text['transparent_color_gray'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 0, 2)); - break; - - case 2: - $thisfile_png_chunk_type_text['transparent_color_red'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 0, 2)); - $thisfile_png_chunk_type_text['transparent_color_green'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 2, 2)); - $thisfile_png_chunk_type_text['transparent_color_blue'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 4, 2)); - break; - - case 3: - for ($i = 0; $i < strlen($chunk['data']); $i++) { - $thisfile_png_chunk_type_text['palette_opacity'][$i] = getid3_lib::BigEndian2Int(substr($chunk['data'], $i, 1)); - } - break; - - case 4: - case 6: - $info['error'][] = 'Invalid color_type in tRNS chunk: '.$thisfile_png['IHDR']['raw']['color_type']; - - default: - $info['warning'][] = 'Unhandled color_type in tRNS chunk: '.$thisfile_png['IHDR']['raw']['color_type']; - break; - } - break; - - - case 'gAMA': // Image Gamma - $thisfile_png_chunk_type_text['header'] = $chunk; - $thisfile_png_chunk_type_text['gamma'] = getid3_lib::BigEndian2Int($chunk['data']) / 100000; - break; - - - case 'cHRM': // Primary Chromaticities - $thisfile_png_chunk_type_text['header'] = $chunk; - $thisfile_png_chunk_type_text['white_x'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 0, 4)) / 100000; - $thisfile_png_chunk_type_text['white_y'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 4, 4)) / 100000; - $thisfile_png_chunk_type_text['red_y'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 8, 4)) / 100000; - $thisfile_png_chunk_type_text['red_y'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 12, 4)) / 100000; - $thisfile_png_chunk_type_text['green_y'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 16, 4)) / 100000; - $thisfile_png_chunk_type_text['green_y'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 20, 4)) / 100000; - $thisfile_png_chunk_type_text['blue_y'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 24, 4)) / 100000; - $thisfile_png_chunk_type_text['blue_y'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 28, 4)) / 100000; - break; - - - case 'sRGB': // Standard RGB Color Space - $thisfile_png_chunk_type_text['header'] = $chunk; - $thisfile_png_chunk_type_text['reindering_intent'] = getid3_lib::BigEndian2Int($chunk['data']); - $thisfile_png_chunk_type_text['reindering_intent_text'] = $this->PNGsRGBintentLookup($thisfile_png_chunk_type_text['reindering_intent']); - break; - - - case 'iCCP': // Embedded ICC Profile - $thisfile_png_chunk_type_text['header'] = $chunk; - list($profilename, $compressiondata) = explode("\x00", $chunk['data'], 2); - $thisfile_png_chunk_type_text['profile_name'] = $profilename; - $thisfile_png_chunk_type_text['compression_method'] = getid3_lib::BigEndian2Int(substr($compressiondata, 0, 1)); - $thisfile_png_chunk_type_text['compression_profile'] = substr($compressiondata, 1); - - $thisfile_png_chunk_type_text['compression_method_text'] = $this->PNGcompressionMethodLookup($thisfile_png_chunk_type_text['compression_method']); - break; - - - case 'tEXt': // Textual Data - $thisfile_png_chunk_type_text['header'] = $chunk; - list($keyword, $text) = explode("\x00", $chunk['data'], 2); - $thisfile_png_chunk_type_text['keyword'] = $keyword; - $thisfile_png_chunk_type_text['text'] = $text; - - $thisfile_png['comments'][$thisfile_png_chunk_type_text['keyword']][] = $thisfile_png_chunk_type_text['text']; - break; - - - case 'zTXt': // Compressed Textual Data - $thisfile_png_chunk_type_text['header'] = $chunk; - list($keyword, $otherdata) = explode("\x00", $chunk['data'], 2); - $thisfile_png_chunk_type_text['keyword'] = $keyword; - $thisfile_png_chunk_type_text['compression_method'] = getid3_lib::BigEndian2Int(substr($otherdata, 0, 1)); - $thisfile_png_chunk_type_text['compressed_text'] = substr($otherdata, 1); - $thisfile_png_chunk_type_text['compression_method_text'] = $this->PNGcompressionMethodLookup($thisfile_png_chunk_type_text['compression_method']); - switch ($thisfile_png_chunk_type_text['compression_method']) { - case 0: - $thisfile_png_chunk_type_text['text'] = gzuncompress($thisfile_png_chunk_type_text['compressed_text']); - break; - - default: - // unknown compression method - break; - } - - if (isset($thisfile_png_chunk_type_text['text'])) { - $thisfile_png['comments'][$thisfile_png_chunk_type_text['keyword']][] = $thisfile_png_chunk_type_text['text']; - } - break; - - - case 'iTXt': // International Textual Data - $thisfile_png_chunk_type_text['header'] = $chunk; - list($keyword, $otherdata) = explode("\x00", $chunk['data'], 2); - $thisfile_png_chunk_type_text['keyword'] = $keyword; - $thisfile_png_chunk_type_text['compression'] = (bool) getid3_lib::BigEndian2Int(substr($otherdata, 0, 1)); - $thisfile_png_chunk_type_text['compression_method'] = getid3_lib::BigEndian2Int(substr($otherdata, 1, 1)); - $thisfile_png_chunk_type_text['compression_method_text'] = $this->PNGcompressionMethodLookup($thisfile_png_chunk_type_text['compression_method']); - list($languagetag, $translatedkeyword, $text) = explode("\x00", substr($otherdata, 2), 3); - $thisfile_png_chunk_type_text['language_tag'] = $languagetag; - $thisfile_png_chunk_type_text['translated_keyword'] = $translatedkeyword; - - if ($thisfile_png_chunk_type_text['compression']) { - - switch ($thisfile_png_chunk_type_text['compression_method']) { - case 0: - $thisfile_png_chunk_type_text['text'] = gzuncompress($text); - break; - - default: - // unknown compression method - break; - } - - } else { - - $thisfile_png_chunk_type_text['text'] = $text; - - } - - if (isset($thisfile_png_chunk_type_text['text'])) { - $thisfile_png['comments'][$thisfile_png_chunk_type_text['keyword']][] = $thisfile_png_chunk_type_text['text']; - } - break; - - - case 'bKGD': // Background Color - $thisfile_png_chunk_type_text['header'] = $chunk; - switch ($thisfile_png['IHDR']['raw']['color_type']) { - case 0: - case 4: - $thisfile_png_chunk_type_text['background_gray'] = getid3_lib::BigEndian2Int($chunk['data']); - break; - - case 2: - case 6: - $thisfile_png_chunk_type_text['background_red'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 0 * $thisfile_png['IHDR']['raw']['bit_depth'], $thisfile_png['IHDR']['raw']['bit_depth'])); - $thisfile_png_chunk_type_text['background_green'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 1 * $thisfile_png['IHDR']['raw']['bit_depth'], $thisfile_png['IHDR']['raw']['bit_depth'])); - $thisfile_png_chunk_type_text['background_blue'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 2 * $thisfile_png['IHDR']['raw']['bit_depth'], $thisfile_png['IHDR']['raw']['bit_depth'])); - break; - - case 3: - $thisfile_png_chunk_type_text['background_index'] = getid3_lib::BigEndian2Int($chunk['data']); - break; - - default: - break; - } - break; - - - case 'pHYs': // Physical Pixel Dimensions - $thisfile_png_chunk_type_text['header'] = $chunk; - $thisfile_png_chunk_type_text['pixels_per_unit_x'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 0, 4)); - $thisfile_png_chunk_type_text['pixels_per_unit_y'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 4, 4)); - $thisfile_png_chunk_type_text['unit_specifier'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 8, 1)); - $thisfile_png_chunk_type_text['unit'] = $this->PNGpHYsUnitLookup($thisfile_png_chunk_type_text['unit_specifier']); - break; - - - case 'sBIT': // Significant Bits - $thisfile_png_chunk_type_text['header'] = $chunk; - switch ($thisfile_png['IHDR']['raw']['color_type']) { - case 0: - $thisfile_png_chunk_type_text['significant_bits_gray'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 0, 1)); - break; - - case 2: - case 3: - $thisfile_png_chunk_type_text['significant_bits_red'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 0, 1)); - $thisfile_png_chunk_type_text['significant_bits_green'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 1, 1)); - $thisfile_png_chunk_type_text['significant_bits_blue'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 2, 1)); - break; - - case 4: - $thisfile_png_chunk_type_text['significant_bits_gray'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 0, 1)); - $thisfile_png_chunk_type_text['significant_bits_alpha'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 1, 1)); - break; - - case 6: - $thisfile_png_chunk_type_text['significant_bits_red'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 0, 1)); - $thisfile_png_chunk_type_text['significant_bits_green'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 1, 1)); - $thisfile_png_chunk_type_text['significant_bits_blue'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 2, 1)); - $thisfile_png_chunk_type_text['significant_bits_alpha'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 3, 1)); - break; - - default: - break; - } - break; - - - case 'sPLT': // Suggested Palette - $thisfile_png_chunk_type_text['header'] = $chunk; - list($palettename, $otherdata) = explode("\x00", $chunk['data'], 2); - $thisfile_png_chunk_type_text['palette_name'] = $palettename; - $sPLToffset = 0; - $thisfile_png_chunk_type_text['sample_depth_bits'] = getid3_lib::BigEndian2Int(substr($otherdata, $sPLToffset, 1)); - $sPLToffset += 1; - $thisfile_png_chunk_type_text['sample_depth_bytes'] = $thisfile_png_chunk_type_text['sample_depth_bits'] / 8; - $paletteCounter = 0; - while ($sPLToffset < strlen($otherdata)) { - $thisfile_png_chunk_type_text['red'][$paletteCounter] = getid3_lib::BigEndian2Int(substr($otherdata, $sPLToffset, $thisfile_png_chunk_type_text['sample_depth_bytes'])); - $sPLToffset += $thisfile_png_chunk_type_text['sample_depth_bytes']; - $thisfile_png_chunk_type_text['green'][$paletteCounter] = getid3_lib::BigEndian2Int(substr($otherdata, $sPLToffset, $thisfile_png_chunk_type_text['sample_depth_bytes'])); - $sPLToffset += $thisfile_png_chunk_type_text['sample_depth_bytes']; - $thisfile_png_chunk_type_text['blue'][$paletteCounter] = getid3_lib::BigEndian2Int(substr($otherdata, $sPLToffset, $thisfile_png_chunk_type_text['sample_depth_bytes'])); - $sPLToffset += $thisfile_png_chunk_type_text['sample_depth_bytes']; - $thisfile_png_chunk_type_text['alpha'][$paletteCounter] = getid3_lib::BigEndian2Int(substr($otherdata, $sPLToffset, $thisfile_png_chunk_type_text['sample_depth_bytes'])); - $sPLToffset += $thisfile_png_chunk_type_text['sample_depth_bytes']; - $thisfile_png_chunk_type_text['frequency'][$paletteCounter] = getid3_lib::BigEndian2Int(substr($otherdata, $sPLToffset, 2)); - $sPLToffset += 2; - $paletteCounter++; - } - break; - - - case 'hIST': // Palette Histogram - $thisfile_png_chunk_type_text['header'] = $chunk; - $hISTcounter = 0; - while ($hISTcounter < strlen($chunk['data'])) { - $thisfile_png_chunk_type_text[$hISTcounter] = getid3_lib::BigEndian2Int(substr($chunk['data'], $hISTcounter / 2, 2)); - $hISTcounter += 2; - } - break; - - - case 'tIME': // Image Last-Modification Time - $thisfile_png_chunk_type_text['header'] = $chunk; - $thisfile_png_chunk_type_text['year'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 0, 2)); - $thisfile_png_chunk_type_text['month'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 2, 1)); - $thisfile_png_chunk_type_text['day'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 3, 1)); - $thisfile_png_chunk_type_text['hour'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 4, 1)); - $thisfile_png_chunk_type_text['minute'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 5, 1)); - $thisfile_png_chunk_type_text['second'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 6, 1)); - $thisfile_png_chunk_type_text['unix'] = gmmktime($thisfile_png_chunk_type_text['hour'], $thisfile_png_chunk_type_text['minute'], $thisfile_png_chunk_type_text['second'], $thisfile_png_chunk_type_text['month'], $thisfile_png_chunk_type_text['day'], $thisfile_png_chunk_type_text['year']); - break; - - - case 'oFFs': // Image Offset - $thisfile_png_chunk_type_text['header'] = $chunk; - $thisfile_png_chunk_type_text['position_x'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 0, 4), false, true); - $thisfile_png_chunk_type_text['position_y'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 4, 4), false, true); - $thisfile_png_chunk_type_text['unit_specifier'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 8, 1)); - $thisfile_png_chunk_type_text['unit'] = $this->PNGoFFsUnitLookup($thisfile_png_chunk_type_text['unit_specifier']); - break; - - - case 'pCAL': // Calibration Of Pixel Values - $thisfile_png_chunk_type_text['header'] = $chunk; - list($calibrationname, $otherdata) = explode("\x00", $chunk['data'], 2); - $thisfile_png_chunk_type_text['calibration_name'] = $calibrationname; - $pCALoffset = 0; - $thisfile_png_chunk_type_text['original_zero'] = getid3_lib::BigEndian2Int(substr($chunk['data'], $pCALoffset, 4), false, true); - $pCALoffset += 4; - $thisfile_png_chunk_type_text['original_max'] = getid3_lib::BigEndian2Int(substr($chunk['data'], $pCALoffset, 4), false, true); - $pCALoffset += 4; - $thisfile_png_chunk_type_text['equation_type'] = getid3_lib::BigEndian2Int(substr($chunk['data'], $pCALoffset, 1)); - $pCALoffset += 1; - $thisfile_png_chunk_type_text['equation_type_text'] = $this->PNGpCALequationTypeLookup($thisfile_png_chunk_type_text['equation_type']); - $thisfile_png_chunk_type_text['parameter_count'] = getid3_lib::BigEndian2Int(substr($chunk['data'], $pCALoffset, 1)); - $pCALoffset += 1; - $thisfile_png_chunk_type_text['parameters'] = explode("\x00", substr($chunk['data'], $pCALoffset)); - break; - - - case 'sCAL': // Physical Scale Of Image Subject - $thisfile_png_chunk_type_text['header'] = $chunk; - $thisfile_png_chunk_type_text['unit_specifier'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 0, 1)); - $thisfile_png_chunk_type_text['unit'] = $this->PNGsCALUnitLookup($thisfile_png_chunk_type_text['unit_specifier']); - list($pixelwidth, $pixelheight) = explode("\x00", substr($chunk['data'], 1)); - $thisfile_png_chunk_type_text['pixel_width'] = $pixelwidth; - $thisfile_png_chunk_type_text['pixel_height'] = $pixelheight; - break; - - - case 'gIFg': // GIF Graphic Control Extension - $gIFgCounter = 0; - if (isset($thisfile_png_chunk_type_text) && is_array($thisfile_png_chunk_type_text)) { - $gIFgCounter = count($thisfile_png_chunk_type_text); - } - $thisfile_png_chunk_type_text[$gIFgCounter]['header'] = $chunk; - $thisfile_png_chunk_type_text[$gIFgCounter]['disposal_method'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 0, 1)); - $thisfile_png_chunk_type_text[$gIFgCounter]['user_input_flag'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 1, 1)); - $thisfile_png_chunk_type_text[$gIFgCounter]['delay_time'] = getid3_lib::BigEndian2Int(substr($chunk['data'], 2, 2)); - break; - - - case 'gIFx': // GIF Application Extension - $gIFxCounter = 0; - if (isset($thisfile_png_chunk_type_text) && is_array($thisfile_png_chunk_type_text)) { - $gIFxCounter = count($thisfile_png_chunk_type_text); - } - $thisfile_png_chunk_type_text[$gIFxCounter]['header'] = $chunk; - $thisfile_png_chunk_type_text[$gIFxCounter]['application_identifier'] = substr($chunk['data'], 0, 8); - $thisfile_png_chunk_type_text[$gIFxCounter]['authentication_code'] = substr($chunk['data'], 8, 3); - $thisfile_png_chunk_type_text[$gIFxCounter]['application_data'] = substr($chunk['data'], 11); - break; - - - case 'IDAT': // Image Data - $idatinformationfieldindex = 0; - if (isset($thisfile_png['IDAT']) && is_array($thisfile_png['IDAT'])) { - $idatinformationfieldindex = count($thisfile_png['IDAT']); - } - unset($chunk['data']); - $thisfile_png_chunk_type_text[$idatinformationfieldindex]['header'] = $chunk; - break; - - - case 'IEND': // Image Trailer - $thisfile_png_chunk_type_text['header'] = $chunk; - break; - - - default: - //unset($chunk['data']); - $thisfile_png_chunk_type_text['header'] = $chunk; - $info['warning'][] = 'Unhandled chunk type: '.$chunk['type_text']; - break; - } - } - - return true; - } - - public function PNGsRGBintentLookup($sRGB) { - static $PNGsRGBintentLookup = array( - 0 => 'Perceptual', - 1 => 'Relative colorimetric', - 2 => 'Saturation', - 3 => 'Absolute colorimetric' - ); - return (isset($PNGsRGBintentLookup[$sRGB]) ? $PNGsRGBintentLookup[$sRGB] : 'invalid'); - } - - public function PNGcompressionMethodLookup($compressionmethod) { - static $PNGcompressionMethodLookup = array( - 0 => 'deflate/inflate' - ); - return (isset($PNGcompressionMethodLookup[$compressionmethod]) ? $PNGcompressionMethodLookup[$compressionmethod] : 'invalid'); - } - - public function PNGpHYsUnitLookup($unitid) { - static $PNGpHYsUnitLookup = array( - 0 => 'unknown', - 1 => 'meter' - ); - return (isset($PNGpHYsUnitLookup[$unitid]) ? $PNGpHYsUnitLookup[$unitid] : 'invalid'); - } - - public function PNGoFFsUnitLookup($unitid) { - static $PNGoFFsUnitLookup = array( - 0 => 'pixel', - 1 => 'micrometer' - ); - return (isset($PNGoFFsUnitLookup[$unitid]) ? $PNGoFFsUnitLookup[$unitid] : 'invalid'); - } - - public function PNGpCALequationTypeLookup($equationtype) { - static $PNGpCALequationTypeLookup = array( - 0 => 'Linear mapping', - 1 => 'Base-e exponential mapping', - 2 => 'Arbitrary-base exponential mapping', - 3 => 'Hyperbolic mapping' - ); - return (isset($PNGpCALequationTypeLookup[$equationtype]) ? $PNGpCALequationTypeLookup[$equationtype] : 'invalid'); - } - - public function PNGsCALUnitLookup($unitid) { - static $PNGsCALUnitLookup = array( - 0 => 'meter', - 1 => 'radian' - ); - return (isset($PNGsCALUnitLookup[$unitid]) ? $PNGsCALUnitLookup[$unitid] : 'invalid'); - } - - public function IHDRcalculateBitsPerSample($color_type, $bit_depth) { - switch ($color_type) { - case 0: // Each pixel is a grayscale sample. - return $bit_depth; - break; - - case 2: // Each pixel is an R,G,B triple - return 3 * $bit_depth; - break; - - case 3: // Each pixel is a palette index; a PLTE chunk must appear. - return $bit_depth; - break; - - case 4: // Each pixel is a grayscale sample, followed by an alpha sample. - return 2 * $bit_depth; - break; - - case 6: // Each pixel is an R,G,B triple, followed by an alpha sample. - return 4 * $bit_depth; - break; - } - return false; - } - -} diff --git a/src/Classes/Vendor/getid3/module.graphic.svg.php b/src/Classes/Vendor/getid3/module.graphic.svg.php deleted file mode 100755 index 8b31167eb..000000000 --- a/src/Classes/Vendor/getid3/module.graphic.svg.php +++ /dev/null @@ -1,101 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.graphic.svg.php // -// module for analyzing SVG Image files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_svg extends getid3_handler -{ - - - public function Analyze() { - $info = &$this->getid3->info; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - - $SVGheader = fread($this->getid3->fp, 4096); - if (preg_match('#\<\?xml([^\>]+)\?\>#i', $SVGheader, $matches)) { - $info['svg']['xml']['raw'] = $matches; - } - if (preg_match('#\<\!DOCTYPE([^\>]+)\>#i', $SVGheader, $matches)) { - $info['svg']['doctype']['raw'] = $matches; - } - if (preg_match('#\]+)\>#i', $SVGheader, $matches)) { - $info['svg']['svg']['raw'] = $matches; - } - if (isset($info['svg']['svg']['raw'])) { - - $sections_to_fix = array('xml', 'doctype', 'svg'); - foreach ($sections_to_fix as $section_to_fix) { - if (!isset($info['svg'][$section_to_fix])) { - continue; - } - $section_data = array(); - while (preg_match('/ "([^"]+)"/', $info['svg'][$section_to_fix]['raw'][1], $matches)) { - $section_data[] = $matches[1]; - $info['svg'][$section_to_fix]['raw'][1] = str_replace($matches[0], '', $info['svg'][$section_to_fix]['raw'][1]); - } - while (preg_match('/([^\s]+)="([^"]+)"/', $info['svg'][$section_to_fix]['raw'][1], $matches)) { - $section_data[] = $matches[0]; - $info['svg'][$section_to_fix]['raw'][1] = str_replace($matches[0], '', $info['svg'][$section_to_fix]['raw'][1]); - } - $section_data = array_merge($section_data, preg_split('/[\s,]+/', $info['svg'][$section_to_fix]['raw'][1])); - foreach ($section_data as $keyvaluepair) { - $keyvaluepair = trim($keyvaluepair); - if ($keyvaluepair) { - $keyvalueexploded = explode('=', $keyvaluepair); - $key = (isset($keyvalueexploded[0]) ? $keyvalueexploded[0] : ''); - $value = (isset($keyvalueexploded[1]) ? $keyvalueexploded[1] : ''); - $info['svg'][$section_to_fix]['sections'][$key] = trim($value, '"'); - } - } - } - - $info['fileformat'] = 'svg'; - $info['video']['dataformat'] = 'svg'; - $info['video']['lossless'] = true; - //$info['video']['bits_per_sample'] = 24; - $info['video']['pixel_aspect_ratio'] = (float) 1; - - if (!empty($info['svg']['svg']['sections']['width'])) { - $info['svg']['width'] = intval($info['svg']['svg']['sections']['width']); - } - if (!empty($info['svg']['svg']['sections']['height'])) { - $info['svg']['height'] = intval($info['svg']['svg']['sections']['height']); - } - if (!empty($info['svg']['svg']['sections']['version'])) { - $info['svg']['version'] = $info['svg']['svg']['sections']['version']; - } - if (!isset($info['svg']['version']) && isset($info['svg']['doctype']['sections'])) { - foreach ($info['svg']['doctype']['sections'] as $key => $value) { - if (preg_match('#//W3C//DTD SVG ([0-9\.]+)//#i', $key, $matches)) { - $info['svg']['version'] = $matches[1]; - break; - } - } - } - - if (!empty($info['svg']['width'])) { - $info['video']['resolution_x'] = $info['svg']['width']; - } - if (!empty($info['svg']['height'])) { - $info['video']['resolution_y'] = $info['svg']['height']; - } - - return true; - } - $info['error'][] = 'Did not find expected tag'; - return false; - } - -} diff --git a/src/Classes/Vendor/getid3/module.graphic.tiff.php b/src/Classes/Vendor/getid3/module.graphic.tiff.php deleted file mode 100755 index 25996f0e5..000000000 --- a/src/Classes/Vendor/getid3/module.graphic.tiff.php +++ /dev/null @@ -1,224 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.archive.tiff.php // -// module for analyzing TIFF files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_tiff extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $TIFFheader = fread($this->getid3->fp, 4); - - switch (substr($TIFFheader, 0, 2)) { - case 'II': - $info['tiff']['byte_order'] = 'Intel'; - break; - case 'MM': - $info['tiff']['byte_order'] = 'Motorola'; - break; - default: - $info['error'][] = 'Invalid TIFF byte order identifier ('.substr($TIFFheader, 0, 2).') at offset '.$info['avdataoffset']; - return false; - break; - } - - $info['fileformat'] = 'tiff'; - $info['video']['dataformat'] = 'tiff'; - $info['video']['lossless'] = true; - $info['tiff']['ifd'] = array(); - $CurrentIFD = array(); - - $FieldTypeByteLength = array(1=>1, 2=>1, 3=>2, 4=>4, 5=>8); - - $nextIFDoffset = $this->TIFFendian2Int(fread($this->getid3->fp, 4), $info['tiff']['byte_order']); - - while ($nextIFDoffset > 0) { - - $CurrentIFD['offset'] = $nextIFDoffset; - - fseek($this->getid3->fp, $info['avdataoffset'] + $nextIFDoffset, SEEK_SET); - $CurrentIFD['fieldcount'] = $this->TIFFendian2Int(fread($this->getid3->fp, 2), $info['tiff']['byte_order']); - - for ($i = 0; $i < $CurrentIFD['fieldcount']; $i++) { - $CurrentIFD['fields'][$i]['raw']['tag'] = $this->TIFFendian2Int(fread($this->getid3->fp, 2), $info['tiff']['byte_order']); - $CurrentIFD['fields'][$i]['raw']['type'] = $this->TIFFendian2Int(fread($this->getid3->fp, 2), $info['tiff']['byte_order']); - $CurrentIFD['fields'][$i]['raw']['length'] = $this->TIFFendian2Int(fread($this->getid3->fp, 4), $info['tiff']['byte_order']); - $CurrentIFD['fields'][$i]['raw']['offset'] = fread($this->getid3->fp, 4); - - switch ($CurrentIFD['fields'][$i]['raw']['type']) { - case 1: // BYTE An 8-bit unsigned integer. - if ($CurrentIFD['fields'][$i]['raw']['length'] <= 4) { - $CurrentIFD['fields'][$i]['value'] = $this->TIFFendian2Int(substr($CurrentIFD['fields'][$i]['raw']['offset'], 0, 1), $info['tiff']['byte_order']); - } else { - $CurrentIFD['fields'][$i]['offset'] = $this->TIFFendian2Int($CurrentIFD['fields'][$i]['raw']['offset'], $info['tiff']['byte_order']); - } - break; - - case 2: // ASCII 8-bit bytes that store ASCII codes; the last byte must be null. - if ($CurrentIFD['fields'][$i]['raw']['length'] <= 4) { - $CurrentIFD['fields'][$i]['value'] = substr($CurrentIFD['fields'][$i]['raw']['offset'], 3); - } else { - $CurrentIFD['fields'][$i]['offset'] = $this->TIFFendian2Int($CurrentIFD['fields'][$i]['raw']['offset'], $info['tiff']['byte_order']); - } - break; - - case 3: // SHORT A 16-bit (2-byte) unsigned integer. - if ($CurrentIFD['fields'][$i]['raw']['length'] <= 2) { - $CurrentIFD['fields'][$i]['value'] = $this->TIFFendian2Int(substr($CurrentIFD['fields'][$i]['raw']['offset'], 0, 2), $info['tiff']['byte_order']); - } else { - $CurrentIFD['fields'][$i]['offset'] = $this->TIFFendian2Int($CurrentIFD['fields'][$i]['raw']['offset'], $info['tiff']['byte_order']); - } - break; - - case 4: // LONG A 32-bit (4-byte) unsigned integer. - if ($CurrentIFD['fields'][$i]['raw']['length'] <= 1) { - $CurrentIFD['fields'][$i]['value'] = $this->TIFFendian2Int($CurrentIFD['fields'][$i]['raw']['offset'], $info['tiff']['byte_order']); - } else { - $CurrentIFD['fields'][$i]['offset'] = $this->TIFFendian2Int($CurrentIFD['fields'][$i]['raw']['offset'], $info['tiff']['byte_order']); - } - break; - - case 5: // RATIONAL Two LONG_s: the first represents the numerator of a fraction, the second the denominator. - break; - } - } - - $info['tiff']['ifd'][] = $CurrentIFD; - $CurrentIFD = array(); - $nextIFDoffset = $this->TIFFendian2Int(fread($this->getid3->fp, 4), $info['tiff']['byte_order']); - - } - - foreach ($info['tiff']['ifd'] as $IFDid => $IFDarray) { - foreach ($IFDarray['fields'] as $key => $fieldarray) { - switch ($fieldarray['raw']['tag']) { - case 256: // ImageWidth - case 257: // ImageLength - case 258: // BitsPerSample - case 259: // Compression - if (!isset($fieldarray['value'])) { - fseek($this->getid3->fp, $fieldarray['offset'], SEEK_SET); - $info['tiff']['ifd'][$IFDid]['fields'][$key]['raw']['data'] = fread($this->getid3->fp, $fieldarray['raw']['length'] * $FieldTypeByteLength[$fieldarray['raw']['type']]); - - } - break; - - case 270: // ImageDescription - case 271: // Make - case 272: // Model - case 305: // Software - case 306: // DateTime - case 315: // Artist - case 316: // HostComputer - if (isset($fieldarray['value'])) { - $info['tiff']['ifd'][$IFDid]['fields'][$key]['raw']['data'] = $fieldarray['value']; - } else { - fseek($this->getid3->fp, $fieldarray['offset'], SEEK_SET); - $info['tiff']['ifd'][$IFDid]['fields'][$key]['raw']['data'] = fread($this->getid3->fp, $fieldarray['raw']['length'] * $FieldTypeByteLength[$fieldarray['raw']['type']]); - - } - break; - } - switch ($fieldarray['raw']['tag']) { - case 256: // ImageWidth - $info['video']['resolution_x'] = $fieldarray['value']; - break; - - case 257: // ImageLength - $info['video']['resolution_y'] = $fieldarray['value']; - break; - - case 258: // BitsPerSample - if (isset($fieldarray['value'])) { - $info['video']['bits_per_sample'] = $fieldarray['value']; - } else { - $info['video']['bits_per_sample'] = 0; - for ($i = 0; $i < $fieldarray['raw']['length']; $i++) { - $info['video']['bits_per_sample'] += $this->TIFFendian2Int(substr($info['tiff']['ifd'][$IFDid]['fields'][$key]['raw']['data'], $i * $FieldTypeByteLength[$fieldarray['raw']['type']], $FieldTypeByteLength[$fieldarray['raw']['type']]), $info['tiff']['byte_order']); - } - } - break; - - case 259: // Compression - $info['video']['codec'] = $this->TIFFcompressionMethod($fieldarray['value']); - break; - - case 270: // ImageDescription - case 271: // Make - case 272: // Model - case 305: // Software - case 306: // DateTime - case 315: // Artist - case 316: // HostComputer - $TIFFcommentName = $this->TIFFcommentName($fieldarray['raw']['tag']); - if (isset($info['tiff']['comments'][$TIFFcommentName])) { - $info['tiff']['comments'][$TIFFcommentName][] = $info['tiff']['ifd'][$IFDid]['fields'][$key]['raw']['data']; - } else { - $info['tiff']['comments'][$TIFFcommentName] = array($info['tiff']['ifd'][$IFDid]['fields'][$key]['raw']['data']); - } - break; - - default: - break; - } - } - } - - return true; - } - - - public function TIFFendian2Int($bytestring, $byteorder) { - if ($byteorder == 'Intel') { - return getid3_lib::LittleEndian2Int($bytestring); - } elseif ($byteorder == 'Motorola') { - return getid3_lib::BigEndian2Int($bytestring); - } - return false; - } - - public function TIFFcompressionMethod($id) { - static $TIFFcompressionMethod = array(); - if (empty($TIFFcompressionMethod)) { - $TIFFcompressionMethod = array( - 1 => 'Uncompressed', - 2 => 'Huffman', - 3 => 'Fax - CCITT 3', - 5 => 'LZW', - 32773 => 'PackBits', - ); - } - return (isset($TIFFcompressionMethod[$id]) ? $TIFFcompressionMethod[$id] : 'unknown/invalid ('.$id.')'); - } - - public function TIFFcommentName($id) { - static $TIFFcommentName = array(); - if (empty($TIFFcommentName)) { - $TIFFcommentName = array( - 270 => 'imagedescription', - 271 => 'make', - 272 => 'model', - 305 => 'software', - 306 => 'datetime', - 315 => 'artist', - 316 => 'hostcomputer', - ); - } - return (isset($TIFFcommentName[$id]) ? $TIFFcommentName[$id] : 'unknown/invalid ('.$id.')'); - } - -} diff --git a/src/Classes/Vendor/getid3/module.misc.cue.php b/src/Classes/Vendor/getid3/module.misc.cue.php deleted file mode 100755 index 1d3a0ceac..000000000 --- a/src/Classes/Vendor/getid3/module.misc.cue.php +++ /dev/null @@ -1,311 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.misc.cue.php // -// module for analyzing CUEsheet files // -// dependencies: NONE // -// // -///////////////////////////////////////////////////////////////// -// // -// Module originally written [2009-Mar-25] by // -// Nigel Barnes // -// Minor reformatting and similar small changes to integrate // -// into getID3 by James Heinrich // -// /// -///////////////////////////////////////////////////////////////// - -/* - * CueSheet parser by Nigel Barnes. - * - * This is a PHP conversion of CueSharp 0.5 by Wyatt O'Day (wyday.com/cuesharp) - */ - -/** - * A CueSheet class used to open and parse cuesheets. - * - */ -class getid3_cue extends getid3_handler -{ - public $cuesheet = array(); - - public function Analyze() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'cue'; - $this->readCueSheetFilename($info['filenamepath']); - $info['cue'] = $this->cuesheet; - return true; - } - - - - public function readCueSheetFilename($filename) - { - $filedata = file_get_contents($filename); - return $this->readCueSheet($filedata); - } - /** - * Parses a cue sheet file. - * - * @param string $filename - The filename for the cue sheet to open. - */ - public function readCueSheet(&$filedata) - { - $cue_lines = array(); - foreach (explode("\n", str_replace("\r", null, $filedata)) as $line) - { - if ( (strlen($line) > 0) && ($line[0] != '#')) - { - $cue_lines[] = trim($line); - } - } - $this->parseCueSheet($cue_lines); - - return $this->cuesheet; - } - - /** - * Parses the cue sheet array. - * - * @param array $file - The cuesheet as an array of each line. - */ - public function parseCueSheet($file) - { - //-1 means still global, all others are track specific - $track_on = -1; - - for ($i=0; $i < count($file); $i++) - { - list($key) = explode(' ', strtolower($file[$i]), 2); - switch ($key) - { - case 'catalog': - case 'cdtextfile': - case 'isrc': - case 'performer': - case 'songwriter': - case 'title': - $this->parseString($file[$i], $track_on); - break; - case 'file': - $currentFile = $this->parseFile($file[$i]); - break; - case 'flags': - $this->parseFlags($file[$i], $track_on); - break; - case 'index': - case 'postgap': - case 'pregap': - $this->parseIndex($file[$i], $track_on); - break; - case 'rem': - $this->parseComment($file[$i], $track_on); - break; - case 'track': - $track_on++; - $this->parseTrack($file[$i], $track_on); - if (isset($currentFile)) // if there's a file - { - $this->cuesheet['tracks'][$track_on]['datafile'] = $currentFile; - } - break; - default: - //save discarded junk and place string[] with track it was found in - $this->parseGarbage($file[$i], $track_on); - break; - } - } - } - - /** - * Parses the REM command. - * - * @param string $line - The line in the cue file that contains the TRACK command. - * @param integer $track_on - The track currently processing. - */ - public function parseComment($line, $track_on) - { - $explodedline = explode(' ', $line, 3); - $comment_REM = (isset($explodedline[0]) ? $explodedline[0] : ''); - $comment_type = (isset($explodedline[1]) ? $explodedline[1] : ''); - $comment_data = (isset($explodedline[2]) ? $explodedline[2] : ''); - if (($comment_REM == 'REM') && $comment_type) { - $comment_type = strtolower($comment_type); - $commment_data = trim($comment_data, ' "'); - if ($track_on != -1) { - $this->cuesheet['tracks'][$track_on]['comments'][$comment_type][] = $comment_data; - } else { - $this->cuesheet['comments'][$comment_type][] = $comment_data; - } - } - } - - /** - * Parses the FILE command. - * - * @param string $line - The line in the cue file that contains the FILE command. - * @return array - Array of FILENAME and TYPE of file.. - */ - public function parseFile($line) - { - $line = substr($line, strpos($line, ' ') + 1); - $type = strtolower(substr($line, strrpos($line, ' '))); - - //remove type - $line = substr($line, 0, strrpos($line, ' ') - 1); - - //if quotes around it, remove them. - $line = trim($line, '"'); - - return array('filename'=>$line, 'type'=>$type); - } - - /** - * Parses the FLAG command. - * - * @param string $line - The line in the cue file that contains the TRACK command. - * @param integer $track_on - The track currently processing. - */ - public function parseFlags($line, $track_on) - { - if ($track_on != -1) - { - foreach (explode(' ', strtolower($line)) as $type) - { - switch ($type) - { - case 'flags': - // first entry in this line - $this->cuesheet['tracks'][$track_on]['flags'] = array( - '4ch' => false, - 'data' => false, - 'dcp' => false, - 'pre' => false, - 'scms' => false, - ); - break; - case 'data': - case 'dcp': - case '4ch': - case 'pre': - case 'scms': - $this->cuesheet['tracks'][$track_on]['flags'][$type] = true; - break; - default: - break; - } - } - } - } - - /** - * Collect any unidentified data. - * - * @param string $line - The line in the cue file that contains the TRACK command. - * @param integer $track_on - The track currently processing. - */ - public function parseGarbage($line, $track_on) - { - if ( strlen($line) > 0 ) - { - if ($track_on == -1) - { - $this->cuesheet['garbage'][] = $line; - } - else - { - $this->cuesheet['tracks'][$track_on]['garbage'][] = $line; - } - } - } - - /** - * Parses the INDEX command of a TRACK. - * - * @param string $line - The line in the cue file that contains the TRACK command. - * @param integer $track_on - The track currently processing. - */ - public function parseIndex($line, $track_on) - { - $type = strtolower(substr($line, 0, strpos($line, ' '))); - $line = substr($line, strpos($line, ' ') + 1); - - if ($type == 'index') - { - //read the index number - $number = intval(substr($line, 0, strpos($line, ' '))); - $line = substr($line, strpos($line, ' ') + 1); - } - - //extract the minutes, seconds, and frames - $explodedline = explode(':', $line); - $minutes = (isset($explodedline[0]) ? $explodedline[0] : ''); - $seconds = (isset($explodedline[1]) ? $explodedline[1] : ''); - $frames = (isset($explodedline[2]) ? $explodedline[2] : ''); - - switch ($type) { - case 'index': - $this->cuesheet['tracks'][$track_on][$type][$number] = array('minutes'=>intval($minutes), 'seconds'=>intval($seconds), 'frames'=>intval($frames)); - break; - case 'pregap': - case 'postgap': - $this->cuesheet['tracks'][$track_on][$type] = array('minutes'=>intval($minutes), 'seconds'=>intval($seconds), 'frames'=>intval($frames)); - break; - } - } - - public function parseString($line, $track_on) - { - $category = strtolower(substr($line, 0, strpos($line, ' '))); - $line = substr($line, strpos($line, ' ') + 1); - - //get rid of the quotes - $line = trim($line, '"'); - - switch ($category) - { - case 'catalog': - case 'cdtextfile': - case 'isrc': - case 'performer': - case 'songwriter': - case 'title': - if ($track_on == -1) - { - $this->cuesheet[$category] = $line; - } - else - { - $this->cuesheet['tracks'][$track_on][$category] = $line; - } - break; - default: - break; - } - } - - /** - * Parses the TRACK command. - * - * @param string $line - The line in the cue file that contains the TRACK command. - * @param integer $track_on - The track currently processing. - */ - public function parseTrack($line, $track_on) - { - $line = substr($line, strpos($line, ' ') + 1); - $track = ltrim(substr($line, 0, strpos($line, ' ')), '0'); - - //find the data type. - $datatype = strtolower(substr($line, strpos($line, ' ') + 1)); - - $this->cuesheet['tracks'][$track_on] = array('track_number'=>$track, 'datatype'=>$datatype); - } - -} - diff --git a/src/Classes/Vendor/getid3/module.misc.exe.php b/src/Classes/Vendor/getid3/module.misc.exe.php deleted file mode 100755 index 15e786b43..000000000 --- a/src/Classes/Vendor/getid3/module.misc.exe.php +++ /dev/null @@ -1,58 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.misc.exe.php // -// module for analyzing EXE files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_exe extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $EXEheader = fread($this->getid3->fp, 28); - - $magic = 'MZ'; - if (substr($EXEheader, 0, 2) != $magic) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" at offset '.$info['avdataoffset'].', found "'.getid3_lib::PrintHexBytes(substr($EXEheader, 0, 2)).'"'; - return false; - } - - $info['fileformat'] = 'exe'; - $info['exe']['mz']['magic'] = 'MZ'; - - $info['exe']['mz']['raw']['last_page_size'] = getid3_lib::LittleEndian2Int(substr($EXEheader, 2, 2)); - $info['exe']['mz']['raw']['page_count'] = getid3_lib::LittleEndian2Int(substr($EXEheader, 4, 2)); - $info['exe']['mz']['raw']['relocation_count'] = getid3_lib::LittleEndian2Int(substr($EXEheader, 6, 2)); - $info['exe']['mz']['raw']['header_paragraphs'] = getid3_lib::LittleEndian2Int(substr($EXEheader, 8, 2)); - $info['exe']['mz']['raw']['min_memory_paragraphs'] = getid3_lib::LittleEndian2Int(substr($EXEheader, 10, 2)); - $info['exe']['mz']['raw']['max_memory_paragraphs'] = getid3_lib::LittleEndian2Int(substr($EXEheader, 12, 2)); - $info['exe']['mz']['raw']['initial_ss'] = getid3_lib::LittleEndian2Int(substr($EXEheader, 14, 2)); - $info['exe']['mz']['raw']['initial_sp'] = getid3_lib::LittleEndian2Int(substr($EXEheader, 16, 2)); - $info['exe']['mz']['raw']['checksum'] = getid3_lib::LittleEndian2Int(substr($EXEheader, 18, 2)); - $info['exe']['mz']['raw']['cs_ip'] = getid3_lib::LittleEndian2Int(substr($EXEheader, 20, 4)); - $info['exe']['mz']['raw']['relocation_table_offset'] = getid3_lib::LittleEndian2Int(substr($EXEheader, 24, 2)); - $info['exe']['mz']['raw']['overlay_number'] = getid3_lib::LittleEndian2Int(substr($EXEheader, 26, 2)); - - $info['exe']['mz']['byte_size'] = (($info['exe']['mz']['raw']['page_count'] - 1)) * 512 + $info['exe']['mz']['raw']['last_page_size']; - $info['exe']['mz']['header_size'] = $info['exe']['mz']['raw']['header_paragraphs'] * 16; - $info['exe']['mz']['memory_minimum'] = $info['exe']['mz']['raw']['min_memory_paragraphs'] * 16; - $info['exe']['mz']['memory_recommended'] = $info['exe']['mz']['raw']['max_memory_paragraphs'] * 16; - -$info['error'][] = 'EXE parsing not enabled in this version of getID3() ['.$this->getid3->version().']'; -return false; - - } - -} diff --git a/src/Classes/Vendor/getid3/module.misc.iso.php b/src/Classes/Vendor/getid3/module.misc.iso.php deleted file mode 100755 index 39bd16af3..000000000 --- a/src/Classes/Vendor/getid3/module.misc.iso.php +++ /dev/null @@ -1,387 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.misc.iso.php // -// module for analyzing ISO files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_iso extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'iso'; - - for ($i = 16; $i <= 19; $i++) { - fseek($this->getid3->fp, 2048 * $i, SEEK_SET); - $ISOheader = fread($this->getid3->fp, 2048); - if (substr($ISOheader, 1, 5) == 'CD001') { - switch (ord($ISOheader{0})) { - case 1: - $info['iso']['primary_volume_descriptor']['offset'] = 2048 * $i; - $this->ParsePrimaryVolumeDescriptor($ISOheader); - break; - - case 2: - $info['iso']['supplementary_volume_descriptor']['offset'] = 2048 * $i; - $this->ParseSupplementaryVolumeDescriptor($ISOheader); - break; - - default: - // skip - break; - } - } - } - - $this->ParsePathTable(); - - $info['iso']['files'] = array(); - foreach ($info['iso']['path_table']['directories'] as $directorynum => $directorydata) { - $info['iso']['directories'][$directorynum] = $this->ParseDirectoryRecord($directorydata); - } - - return true; - } - - - public function ParsePrimaryVolumeDescriptor(&$ISOheader) { - // ISO integer values are stored *BOTH* Little-Endian AND Big-Endian format!! - // ie 12345 == 0x3039 is stored as $39 $30 $30 $39 in a 4-byte field - - // shortcuts - $info = &$this->getid3->info; - $info['iso']['primary_volume_descriptor']['raw'] = array(); - $thisfile_iso_primaryVD = &$info['iso']['primary_volume_descriptor']; - $thisfile_iso_primaryVD_raw = &$thisfile_iso_primaryVD['raw']; - - $thisfile_iso_primaryVD_raw['volume_descriptor_type'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 0, 1)); - $thisfile_iso_primaryVD_raw['standard_identifier'] = substr($ISOheader, 1, 5); - if ($thisfile_iso_primaryVD_raw['standard_identifier'] != 'CD001') { - $info['error'][] = 'Expected "CD001" at offset ('.($thisfile_iso_primaryVD['offset'] + 1).'), found "'.$thisfile_iso_primaryVD_raw['standard_identifier'].'" instead'; - unset($info['fileformat']); - unset($info['iso']); - return false; - } - - - $thisfile_iso_primaryVD_raw['volume_descriptor_version'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 6, 1)); - //$thisfile_iso_primaryVD_raw['unused_1'] = substr($ISOheader, 7, 1); - $thisfile_iso_primaryVD_raw['system_identifier'] = substr($ISOheader, 8, 32); - $thisfile_iso_primaryVD_raw['volume_identifier'] = substr($ISOheader, 40, 32); - //$thisfile_iso_primaryVD_raw['unused_2'] = substr($ISOheader, 72, 8); - $thisfile_iso_primaryVD_raw['volume_space_size'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 80, 4)); - //$thisfile_iso_primaryVD_raw['unused_3'] = substr($ISOheader, 88, 32); - $thisfile_iso_primaryVD_raw['volume_set_size'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 120, 2)); - $thisfile_iso_primaryVD_raw['volume_sequence_number'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 124, 2)); - $thisfile_iso_primaryVD_raw['logical_block_size'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 128, 2)); - $thisfile_iso_primaryVD_raw['path_table_size'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 132, 4)); - $thisfile_iso_primaryVD_raw['path_table_l_location'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 140, 2)); - $thisfile_iso_primaryVD_raw['path_table_l_opt_location'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 144, 2)); - $thisfile_iso_primaryVD_raw['path_table_m_location'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 148, 2)); - $thisfile_iso_primaryVD_raw['path_table_m_opt_location'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 152, 2)); - $thisfile_iso_primaryVD_raw['root_directory_record'] = substr($ISOheader, 156, 34); - $thisfile_iso_primaryVD_raw['volume_set_identifier'] = substr($ISOheader, 190, 128); - $thisfile_iso_primaryVD_raw['publisher_identifier'] = substr($ISOheader, 318, 128); - $thisfile_iso_primaryVD_raw['data_preparer_identifier'] = substr($ISOheader, 446, 128); - $thisfile_iso_primaryVD_raw['application_identifier'] = substr($ISOheader, 574, 128); - $thisfile_iso_primaryVD_raw['copyright_file_identifier'] = substr($ISOheader, 702, 37); - $thisfile_iso_primaryVD_raw['abstract_file_identifier'] = substr($ISOheader, 739, 37); - $thisfile_iso_primaryVD_raw['bibliographic_file_identifier'] = substr($ISOheader, 776, 37); - $thisfile_iso_primaryVD_raw['volume_creation_date_time'] = substr($ISOheader, 813, 17); - $thisfile_iso_primaryVD_raw['volume_modification_date_time'] = substr($ISOheader, 830, 17); - $thisfile_iso_primaryVD_raw['volume_expiration_date_time'] = substr($ISOheader, 847, 17); - $thisfile_iso_primaryVD_raw['volume_effective_date_time'] = substr($ISOheader, 864, 17); - $thisfile_iso_primaryVD_raw['file_structure_version'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 881, 1)); - //$thisfile_iso_primaryVD_raw['unused_4'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 882, 1)); - $thisfile_iso_primaryVD_raw['application_data'] = substr($ISOheader, 883, 512); - //$thisfile_iso_primaryVD_raw['unused_5'] = substr($ISOheader, 1395, 653); - - $thisfile_iso_primaryVD['system_identifier'] = trim($thisfile_iso_primaryVD_raw['system_identifier']); - $thisfile_iso_primaryVD['volume_identifier'] = trim($thisfile_iso_primaryVD_raw['volume_identifier']); - $thisfile_iso_primaryVD['volume_set_identifier'] = trim($thisfile_iso_primaryVD_raw['volume_set_identifier']); - $thisfile_iso_primaryVD['publisher_identifier'] = trim($thisfile_iso_primaryVD_raw['publisher_identifier']); - $thisfile_iso_primaryVD['data_preparer_identifier'] = trim($thisfile_iso_primaryVD_raw['data_preparer_identifier']); - $thisfile_iso_primaryVD['application_identifier'] = trim($thisfile_iso_primaryVD_raw['application_identifier']); - $thisfile_iso_primaryVD['copyright_file_identifier'] = trim($thisfile_iso_primaryVD_raw['copyright_file_identifier']); - $thisfile_iso_primaryVD['abstract_file_identifier'] = trim($thisfile_iso_primaryVD_raw['abstract_file_identifier']); - $thisfile_iso_primaryVD['bibliographic_file_identifier'] = trim($thisfile_iso_primaryVD_raw['bibliographic_file_identifier']); - $thisfile_iso_primaryVD['volume_creation_date_time'] = $this->ISOtimeText2UNIXtime($thisfile_iso_primaryVD_raw['volume_creation_date_time']); - $thisfile_iso_primaryVD['volume_modification_date_time'] = $this->ISOtimeText2UNIXtime($thisfile_iso_primaryVD_raw['volume_modification_date_time']); - $thisfile_iso_primaryVD['volume_expiration_date_time'] = $this->ISOtimeText2UNIXtime($thisfile_iso_primaryVD_raw['volume_expiration_date_time']); - $thisfile_iso_primaryVD['volume_effective_date_time'] = $this->ISOtimeText2UNIXtime($thisfile_iso_primaryVD_raw['volume_effective_date_time']); - - if (($thisfile_iso_primaryVD_raw['volume_space_size'] * 2048) > $info['filesize']) { - $info['error'][] = 'Volume Space Size ('.($thisfile_iso_primaryVD_raw['volume_space_size'] * 2048).' bytes) is larger than the file size ('.$info['filesize'].' bytes) (truncated file?)'; - } - - return true; - } - - - public function ParseSupplementaryVolumeDescriptor(&$ISOheader) { - // ISO integer values are stored Both-Endian format!! - // ie 12345 == 0x3039 is stored as $39 $30 $30 $39 in a 4-byte field - - // shortcuts - $info = &$this->getid3->info; - $info['iso']['supplementary_volume_descriptor']['raw'] = array(); - $thisfile_iso_supplementaryVD = &$info['iso']['supplementary_volume_descriptor']; - $thisfile_iso_supplementaryVD_raw = &$thisfile_iso_supplementaryVD['raw']; - - $thisfile_iso_supplementaryVD_raw['volume_descriptor_type'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 0, 1)); - $thisfile_iso_supplementaryVD_raw['standard_identifier'] = substr($ISOheader, 1, 5); - if ($thisfile_iso_supplementaryVD_raw['standard_identifier'] != 'CD001') { - $info['error'][] = 'Expected "CD001" at offset ('.($thisfile_iso_supplementaryVD['offset'] + 1).'), found "'.$thisfile_iso_supplementaryVD_raw['standard_identifier'].'" instead'; - unset($info['fileformat']); - unset($info['iso']); - return false; - } - - $thisfile_iso_supplementaryVD_raw['volume_descriptor_version'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 6, 1)); - //$thisfile_iso_supplementaryVD_raw['unused_1'] = substr($ISOheader, 7, 1); - $thisfile_iso_supplementaryVD_raw['system_identifier'] = substr($ISOheader, 8, 32); - $thisfile_iso_supplementaryVD_raw['volume_identifier'] = substr($ISOheader, 40, 32); - //$thisfile_iso_supplementaryVD_raw['unused_2'] = substr($ISOheader, 72, 8); - $thisfile_iso_supplementaryVD_raw['volume_space_size'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 80, 4)); - if ($thisfile_iso_supplementaryVD_raw['volume_space_size'] == 0) { - // Supplementary Volume Descriptor not used - //unset($thisfile_iso_supplementaryVD); - //return false; - } - - //$thisfile_iso_supplementaryVD_raw['unused_3'] = substr($ISOheader, 88, 32); - $thisfile_iso_supplementaryVD_raw['volume_set_size'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 120, 2)); - $thisfile_iso_supplementaryVD_raw['volume_sequence_number'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 124, 2)); - $thisfile_iso_supplementaryVD_raw['logical_block_size'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 128, 2)); - $thisfile_iso_supplementaryVD_raw['path_table_size'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 132, 4)); - $thisfile_iso_supplementaryVD_raw['path_table_l_location'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 140, 2)); - $thisfile_iso_supplementaryVD_raw['path_table_l_opt_location'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 144, 2)); - $thisfile_iso_supplementaryVD_raw['path_table_m_location'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 148, 2)); - $thisfile_iso_supplementaryVD_raw['path_table_m_opt_location'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 152, 2)); - $thisfile_iso_supplementaryVD_raw['root_directory_record'] = substr($ISOheader, 156, 34); - $thisfile_iso_supplementaryVD_raw['volume_set_identifier'] = substr($ISOheader, 190, 128); - $thisfile_iso_supplementaryVD_raw['publisher_identifier'] = substr($ISOheader, 318, 128); - $thisfile_iso_supplementaryVD_raw['data_preparer_identifier'] = substr($ISOheader, 446, 128); - $thisfile_iso_supplementaryVD_raw['application_identifier'] = substr($ISOheader, 574, 128); - $thisfile_iso_supplementaryVD_raw['copyright_file_identifier'] = substr($ISOheader, 702, 37); - $thisfile_iso_supplementaryVD_raw['abstract_file_identifier'] = substr($ISOheader, 739, 37); - $thisfile_iso_supplementaryVD_raw['bibliographic_file_identifier'] = substr($ISOheader, 776, 37); - $thisfile_iso_supplementaryVD_raw['volume_creation_date_time'] = substr($ISOheader, 813, 17); - $thisfile_iso_supplementaryVD_raw['volume_modification_date_time'] = substr($ISOheader, 830, 17); - $thisfile_iso_supplementaryVD_raw['volume_expiration_date_time'] = substr($ISOheader, 847, 17); - $thisfile_iso_supplementaryVD_raw['volume_effective_date_time'] = substr($ISOheader, 864, 17); - $thisfile_iso_supplementaryVD_raw['file_structure_version'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 881, 1)); - //$thisfile_iso_supplementaryVD_raw['unused_4'] = getid3_lib::LittleEndian2Int(substr($ISOheader, 882, 1)); - $thisfile_iso_supplementaryVD_raw['application_data'] = substr($ISOheader, 883, 512); - //$thisfile_iso_supplementaryVD_raw['unused_5'] = substr($ISOheader, 1395, 653); - - $thisfile_iso_supplementaryVD['system_identifier'] = trim($thisfile_iso_supplementaryVD_raw['system_identifier']); - $thisfile_iso_supplementaryVD['volume_identifier'] = trim($thisfile_iso_supplementaryVD_raw['volume_identifier']); - $thisfile_iso_supplementaryVD['volume_set_identifier'] = trim($thisfile_iso_supplementaryVD_raw['volume_set_identifier']); - $thisfile_iso_supplementaryVD['publisher_identifier'] = trim($thisfile_iso_supplementaryVD_raw['publisher_identifier']); - $thisfile_iso_supplementaryVD['data_preparer_identifier'] = trim($thisfile_iso_supplementaryVD_raw['data_preparer_identifier']); - $thisfile_iso_supplementaryVD['application_identifier'] = trim($thisfile_iso_supplementaryVD_raw['application_identifier']); - $thisfile_iso_supplementaryVD['copyright_file_identifier'] = trim($thisfile_iso_supplementaryVD_raw['copyright_file_identifier']); - $thisfile_iso_supplementaryVD['abstract_file_identifier'] = trim($thisfile_iso_supplementaryVD_raw['abstract_file_identifier']); - $thisfile_iso_supplementaryVD['bibliographic_file_identifier'] = trim($thisfile_iso_supplementaryVD_raw['bibliographic_file_identifier']); - $thisfile_iso_supplementaryVD['volume_creation_date_time'] = $this->ISOtimeText2UNIXtime($thisfile_iso_supplementaryVD_raw['volume_creation_date_time']); - $thisfile_iso_supplementaryVD['volume_modification_date_time'] = $this->ISOtimeText2UNIXtime($thisfile_iso_supplementaryVD_raw['volume_modification_date_time']); - $thisfile_iso_supplementaryVD['volume_expiration_date_time'] = $this->ISOtimeText2UNIXtime($thisfile_iso_supplementaryVD_raw['volume_expiration_date_time']); - $thisfile_iso_supplementaryVD['volume_effective_date_time'] = $this->ISOtimeText2UNIXtime($thisfile_iso_supplementaryVD_raw['volume_effective_date_time']); - - if (($thisfile_iso_supplementaryVD_raw['volume_space_size'] * $thisfile_iso_supplementaryVD_raw['logical_block_size']) > $info['filesize']) { - $info['error'][] = 'Volume Space Size ('.($thisfile_iso_supplementaryVD_raw['volume_space_size'] * $thisfile_iso_supplementaryVD_raw['logical_block_size']).' bytes) is larger than the file size ('.$info['filesize'].' bytes) (truncated file?)'; - } - - return true; - } - - - public function ParsePathTable() { - $info = &$this->getid3->info; - if (!isset($info['iso']['supplementary_volume_descriptor']['raw']['path_table_l_location']) && !isset($info['iso']['primary_volume_descriptor']['raw']['path_table_l_location'])) { - return false; - } - if (isset($info['iso']['supplementary_volume_descriptor']['raw']['path_table_l_location'])) { - $PathTableLocation = $info['iso']['supplementary_volume_descriptor']['raw']['path_table_l_location']; - $PathTableSize = $info['iso']['supplementary_volume_descriptor']['raw']['path_table_size']; - $TextEncoding = 'UTF-16BE'; // Big-Endian Unicode - } else { - $PathTableLocation = $info['iso']['primary_volume_descriptor']['raw']['path_table_l_location']; - $PathTableSize = $info['iso']['primary_volume_descriptor']['raw']['path_table_size']; - $TextEncoding = 'ISO-8859-1'; // Latin-1 - } - - if (($PathTableLocation * 2048) > $info['filesize']) { - $info['error'][] = 'Path Table Location specifies an offset ('.($PathTableLocation * 2048).') beyond the end-of-file ('.$info['filesize'].')'; - return false; - } - - $info['iso']['path_table']['offset'] = $PathTableLocation * 2048; - fseek($this->getid3->fp, $info['iso']['path_table']['offset'], SEEK_SET); - $info['iso']['path_table']['raw'] = fread($this->getid3->fp, $PathTableSize); - - $offset = 0; - $pathcounter = 1; - while ($offset < $PathTableSize) { - // shortcut - $info['iso']['path_table']['directories'][$pathcounter] = array(); - $thisfile_iso_pathtable_directories_current = &$info['iso']['path_table']['directories'][$pathcounter]; - - $thisfile_iso_pathtable_directories_current['length'] = getid3_lib::LittleEndian2Int(substr($info['iso']['path_table']['raw'], $offset, 1)); - $offset += 1; - $thisfile_iso_pathtable_directories_current['extended_length'] = getid3_lib::LittleEndian2Int(substr($info['iso']['path_table']['raw'], $offset, 1)); - $offset += 1; - $thisfile_iso_pathtable_directories_current['location_logical'] = getid3_lib::LittleEndian2Int(substr($info['iso']['path_table']['raw'], $offset, 4)); - $offset += 4; - $thisfile_iso_pathtable_directories_current['parent_directory'] = getid3_lib::LittleEndian2Int(substr($info['iso']['path_table']['raw'], $offset, 2)); - $offset += 2; - $thisfile_iso_pathtable_directories_current['name'] = substr($info['iso']['path_table']['raw'], $offset, $thisfile_iso_pathtable_directories_current['length']); - $offset += $thisfile_iso_pathtable_directories_current['length'] + ($thisfile_iso_pathtable_directories_current['length'] % 2); - - $thisfile_iso_pathtable_directories_current['name_ascii'] = getid3_lib::iconv_fallback($TextEncoding, $info['encoding'], $thisfile_iso_pathtable_directories_current['name']); - - $thisfile_iso_pathtable_directories_current['location_bytes'] = $thisfile_iso_pathtable_directories_current['location_logical'] * 2048; - if ($pathcounter == 1) { - $thisfile_iso_pathtable_directories_current['full_path'] = '/'; - } else { - $thisfile_iso_pathtable_directories_current['full_path'] = $info['iso']['path_table']['directories'][$thisfile_iso_pathtable_directories_current['parent_directory']]['full_path'].$thisfile_iso_pathtable_directories_current['name_ascii'].'/'; - } - $FullPathArray[] = $thisfile_iso_pathtable_directories_current['full_path']; - - $pathcounter++; - } - - return true; - } - - - public function ParseDirectoryRecord($directorydata) { - $info = &$this->getid3->info; - if (isset($info['iso']['supplementary_volume_descriptor'])) { - $TextEncoding = 'UTF-16BE'; // Big-Endian Unicode - } else { - $TextEncoding = 'ISO-8859-1'; // Latin-1 - } - - fseek($this->getid3->fp, $directorydata['location_bytes'], SEEK_SET); - $DirectoryRecordData = fread($this->getid3->fp, 1); - - while (ord($DirectoryRecordData{0}) > 33) { - - $DirectoryRecordData .= fread($this->getid3->fp, ord($DirectoryRecordData{0}) - 1); - - $ThisDirectoryRecord['raw']['length'] = getid3_lib::LittleEndian2Int(substr($DirectoryRecordData, 0, 1)); - $ThisDirectoryRecord['raw']['extended_attribute_length'] = getid3_lib::LittleEndian2Int(substr($DirectoryRecordData, 1, 1)); - $ThisDirectoryRecord['raw']['offset_logical'] = getid3_lib::LittleEndian2Int(substr($DirectoryRecordData, 2, 4)); - $ThisDirectoryRecord['raw']['filesize'] = getid3_lib::LittleEndian2Int(substr($DirectoryRecordData, 10, 4)); - $ThisDirectoryRecord['raw']['recording_date_time'] = substr($DirectoryRecordData, 18, 7); - $ThisDirectoryRecord['raw']['file_flags'] = getid3_lib::LittleEndian2Int(substr($DirectoryRecordData, 25, 1)); - $ThisDirectoryRecord['raw']['file_unit_size'] = getid3_lib::LittleEndian2Int(substr($DirectoryRecordData, 26, 1)); - $ThisDirectoryRecord['raw']['interleave_gap_size'] = getid3_lib::LittleEndian2Int(substr($DirectoryRecordData, 27, 1)); - $ThisDirectoryRecord['raw']['volume_sequence_number'] = getid3_lib::LittleEndian2Int(substr($DirectoryRecordData, 28, 2)); - $ThisDirectoryRecord['raw']['file_identifier_length'] = getid3_lib::LittleEndian2Int(substr($DirectoryRecordData, 32, 1)); - $ThisDirectoryRecord['raw']['file_identifier'] = substr($DirectoryRecordData, 33, $ThisDirectoryRecord['raw']['file_identifier_length']); - - $ThisDirectoryRecord['file_identifier_ascii'] = getid3_lib::iconv_fallback($TextEncoding, $info['encoding'], $ThisDirectoryRecord['raw']['file_identifier']); - - $ThisDirectoryRecord['filesize'] = $ThisDirectoryRecord['raw']['filesize']; - $ThisDirectoryRecord['offset_bytes'] = $ThisDirectoryRecord['raw']['offset_logical'] * 2048; - $ThisDirectoryRecord['file_flags']['hidden'] = (bool) ($ThisDirectoryRecord['raw']['file_flags'] & 0x01); - $ThisDirectoryRecord['file_flags']['directory'] = (bool) ($ThisDirectoryRecord['raw']['file_flags'] & 0x02); - $ThisDirectoryRecord['file_flags']['associated'] = (bool) ($ThisDirectoryRecord['raw']['file_flags'] & 0x04); - $ThisDirectoryRecord['file_flags']['extended'] = (bool) ($ThisDirectoryRecord['raw']['file_flags'] & 0x08); - $ThisDirectoryRecord['file_flags']['permissions'] = (bool) ($ThisDirectoryRecord['raw']['file_flags'] & 0x10); - $ThisDirectoryRecord['file_flags']['multiple'] = (bool) ($ThisDirectoryRecord['raw']['file_flags'] & 0x80); - $ThisDirectoryRecord['recording_timestamp'] = $this->ISOtime2UNIXtime($ThisDirectoryRecord['raw']['recording_date_time']); - - if ($ThisDirectoryRecord['file_flags']['directory']) { - $ThisDirectoryRecord['filename'] = $directorydata['full_path']; - } else { - $ThisDirectoryRecord['filename'] = $directorydata['full_path'].$this->ISOstripFilenameVersion($ThisDirectoryRecord['file_identifier_ascii']); - $info['iso']['files'] = getid3_lib::array_merge_clobber($info['iso']['files'], getid3_lib::CreateDeepArray($ThisDirectoryRecord['filename'], '/', $ThisDirectoryRecord['filesize'])); - } - - $DirectoryRecord[] = $ThisDirectoryRecord; - $DirectoryRecordData = fread($this->getid3->fp, 1); - } - - return $DirectoryRecord; - } - - public function ISOstripFilenameVersion($ISOfilename) { - // convert 'filename.ext;1' to 'filename.ext' - if (!strstr($ISOfilename, ';')) { - return $ISOfilename; - } else { - return substr($ISOfilename, 0, strpos($ISOfilename, ';')); - } - } - - public function ISOtimeText2UNIXtime($ISOtime) { - - $UNIXyear = (int) substr($ISOtime, 0, 4); - $UNIXmonth = (int) substr($ISOtime, 4, 2); - $UNIXday = (int) substr($ISOtime, 6, 2); - $UNIXhour = (int) substr($ISOtime, 8, 2); - $UNIXminute = (int) substr($ISOtime, 10, 2); - $UNIXsecond = (int) substr($ISOtime, 12, 2); - - if (!$UNIXyear) { - return false; - } - return gmmktime($UNIXhour, $UNIXminute, $UNIXsecond, $UNIXmonth, $UNIXday, $UNIXyear); - } - - public function ISOtime2UNIXtime($ISOtime) { - // Represented by seven bytes: - // 1: Number of years since 1900 - // 2: Month of the year from 1 to 12 - // 3: Day of the Month from 1 to 31 - // 4: Hour of the day from 0 to 23 - // 5: Minute of the hour from 0 to 59 - // 6: second of the minute from 0 to 59 - // 7: Offset from Greenwich Mean Time in number of 15 minute intervals from -48 (West) to +52 (East) - - $UNIXyear = ord($ISOtime{0}) + 1900; - $UNIXmonth = ord($ISOtime{1}); - $UNIXday = ord($ISOtime{2}); - $UNIXhour = ord($ISOtime{3}); - $UNIXminute = ord($ISOtime{4}); - $UNIXsecond = ord($ISOtime{5}); - $GMToffset = $this->TwosCompliment2Decimal(ord($ISOtime{5})); - - return gmmktime($UNIXhour, $UNIXminute, $UNIXsecond, $UNIXmonth, $UNIXday, $UNIXyear); - } - - public function TwosCompliment2Decimal($BinaryValue) { - // http://sandbox.mc.edu/~bennet/cs110/tc/tctod.html - // First check if the number is negative or positive by looking at the sign bit. - // If it is positive, simply convert it to decimal. - // If it is negative, make it positive by inverting the bits and adding one. - // Then, convert the result to decimal. - // The negative of this number is the value of the original binary. - - if ($BinaryValue & 0x80) { - - // negative number - return (0 - ((~$BinaryValue & 0xFF) + 1)); - } else { - // positive number - return $BinaryValue; - } - } - - -} diff --git a/src/Classes/Vendor/getid3/module.misc.msoffice.php b/src/Classes/Vendor/getid3/module.misc.msoffice.php deleted file mode 100755 index a5077e129..000000000 --- a/src/Classes/Vendor/getid3/module.misc.msoffice.php +++ /dev/null @@ -1,37 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.archive.doc.php // -// module for analyzing MS Office (.doc, .xls, etc) files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_msoffice extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - fseek($this->getid3->fp, $info['avdataoffset'], SEEK_SET); - $DOCFILEheader = fread($this->getid3->fp, 8); - $magic = "\xD0\xCF\x11\xE0\xA1\xB1\x1A\xE1"; - if (substr($DOCFILEheader, 0, 8) != $magic) { - $info['error'][] = 'Expecting "'.getid3_lib::PrintHexBytes($magic).'" at '.$info['avdataoffset'].', found '.getid3_lib::PrintHexBytes(substr($DOCFILEheader, 0, 8)).' instead.'; - return false; - } - $info['fileformat'] = 'msoffice'; - -$info['error'][] = 'MS Office (.doc, .xls, etc) parsing not enabled in this version of getID3() ['.$this->getid3->version().']'; -return false; - - } - -} diff --git a/src/Classes/Vendor/getid3/module.misc.par2.php b/src/Classes/Vendor/getid3/module.misc.par2.php deleted file mode 100755 index 80b47d295..000000000 --- a/src/Classes/Vendor/getid3/module.misc.par2.php +++ /dev/null @@ -1,30 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.misc.par2.php // -// module for analyzing PAR2 files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_par2 extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'par2'; - - $info['error'][] = 'PAR2 parsing not enabled in this version of getID3()'; - return false; - - } - -} diff --git a/src/Classes/Vendor/getid3/module.misc.pdf.php b/src/Classes/Vendor/getid3/module.misc.pdf.php deleted file mode 100755 index 3b8aaa146..000000000 --- a/src/Classes/Vendor/getid3/module.misc.pdf.php +++ /dev/null @@ -1,30 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.misc.pdf.php // -// module for analyzing PDF files // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_pdf extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - $info['fileformat'] = 'pdf'; - - $info['error'][] = 'PDF parsing not enabled in this version of getID3() ['.$this->getid3->version().']'; - return false; - - } - -} diff --git a/src/Classes/Vendor/getid3/module.tag.apetag.php b/src/Classes/Vendor/getid3/module.tag.apetag.php deleted file mode 100755 index afeede769..000000000 --- a/src/Classes/Vendor/getid3/module.tag.apetag.php +++ /dev/null @@ -1,370 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.tag.apetag.php // -// module for analyzing APE tags // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - -class getid3_apetag extends getid3_handler -{ - public $inline_attachments = true; // true: return full data for all attachments; false: return no data for all attachments; integer: return data for attachments <= than this; string: save as file to this directory - public $overrideendoffset = 0; - - public function Analyze() { - $info = &$this->getid3->info; - - if (!getid3_lib::intValueSupported($info['filesize'])) { - $info['warning'][] = 'Unable to check for APEtags because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB'; - return false; - } - - $id3v1tagsize = 128; - $apetagheadersize = 32; - $lyrics3tagsize = 10; - - if ($this->overrideendoffset == 0) { - - fseek($this->getid3->fp, 0 - $id3v1tagsize - $apetagheadersize - $lyrics3tagsize, SEEK_END); - $APEfooterID3v1 = fread($this->getid3->fp, $id3v1tagsize + $apetagheadersize + $lyrics3tagsize); - - //if (preg_match('/APETAGEX.{24}TAG.{125}$/i', $APEfooterID3v1)) { - if (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $id3v1tagsize - $apetagheadersize, 8) == 'APETAGEX') { - - // APE tag found before ID3v1 - $info['ape']['tag_offset_end'] = $info['filesize'] - $id3v1tagsize; - - //} elseif (preg_match('/APETAGEX.{24}$/i', $APEfooterID3v1)) { - } elseif (substr($APEfooterID3v1, strlen($APEfooterID3v1) - $apetagheadersize, 8) == 'APETAGEX') { - - // APE tag found, no ID3v1 - $info['ape']['tag_offset_end'] = $info['filesize']; - - } - - } else { - - fseek($this->getid3->fp, $this->overrideendoffset - $apetagheadersize, SEEK_SET); - if (fread($this->getid3->fp, 8) == 'APETAGEX') { - $info['ape']['tag_offset_end'] = $this->overrideendoffset; - } - - } - if (!isset($info['ape']['tag_offset_end'])) { - - // APE tag not found - unset($info['ape']); - return false; - - } - - // shortcut - $thisfile_ape = &$info['ape']; - - fseek($this->getid3->fp, $thisfile_ape['tag_offset_end'] - $apetagheadersize, SEEK_SET); - $APEfooterData = fread($this->getid3->fp, 32); - if (!($thisfile_ape['footer'] = $this->parseAPEheaderFooter($APEfooterData))) { - $info['error'][] = 'Error parsing APE footer at offset '.$thisfile_ape['tag_offset_end']; - return false; - } - - if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) { - fseek($this->getid3->fp, $thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize'] - $apetagheadersize, SEEK_SET); - $thisfile_ape['tag_offset_start'] = ftell($this->getid3->fp); - $APEtagData = fread($this->getid3->fp, $thisfile_ape['footer']['raw']['tagsize'] + $apetagheadersize); - } else { - $thisfile_ape['tag_offset_start'] = $thisfile_ape['tag_offset_end'] - $thisfile_ape['footer']['raw']['tagsize']; - fseek($this->getid3->fp, $thisfile_ape['tag_offset_start'], SEEK_SET); - $APEtagData = fread($this->getid3->fp, $thisfile_ape['footer']['raw']['tagsize']); - } - $info['avdataend'] = $thisfile_ape['tag_offset_start']; - - if (isset($info['id3v1']['tag_offset_start']) && ($info['id3v1']['tag_offset_start'] < $thisfile_ape['tag_offset_end'])) { - $info['warning'][] = 'ID3v1 tag information ignored since it appears to be a false synch in APEtag data'; - unset($info['id3v1']); - foreach ($info['warning'] as $key => $value) { - if ($value == 'Some ID3v1 fields do not use NULL characters for padding') { - unset($info['warning'][$key]); - sort($info['warning']); - break; - } - } - } - - $offset = 0; - if (isset($thisfile_ape['footer']['flags']['header']) && $thisfile_ape['footer']['flags']['header']) { - if ($thisfile_ape['header'] = $this->parseAPEheaderFooter(substr($APEtagData, 0, $apetagheadersize))) { - $offset += $apetagheadersize; - } else { - $info['error'][] = 'Error parsing APE header at offset '.$thisfile_ape['tag_offset_start']; - return false; - } - } - - // shortcut - $info['replay_gain'] = array(); - $thisfile_replaygain = &$info['replay_gain']; - - for ($i = 0; $i < $thisfile_ape['footer']['raw']['tag_items']; $i++) { - $value_size = getid3_lib::LittleEndian2Int(substr($APEtagData, $offset, 4)); - $offset += 4; - $item_flags = getid3_lib::LittleEndian2Int(substr($APEtagData, $offset, 4)); - $offset += 4; - if (strstr(substr($APEtagData, $offset), "\x00") === false) { - $info['error'][] = 'Cannot find null-byte (0x00) seperator between ItemKey #'.$i.' and value. ItemKey starts '.$offset.' bytes into the APE tag, at file offset '.($thisfile_ape['tag_offset_start'] + $offset); - return false; - } - $ItemKeyLength = strpos($APEtagData, "\x00", $offset) - $offset; - $item_key = strtolower(substr($APEtagData, $offset, $ItemKeyLength)); - - // shortcut - $thisfile_ape['items'][$item_key] = array(); - $thisfile_ape_items_current = &$thisfile_ape['items'][$item_key]; - - $thisfile_ape_items_current['offset'] = $thisfile_ape['tag_offset_start'] + $offset; - - $offset += ($ItemKeyLength + 1); // skip 0x00 terminator - $thisfile_ape_items_current['data'] = substr($APEtagData, $offset, $value_size); - $offset += $value_size; - - $thisfile_ape_items_current['flags'] = $this->parseAPEtagFlags($item_flags); - switch ($thisfile_ape_items_current['flags']['item_contents_raw']) { - case 0: // UTF-8 - case 3: // Locator (URL, filename, etc), UTF-8 encoded - $thisfile_ape_items_current['data'] = explode("\x00", trim($thisfile_ape_items_current['data'])); - break; - - default: // binary data - break; - } - - switch (strtolower($item_key)) { - case 'replaygain_track_gain': - $thisfile_replaygain['track']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! - $thisfile_replaygain['track']['originator'] = 'unspecified'; - break; - - case 'replaygain_track_peak': - $thisfile_replaygain['track']['peak'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! - $thisfile_replaygain['track']['originator'] = 'unspecified'; - if ($thisfile_replaygain['track']['peak'] <= 0) { - $info['warning'][] = 'ReplayGain Track peak from APEtag appears invalid: '.$thisfile_replaygain['track']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")'; - } - break; - - case 'replaygain_album_gain': - $thisfile_replaygain['album']['adjustment'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! - $thisfile_replaygain['album']['originator'] = 'unspecified'; - break; - - case 'replaygain_album_peak': - $thisfile_replaygain['album']['peak'] = (float) str_replace(',', '.', $thisfile_ape_items_current['data'][0]); // float casting will see "0,95" as zero! - $thisfile_replaygain['album']['originator'] = 'unspecified'; - if ($thisfile_replaygain['album']['peak'] <= 0) { - $info['warning'][] = 'ReplayGain Album peak from APEtag appears invalid: '.$thisfile_replaygain['album']['peak'].' (original value = "'.$thisfile_ape_items_current['data'][0].'")'; - } - break; - - case 'mp3gain_undo': - list($mp3gain_undo_left, $mp3gain_undo_right, $mp3gain_undo_wrap) = explode(',', $thisfile_ape_items_current['data'][0]); - $thisfile_replaygain['mp3gain']['undo_left'] = intval($mp3gain_undo_left); - $thisfile_replaygain['mp3gain']['undo_right'] = intval($mp3gain_undo_right); - $thisfile_replaygain['mp3gain']['undo_wrap'] = (($mp3gain_undo_wrap == 'Y') ? true : false); - break; - - case 'mp3gain_minmax': - list($mp3gain_globalgain_min, $mp3gain_globalgain_max) = explode(',', $thisfile_ape_items_current['data'][0]); - $thisfile_replaygain['mp3gain']['globalgain_track_min'] = intval($mp3gain_globalgain_min); - $thisfile_replaygain['mp3gain']['globalgain_track_max'] = intval($mp3gain_globalgain_max); - break; - - case 'mp3gain_album_minmax': - list($mp3gain_globalgain_album_min, $mp3gain_globalgain_album_max) = explode(',', $thisfile_ape_items_current['data'][0]); - $thisfile_replaygain['mp3gain']['globalgain_album_min'] = intval($mp3gain_globalgain_album_min); - $thisfile_replaygain['mp3gain']['globalgain_album_max'] = intval($mp3gain_globalgain_album_max); - break; - - case 'tracknumber': - if (is_array($thisfile_ape_items_current['data'])) { - foreach ($thisfile_ape_items_current['data'] as $comment) { - $thisfile_ape['comments']['track'][] = $comment; - } - } - break; - - case 'cover art (artist)': - case 'cover art (back)': - case 'cover art (band logo)': - case 'cover art (band)': - case 'cover art (colored fish)': - case 'cover art (composer)': - case 'cover art (conductor)': - case 'cover art (front)': - case 'cover art (icon)': - case 'cover art (illustration)': - case 'cover art (lead)': - case 'cover art (leaflet)': - case 'cover art (lyricist)': - case 'cover art (media)': - case 'cover art (movie scene)': - case 'cover art (other icon)': - case 'cover art (other)': - case 'cover art (performance)': - case 'cover art (publisher logo)': - case 'cover art (recording)': - case 'cover art (studio)': - // list of possible cover arts from http://taglib-sharp.sourcearchive.com/documentation/2.0.3.0-2/Ape_2Tag_8cs-source.html - list($thisfile_ape_items_current['filename'], $thisfile_ape_items_current['data']) = explode("\x00", $thisfile_ape_items_current['data'], 2); - $thisfile_ape_items_current['data_offset'] = $thisfile_ape_items_current['offset'] + strlen($thisfile_ape_items_current['filename']."\x00"); - $thisfile_ape_items_current['data_length'] = strlen($thisfile_ape_items_current['data']); - - $thisfile_ape_items_current['image_mime'] = ''; - $imageinfo = array(); - $imagechunkcheck = getid3_lib::GetDataImageSize($thisfile_ape_items_current['data'], $imageinfo); - $thisfile_ape_items_current['image_mime'] = image_type_to_mime_type($imagechunkcheck[2]); - - do { - if ($this->inline_attachments === false) { - // skip entirely - unset($thisfile_ape_items_current['data']); - break; - } - if ($this->inline_attachments === true) { - // great - } elseif (is_int($this->inline_attachments)) { - if ($this->inline_attachments < $thisfile_ape_items_current['data_length']) { - // too big, skip - $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' is too large to process inline ('.number_format($thisfile_ape_items_current['data_length']).' bytes)'; - unset($thisfile_ape_items_current['data']); - break; - } - } elseif (is_string($this->inline_attachments)) { - $this->inline_attachments = rtrim(str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->inline_attachments), DIRECTORY_SEPARATOR); - if (!is_dir($this->inline_attachments) || !is_writable($this->inline_attachments)) { - // cannot write, skip - $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$this->inline_attachments.'" (not writable)'; - unset($thisfile_ape_items_current['data']); - break; - } - } - // if we get this far, must be OK - if (is_string($this->inline_attachments)) { - $destination_filename = $this->inline_attachments.DIRECTORY_SEPARATOR.md5($info['filenamepath']).'_'.$thisfile_ape_items_current['data_offset']; - if (!file_exists($destination_filename) || is_writable($destination_filename)) { - file_put_contents($destination_filename, $thisfile_ape_items_current['data']); - } else { - $info['warning'][] = 'attachment at '.$thisfile_ape_items_current['offset'].' cannot be saved to "'.$destination_filename.'" (not writable)'; - } - $thisfile_ape_items_current['data_filename'] = $destination_filename; - unset($thisfile_ape_items_current['data']); - } else { - if (!isset($info['ape']['comments']['picture'])) { - $info['ape']['comments']['picture'] = array(); - } - $info['ape']['comments']['picture'][] = array('data'=>$thisfile_ape_items_current['data'], 'image_mime'=>$thisfile_ape_items_current['image_mime']); - } - } while (false); - break; - - default: - if (is_array($thisfile_ape_items_current['data'])) { - foreach ($thisfile_ape_items_current['data'] as $comment) { - $thisfile_ape['comments'][strtolower($item_key)][] = $comment; - } - } - break; - } - - } - if (empty($thisfile_replaygain)) { - unset($info['replay_gain']); - } - return true; - } - - public function parseAPEheaderFooter($APEheaderFooterData) { - // http://www.uni-jena.de/~pfk/mpp/sv8/apeheader.html - - // shortcut - $headerfooterinfo['raw'] = array(); - $headerfooterinfo_raw = &$headerfooterinfo['raw']; - - $headerfooterinfo_raw['footer_tag'] = substr($APEheaderFooterData, 0, 8); - if ($headerfooterinfo_raw['footer_tag'] != 'APETAGEX') { - return false; - } - $headerfooterinfo_raw['version'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 8, 4)); - $headerfooterinfo_raw['tagsize'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 12, 4)); - $headerfooterinfo_raw['tag_items'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 16, 4)); - $headerfooterinfo_raw['global_flags'] = getid3_lib::LittleEndian2Int(substr($APEheaderFooterData, 20, 4)); - $headerfooterinfo_raw['reserved'] = substr($APEheaderFooterData, 24, 8); - - $headerfooterinfo['tag_version'] = $headerfooterinfo_raw['version'] / 1000; - if ($headerfooterinfo['tag_version'] >= 2) { - $headerfooterinfo['flags'] = $this->parseAPEtagFlags($headerfooterinfo_raw['global_flags']); - } - return $headerfooterinfo; - } - - public function parseAPEtagFlags($rawflagint) { - // "Note: APE Tags 1.0 do not use any of the APE Tag flags. - // All are set to zero on creation and ignored on reading." - // http://www.uni-jena.de/~pfk/mpp/sv8/apetagflags.html - $flags['header'] = (bool) ($rawflagint & 0x80000000); - $flags['footer'] = (bool) ($rawflagint & 0x40000000); - $flags['this_is_header'] = (bool) ($rawflagint & 0x20000000); - $flags['item_contents_raw'] = ($rawflagint & 0x00000006) >> 1; - $flags['read_only'] = (bool) ($rawflagint & 0x00000001); - - $flags['item_contents'] = $this->APEcontentTypeFlagLookup($flags['item_contents_raw']); - - return $flags; - } - - public function APEcontentTypeFlagLookup($contenttypeid) { - static $APEcontentTypeFlagLookup = array( - 0 => 'utf-8', - 1 => 'binary', - 2 => 'external', - 3 => 'reserved' - ); - return (isset($APEcontentTypeFlagLookup[$contenttypeid]) ? $APEcontentTypeFlagLookup[$contenttypeid] : 'invalid'); - } - - public function APEtagItemIsUTF8Lookup($itemkey) { - static $APEtagItemIsUTF8Lookup = array( - 'title', - 'subtitle', - 'artist', - 'album', - 'debut album', - 'publisher', - 'conductor', - 'track', - 'composer', - 'comment', - 'copyright', - 'publicationright', - 'file', - 'year', - 'record date', - 'record location', - 'genre', - 'media', - 'related', - 'isrc', - 'abstract', - 'language', - 'bibliography' - ); - return in_array(strtolower($itemkey), $APEtagItemIsUTF8Lookup); - } - -} diff --git a/src/Classes/Vendor/getid3/module.tag.id3v1.php b/src/Classes/Vendor/getid3/module.tag.id3v1.php deleted file mode 100755 index fd9069e04..000000000 --- a/src/Classes/Vendor/getid3/module.tag.id3v1.php +++ /dev/null @@ -1,359 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.tag.id3v1.php // -// module for analyzing ID3v1 tags // -// dependencies: NONE // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_id3v1 extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - if (!getid3_lib::intValueSupported($info['filesize'])) { - $info['warning'][] = 'Unable to check for ID3v1 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB'; - return false; - } - - fseek($this->getid3->fp, -256, SEEK_END); - $preid3v1 = fread($this->getid3->fp, 128); - $id3v1tag = fread($this->getid3->fp, 128); - - if (substr($id3v1tag, 0, 3) == 'TAG') { - - $info['avdataend'] = $info['filesize'] - 128; - - $ParsedID3v1['title'] = $this->cutfield(substr($id3v1tag, 3, 30)); - $ParsedID3v1['artist'] = $this->cutfield(substr($id3v1tag, 33, 30)); - $ParsedID3v1['album'] = $this->cutfield(substr($id3v1tag, 63, 30)); - $ParsedID3v1['year'] = $this->cutfield(substr($id3v1tag, 93, 4)); - $ParsedID3v1['comment'] = substr($id3v1tag, 97, 30); // can't remove nulls yet, track detection depends on them - $ParsedID3v1['genreid'] = ord(substr($id3v1tag, 127, 1)); - - // If second-last byte of comment field is null and last byte of comment field is non-null - // then this is ID3v1.1 and the comment field is 28 bytes long and the 30th byte is the track number - if (($id3v1tag{125} === "\x00") && ($id3v1tag{126} !== "\x00")) { - $ParsedID3v1['track'] = ord(substr($ParsedID3v1['comment'], 29, 1)); - $ParsedID3v1['comment'] = substr($ParsedID3v1['comment'], 0, 28); - } - $ParsedID3v1['comment'] = $this->cutfield($ParsedID3v1['comment']); - - $ParsedID3v1['genre'] = $this->LookupGenreName($ParsedID3v1['genreid']); - if (!empty($ParsedID3v1['genre'])) { - unset($ParsedID3v1['genreid']); - } - if (isset($ParsedID3v1['genre']) && (empty($ParsedID3v1['genre']) || ($ParsedID3v1['genre'] == 'Unknown'))) { - unset($ParsedID3v1['genre']); - } - - foreach ($ParsedID3v1 as $key => $value) { - $ParsedID3v1['comments'][$key][0] = $value; - } - - // ID3v1 data is supposed to be padded with NULL characters, but some taggers pad with spaces - $GoodFormatID3v1tag = $this->GenerateID3v1Tag( - $ParsedID3v1['title'], - $ParsedID3v1['artist'], - $ParsedID3v1['album'], - $ParsedID3v1['year'], - (isset($ParsedID3v1['genre']) ? $this->LookupGenreID($ParsedID3v1['genre']) : false), - $ParsedID3v1['comment'], - (!empty($ParsedID3v1['track']) ? $ParsedID3v1['track'] : '')); - $ParsedID3v1['padding_valid'] = true; - if ($id3v1tag !== $GoodFormatID3v1tag) { - $ParsedID3v1['padding_valid'] = false; - $info['warning'][] = 'Some ID3v1 fields do not use NULL characters for padding'; - } - - $ParsedID3v1['tag_offset_end'] = $info['filesize']; - $ParsedID3v1['tag_offset_start'] = $ParsedID3v1['tag_offset_end'] - 128; - - $info['id3v1'] = $ParsedID3v1; - } - - if (substr($preid3v1, 0, 3) == 'TAG') { - // The way iTunes handles tags is, well, brain-damaged. - // It completely ignores v1 if ID3v2 is present. - // This goes as far as adding a new v1 tag *even if there already is one* - - // A suspected double-ID3v1 tag has been detected, but it could be that - // the "TAG" identifier is a legitimate part of an APE or Lyrics3 tag - if (substr($preid3v1, 96, 8) == 'APETAGEX') { - // an APE tag footer was found before the last ID3v1, assume false "TAG" synch - } elseif (substr($preid3v1, 119, 6) == 'LYRICS') { - // a Lyrics3 tag footer was found before the last ID3v1, assume false "TAG" synch - } else { - // APE and Lyrics3 footers not found - assume double ID3v1 - $info['warning'][] = 'Duplicate ID3v1 tag detected - this has been known to happen with iTunes'; - $info['avdataend'] -= 128; - } - } - - return true; - } - - public static function cutfield($str) { - return trim(substr($str, 0, strcspn($str, "\x00"))); - } - - public static function ArrayOfGenres($allowSCMPXextended=false) { - static $GenreLookup = array( - 0 => 'Blues', - 1 => 'Classic Rock', - 2 => 'Country', - 3 => 'Dance', - 4 => 'Disco', - 5 => 'Funk', - 6 => 'Grunge', - 7 => 'Hip-Hop', - 8 => 'Jazz', - 9 => 'Metal', - 10 => 'New Age', - 11 => 'Oldies', - 12 => 'Other', - 13 => 'Pop', - 14 => 'R&B', - 15 => 'Rap', - 16 => 'Reggae', - 17 => 'Rock', - 18 => 'Techno', - 19 => 'Industrial', - 20 => 'Alternative', - 21 => 'Ska', - 22 => 'Death Metal', - 23 => 'Pranks', - 24 => 'Soundtrack', - 25 => 'Euro-Techno', - 26 => 'Ambient', - 27 => 'Trip-Hop', - 28 => 'Vocal', - 29 => 'Jazz+Funk', - 30 => 'Fusion', - 31 => 'Trance', - 32 => 'Classical', - 33 => 'Instrumental', - 34 => 'Acid', - 35 => 'House', - 36 => 'Game', - 37 => 'Sound Clip', - 38 => 'Gospel', - 39 => 'Noise', - 40 => 'Alt. Rock', - 41 => 'Bass', - 42 => 'Soul', - 43 => 'Punk', - 44 => 'Space', - 45 => 'Meditative', - 46 => 'Instrumental Pop', - 47 => 'Instrumental Rock', - 48 => 'Ethnic', - 49 => 'Gothic', - 50 => 'Darkwave', - 51 => 'Techno-Industrial', - 52 => 'Electronic', - 53 => 'Pop-Folk', - 54 => 'Eurodance', - 55 => 'Dream', - 56 => 'Southern Rock', - 57 => 'Comedy', - 58 => 'Cult', - 59 => 'Gangsta Rap', - 60 => 'Top 40', - 61 => 'Christian Rap', - 62 => 'Pop/Funk', - 63 => 'Jungle', - 64 => 'Native American', - 65 => 'Cabaret', - 66 => 'New Wave', - 67 => 'Psychedelic', - 68 => 'Rave', - 69 => 'Showtunes', - 70 => 'Trailer', - 71 => 'Lo-Fi', - 72 => 'Tribal', - 73 => 'Acid Punk', - 74 => 'Acid Jazz', - 75 => 'Polka', - 76 => 'Retro', - 77 => 'Musical', - 78 => 'Rock & Roll', - 79 => 'Hard Rock', - 80 => 'Folk', - 81 => 'Folk/Rock', - 82 => 'National Folk', - 83 => 'Swing', - 84 => 'Fast-Fusion', - 85 => 'Bebob', - 86 => 'Latin', - 87 => 'Revival', - 88 => 'Celtic', - 89 => 'Bluegrass', - 90 => 'Avantgarde', - 91 => 'Gothic Rock', - 92 => 'Progressive Rock', - 93 => 'Psychedelic Rock', - 94 => 'Symphonic Rock', - 95 => 'Slow Rock', - 96 => 'Big Band', - 97 => 'Chorus', - 98 => 'Easy Listening', - 99 => 'Acoustic', - 100 => 'Humour', - 101 => 'Speech', - 102 => 'Chanson', - 103 => 'Opera', - 104 => 'Chamber Music', - 105 => 'Sonata', - 106 => 'Symphony', - 107 => 'Booty Bass', - 108 => 'Primus', - 109 => 'Porn Groove', - 110 => 'Satire', - 111 => 'Slow Jam', - 112 => 'Club', - 113 => 'Tango', - 114 => 'Samba', - 115 => 'Folklore', - 116 => 'Ballad', - 117 => 'Power Ballad', - 118 => 'Rhythmic Soul', - 119 => 'Freestyle', - 120 => 'Duet', - 121 => 'Punk Rock', - 122 => 'Drum Solo', - 123 => 'A Cappella', - 124 => 'Euro-House', - 125 => 'Dance Hall', - 126 => 'Goa', - 127 => 'Drum & Bass', - 128 => 'Club-House', - 129 => 'Hardcore', - 130 => 'Terror', - 131 => 'Indie', - 132 => 'BritPop', - 133 => 'Negerpunk', - 134 => 'Polsk Punk', - 135 => 'Beat', - 136 => 'Christian Gangsta Rap', - 137 => 'Heavy Metal', - 138 => 'Black Metal', - 139 => 'Crossover', - 140 => 'Contemporary Christian', - 141 => 'Christian Rock', - 142 => 'Merengue', - 143 => 'Salsa', - 144 => 'Thrash Metal', - 145 => 'Anime', - 146 => 'JPop', - 147 => 'Synthpop', - - 255 => 'Unknown', - - 'CR' => 'Cover', - 'RX' => 'Remix' - ); - - static $GenreLookupSCMPX = array(); - if ($allowSCMPXextended && empty($GenreLookupSCMPX)) { - $GenreLookupSCMPX = $GenreLookup; - // http://www.geocities.co.jp/SiliconValley-Oakland/3664/alittle.html#GenreExtended - // Extended ID3v1 genres invented by SCMPX - // Note that 255 "Japanese Anime" conflicts with standard "Unknown" - $GenreLookupSCMPX[240] = 'Sacred'; - $GenreLookupSCMPX[241] = 'Northern Europe'; - $GenreLookupSCMPX[242] = 'Irish & Scottish'; - $GenreLookupSCMPX[243] = 'Scotland'; - $GenreLookupSCMPX[244] = 'Ethnic Europe'; - $GenreLookupSCMPX[245] = 'Enka'; - $GenreLookupSCMPX[246] = 'Children\'s Song'; - $GenreLookupSCMPX[247] = 'Japanese Sky'; - $GenreLookupSCMPX[248] = 'Japanese Heavy Rock'; - $GenreLookupSCMPX[249] = 'Japanese Doom Rock'; - $GenreLookupSCMPX[250] = 'Japanese J-POP'; - $GenreLookupSCMPX[251] = 'Japanese Seiyu'; - $GenreLookupSCMPX[252] = 'Japanese Ambient Techno'; - $GenreLookupSCMPX[253] = 'Japanese Moemoe'; - $GenreLookupSCMPX[254] = 'Japanese Tokusatsu'; - //$GenreLookupSCMPX[255] = 'Japanese Anime'; - } - - return ($allowSCMPXextended ? $GenreLookupSCMPX : $GenreLookup); - } - - public static function LookupGenreName($genreid, $allowSCMPXextended=true) { - switch ($genreid) { - case 'RX': - case 'CR': - break; - default: - if (!is_numeric($genreid)) { - return false; - } - $genreid = intval($genreid); // to handle 3 or '3' or '03' - break; - } - $GenreLookup = self::ArrayOfGenres($allowSCMPXextended); - return (isset($GenreLookup[$genreid]) ? $GenreLookup[$genreid] : false); - } - - public static function LookupGenreID($genre, $allowSCMPXextended=false) { - $GenreLookup = self::ArrayOfGenres($allowSCMPXextended); - $LowerCaseNoSpaceSearchTerm = strtolower(str_replace(' ', '', $genre)); - foreach ($GenreLookup as $key => $value) { - if (strtolower(str_replace(' ', '', $value)) == $LowerCaseNoSpaceSearchTerm) { - return $key; - } - } - return false; - } - - public static function StandardiseID3v1GenreName($OriginalGenre) { - if (($GenreID = self::LookupGenreID($OriginalGenre)) !== false) { - return self::LookupGenreName($GenreID); - } - return $OriginalGenre; - } - - public static function GenerateID3v1Tag($title, $artist, $album, $year, $genreid, $comment, $track='') { - $ID3v1Tag = 'TAG'; - $ID3v1Tag .= str_pad(trim(substr($title, 0, 30)), 30, "\x00", STR_PAD_RIGHT); - $ID3v1Tag .= str_pad(trim(substr($artist, 0, 30)), 30, "\x00", STR_PAD_RIGHT); - $ID3v1Tag .= str_pad(trim(substr($album, 0, 30)), 30, "\x00", STR_PAD_RIGHT); - $ID3v1Tag .= str_pad(trim(substr($year, 0, 4)), 4, "\x00", STR_PAD_LEFT); - if (!empty($track) && ($track > 0) && ($track <= 255)) { - $ID3v1Tag .= str_pad(trim(substr($comment, 0, 28)), 28, "\x00", STR_PAD_RIGHT); - $ID3v1Tag .= "\x00"; - if (gettype($track) == 'string') { - $track = (int) $track; - } - $ID3v1Tag .= chr($track); - } else { - $ID3v1Tag .= str_pad(trim(substr($comment, 0, 30)), 30, "\x00", STR_PAD_RIGHT); - } - if (($genreid < 0) || ($genreid > 147)) { - $genreid = 255; // 'unknown' genre - } - switch (gettype($genreid)) { - case 'string': - case 'integer': - $ID3v1Tag .= chr(intval($genreid)); - break; - default: - $ID3v1Tag .= chr(255); // 'unknown' genre - break; - } - - return $ID3v1Tag; - } - -} diff --git a/src/Classes/Vendor/getid3/module.tag.id3v2.php b/src/Classes/Vendor/getid3/module.tag.id3v2.php deleted file mode 100755 index eac73d71f..000000000 --- a/src/Classes/Vendor/getid3/module.tag.id3v2.php +++ /dev/null @@ -1,3414 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -/// // -// module.tag.id3v2.php // -// module for analyzing ID3v2 tags // -// dependencies: module.tag.id3v1.php // -// /// -///////////////////////////////////////////////////////////////// - -getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v1.php', __FILE__, true); - -class getid3_id3v2 extends getid3_handler -{ - public $StartingOffset = 0; - - public function Analyze() { - $info = &$this->getid3->info; - - // Overall tag structure: - // +-----------------------------+ - // | Header (10 bytes) | - // +-----------------------------+ - // | Extended Header | - // | (variable length, OPTIONAL) | - // +-----------------------------+ - // | Frames (variable length) | - // +-----------------------------+ - // | Padding | - // | (variable length, OPTIONAL) | - // +-----------------------------+ - // | Footer (10 bytes, OPTIONAL) | - // +-----------------------------+ - - // Header - // ID3v2/file identifier "ID3" - // ID3v2 version $04 00 - // ID3v2 flags (%ab000000 in v2.2, %abc00000 in v2.3, %abcd0000 in v2.4.x) - // ID3v2 size 4 * %0xxxxxxx - - - // shortcuts - $info['id3v2']['header'] = true; - $thisfile_id3v2 = &$info['id3v2']; - $thisfile_id3v2['flags'] = array(); - $thisfile_id3v2_flags = &$thisfile_id3v2['flags']; - - - fseek($this->getid3->fp, $this->StartingOffset, SEEK_SET); - $header = fread($this->getid3->fp, 10); - if (substr($header, 0, 3) == 'ID3' && strlen($header) == 10) { - - $thisfile_id3v2['majorversion'] = ord($header{3}); - $thisfile_id3v2['minorversion'] = ord($header{4}); - - // shortcut - $id3v2_majorversion = &$thisfile_id3v2['majorversion']; - - } else { - - unset($info['id3v2']); - return false; - - } - - if ($id3v2_majorversion > 4) { // this script probably won't correctly parse ID3v2.5.x and above (if it ever exists) - - $info['error'][] = 'this script only parses up to ID3v2.4.x - this tag is ID3v2.'.$id3v2_majorversion.'.'.$thisfile_id3v2['minorversion']; - return false; - - } - - $id3_flags = ord($header{5}); - switch ($id3v2_majorversion) { - case 2: - // %ab000000 in v2.2 - $thisfile_id3v2_flags['unsynch'] = (bool) ($id3_flags & 0x80); // a - Unsynchronisation - $thisfile_id3v2_flags['compression'] = (bool) ($id3_flags & 0x40); // b - Compression - break; - - case 3: - // %abc00000 in v2.3 - $thisfile_id3v2_flags['unsynch'] = (bool) ($id3_flags & 0x80); // a - Unsynchronisation - $thisfile_id3v2_flags['exthead'] = (bool) ($id3_flags & 0x40); // b - Extended header - $thisfile_id3v2_flags['experim'] = (bool) ($id3_flags & 0x20); // c - Experimental indicator - break; - - case 4: - // %abcd0000 in v2.4 - $thisfile_id3v2_flags['unsynch'] = (bool) ($id3_flags & 0x80); // a - Unsynchronisation - $thisfile_id3v2_flags['exthead'] = (bool) ($id3_flags & 0x40); // b - Extended header - $thisfile_id3v2_flags['experim'] = (bool) ($id3_flags & 0x20); // c - Experimental indicator - $thisfile_id3v2_flags['isfooter'] = (bool) ($id3_flags & 0x10); // d - Footer present - break; - } - - $thisfile_id3v2['headerlength'] = getid3_lib::BigEndian2Int(substr($header, 6, 4), 1) + 10; // length of ID3v2 tag in 10-byte header doesn't include 10-byte header length - - $thisfile_id3v2['tag_offset_start'] = $this->StartingOffset; - $thisfile_id3v2['tag_offset_end'] = $thisfile_id3v2['tag_offset_start'] + $thisfile_id3v2['headerlength']; - - - - // create 'encoding' key - used by getid3::HandleAllTags() - // in ID3v2 every field can have it's own encoding type - // so force everything to UTF-8 so it can be handled consistantly - $thisfile_id3v2['encoding'] = 'UTF-8'; - - - // Frames - - // All ID3v2 frames consists of one frame header followed by one or more - // fields containing the actual information. The header is always 10 - // bytes and laid out as follows: - // - // Frame ID $xx xx xx xx (four characters) - // Size 4 * %0xxxxxxx - // Flags $xx xx - - $sizeofframes = $thisfile_id3v2['headerlength'] - 10; // not including 10-byte initial header - if (!empty($thisfile_id3v2['exthead']['length'])) { - $sizeofframes -= ($thisfile_id3v2['exthead']['length'] + 4); - } - if (!empty($thisfile_id3v2_flags['isfooter'])) { - $sizeofframes -= 10; // footer takes last 10 bytes of ID3v2 header, after frame data, before audio - } - if ($sizeofframes > 0) { - - $framedata = fread($this->getid3->fp, $sizeofframes); // read all frames from file into $framedata variable - - // if entire frame data is unsynched, de-unsynch it now (ID3v2.3.x) - if (!empty($thisfile_id3v2_flags['unsynch']) && ($id3v2_majorversion <= 3)) { - $framedata = $this->DeUnsynchronise($framedata); - } - // [in ID3v2.4.0] Unsynchronisation [S:6.1] is done on frame level, instead - // of on tag level, making it easier to skip frames, increasing the streamability - // of the tag. The unsynchronisation flag in the header [S:3.1] indicates that - // there exists an unsynchronised frame, while the new unsynchronisation flag in - // the frame header [S:4.1.2] indicates unsynchronisation. - - - //$framedataoffset = 10 + ($thisfile_id3v2['exthead']['length'] ? $thisfile_id3v2['exthead']['length'] + 4 : 0); // how many bytes into the stream - start from after the 10-byte header (and extended header length+4, if present) - $framedataoffset = 10; // how many bytes into the stream - start from after the 10-byte header - - - // Extended Header - if (!empty($thisfile_id3v2_flags['exthead'])) { - $extended_header_offset = 0; - - if ($id3v2_majorversion == 3) { - - // v2.3 definition: - //Extended header size $xx xx xx xx // 32-bit integer - //Extended Flags $xx xx - // %x0000000 %00000000 // v2.3 - // x - CRC data present - //Size of padding $xx xx xx xx - - $thisfile_id3v2['exthead']['length'] = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 4), 0); - $extended_header_offset += 4; - - $thisfile_id3v2['exthead']['flag_bytes'] = 2; - $thisfile_id3v2['exthead']['flag_raw'] = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, $thisfile_id3v2['exthead']['flag_bytes'])); - $extended_header_offset += $thisfile_id3v2['exthead']['flag_bytes']; - - $thisfile_id3v2['exthead']['flags']['crc'] = (bool) ($thisfile_id3v2['exthead']['flag_raw'] & 0x8000); - - $thisfile_id3v2['exthead']['padding_size'] = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 4)); - $extended_header_offset += 4; - - if ($thisfile_id3v2['exthead']['flags']['crc']) { - $thisfile_id3v2['exthead']['flag_data']['crc'] = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 4)); - $extended_header_offset += 4; - } - $extended_header_offset += $thisfile_id3v2['exthead']['padding_size']; - - } elseif ($id3v2_majorversion == 4) { - - // v2.4 definition: - //Extended header size 4 * %0xxxxxxx // 28-bit synchsafe integer - //Number of flag bytes $01 - //Extended Flags $xx - // %0bcd0000 // v2.4 - // b - Tag is an update - // Flag data length $00 - // c - CRC data present - // Flag data length $05 - // Total frame CRC 5 * %0xxxxxxx - // d - Tag restrictions - // Flag data length $01 - - $thisfile_id3v2['exthead']['length'] = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 4), true); - $extended_header_offset += 4; - - $thisfile_id3v2['exthead']['flag_bytes'] = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 1)); // should always be 1 - $extended_header_offset += 1; - - $thisfile_id3v2['exthead']['flag_raw'] = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, $thisfile_id3v2['exthead']['flag_bytes'])); - $extended_header_offset += $thisfile_id3v2['exthead']['flag_bytes']; - - $thisfile_id3v2['exthead']['flags']['update'] = (bool) ($thisfile_id3v2['exthead']['flag_raw'] & 0x40); - $thisfile_id3v2['exthead']['flags']['crc'] = (bool) ($thisfile_id3v2['exthead']['flag_raw'] & 0x20); - $thisfile_id3v2['exthead']['flags']['restrictions'] = (bool) ($thisfile_id3v2['exthead']['flag_raw'] & 0x10); - - if ($thisfile_id3v2['exthead']['flags']['update']) { - $ext_header_chunk_length = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 1)); // should be 0 - $extended_header_offset += 1; - } - - if ($thisfile_id3v2['exthead']['flags']['crc']) { - $ext_header_chunk_length = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 1)); // should be 5 - $extended_header_offset += 1; - $thisfile_id3v2['exthead']['flag_data']['crc'] = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, $ext_header_chunk_length), true, false); - $extended_header_offset += $ext_header_chunk_length; - } - - if ($thisfile_id3v2['exthead']['flags']['restrictions']) { - $ext_header_chunk_length = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 1)); // should be 1 - $extended_header_offset += 1; - - // %ppqrrstt - $restrictions_raw = getid3_lib::BigEndian2Int(substr($framedata, $extended_header_offset, 1)); - $extended_header_offset += 1; - $thisfile_id3v2['exthead']['flags']['restrictions']['tagsize'] = ($restrictions_raw & 0xC0) >> 6; // p - Tag size restrictions - $thisfile_id3v2['exthead']['flags']['restrictions']['textenc'] = ($restrictions_raw & 0x20) >> 5; // q - Text encoding restrictions - $thisfile_id3v2['exthead']['flags']['restrictions']['textsize'] = ($restrictions_raw & 0x18) >> 3; // r - Text fields size restrictions - $thisfile_id3v2['exthead']['flags']['restrictions']['imgenc'] = ($restrictions_raw & 0x04) >> 2; // s - Image encoding restrictions - $thisfile_id3v2['exthead']['flags']['restrictions']['imgsize'] = ($restrictions_raw & 0x03) >> 0; // t - Image size restrictions - - $thisfile_id3v2['exthead']['flags']['restrictions_text']['tagsize'] = $this->LookupExtendedHeaderRestrictionsTagSizeLimits($thisfile_id3v2['exthead']['flags']['restrictions']['tagsize']); - $thisfile_id3v2['exthead']['flags']['restrictions_text']['textenc'] = $this->LookupExtendedHeaderRestrictionsTextEncodings($thisfile_id3v2['exthead']['flags']['restrictions']['textenc']); - $thisfile_id3v2['exthead']['flags']['restrictions_text']['textsize'] = $this->LookupExtendedHeaderRestrictionsTextFieldSize($thisfile_id3v2['exthead']['flags']['restrictions']['textsize']); - $thisfile_id3v2['exthead']['flags']['restrictions_text']['imgenc'] = $this->LookupExtendedHeaderRestrictionsImageEncoding($thisfile_id3v2['exthead']['flags']['restrictions']['imgenc']); - $thisfile_id3v2['exthead']['flags']['restrictions_text']['imgsize'] = $this->LookupExtendedHeaderRestrictionsImageSizeSize($thisfile_id3v2['exthead']['flags']['restrictions']['imgsize']); - } - - if ($thisfile_id3v2['exthead']['length'] != $extended_header_offset) { - $info['warning'][] = 'ID3v2.4 extended header length mismatch (expecting '.intval($thisfile_id3v2['exthead']['length']).', found '.intval($extended_header_offset).')'; - } - } - - $framedataoffset += $extended_header_offset; - $framedata = substr($framedata, $extended_header_offset); - } // end extended header - - - while (isset($framedata) && (strlen($framedata) > 0)) { // cycle through until no more frame data is left to parse - if (strlen($framedata) <= $this->ID3v2HeaderLength($id3v2_majorversion)) { - // insufficient room left in ID3v2 header for actual data - must be padding - $thisfile_id3v2['padding']['start'] = $framedataoffset; - $thisfile_id3v2['padding']['length'] = strlen($framedata); - $thisfile_id3v2['padding']['valid'] = true; - for ($i = 0; $i < $thisfile_id3v2['padding']['length']; $i++) { - if ($framedata{$i} != "\x00") { - $thisfile_id3v2['padding']['valid'] = false; - $thisfile_id3v2['padding']['errorpos'] = $thisfile_id3v2['padding']['start'] + $i; - $info['warning'][] = 'Invalid ID3v2 padding found at offset '.$thisfile_id3v2['padding']['errorpos'].' (the remaining '.($thisfile_id3v2['padding']['length'] - $i).' bytes are considered invalid)'; - break; - } - } - break; // skip rest of ID3v2 header - } - if ($id3v2_majorversion == 2) { - // Frame ID $xx xx xx (three characters) - // Size $xx xx xx (24-bit integer) - // Flags $xx xx - - $frame_header = substr($framedata, 0, 6); // take next 6 bytes for header - $framedata = substr($framedata, 6); // and leave the rest in $framedata - $frame_name = substr($frame_header, 0, 3); - $frame_size = getid3_lib::BigEndian2Int(substr($frame_header, 3, 3), 0); - $frame_flags = 0; // not used for anything in ID3v2.2, just set to avoid E_NOTICEs - - } elseif ($id3v2_majorversion > 2) { - - // Frame ID $xx xx xx xx (four characters) - // Size $xx xx xx xx (32-bit integer in v2.3, 28-bit synchsafe in v2.4+) - // Flags $xx xx - - $frame_header = substr($framedata, 0, 10); // take next 10 bytes for header - $framedata = substr($framedata, 10); // and leave the rest in $framedata - - $frame_name = substr($frame_header, 0, 4); - if ($id3v2_majorversion == 3) { - $frame_size = getid3_lib::BigEndian2Int(substr($frame_header, 4, 4), 0); // 32-bit integer - } else { // ID3v2.4+ - $frame_size = getid3_lib::BigEndian2Int(substr($frame_header, 4, 4), 1); // 32-bit synchsafe integer (28-bit value) - } - - if ($frame_size < (strlen($framedata) + 4)) { - $nextFrameID = substr($framedata, $frame_size, 4); - if ($this->IsValidID3v2FrameName($nextFrameID, $id3v2_majorversion)) { - // next frame is OK - } elseif (($frame_name == "\x00".'MP3') || ($frame_name == "\x00\x00".'MP') || ($frame_name == ' MP3') || ($frame_name == 'MP3e')) { - // MP3ext known broken frames - "ok" for the purposes of this test - } elseif (($id3v2_majorversion == 4) && ($this->IsValidID3v2FrameName(substr($framedata, getid3_lib::BigEndian2Int(substr($frame_header, 4, 4), 0), 4), 3))) { - $info['warning'][] = 'ID3v2 tag written as ID3v2.4, but with non-synchsafe integers (ID3v2.3 style). Older versions of (Helium2; iTunes) are known culprits of this. Tag has been parsed as ID3v2.3'; - $id3v2_majorversion = 3; - $frame_size = getid3_lib::BigEndian2Int(substr($frame_header, 4, 4), 0); // 32-bit integer - } - } - - - $frame_flags = getid3_lib::BigEndian2Int(substr($frame_header, 8, 2)); - } - - if ((($id3v2_majorversion == 2) && ($frame_name == "\x00\x00\x00")) || ($frame_name == "\x00\x00\x00\x00")) { - // padding encountered - - $thisfile_id3v2['padding']['start'] = $framedataoffset; - $thisfile_id3v2['padding']['length'] = strlen($frame_header) + strlen($framedata); - $thisfile_id3v2['padding']['valid'] = true; - - $len = strlen($framedata); - for ($i = 0; $i < $len; $i++) { - if ($framedata{$i} != "\x00") { - $thisfile_id3v2['padding']['valid'] = false; - $thisfile_id3v2['padding']['errorpos'] = $thisfile_id3v2['padding']['start'] + $i; - $info['warning'][] = 'Invalid ID3v2 padding found at offset '.$thisfile_id3v2['padding']['errorpos'].' (the remaining '.($thisfile_id3v2['padding']['length'] - $i).' bytes are considered invalid)'; - break; - } - } - break; // skip rest of ID3v2 header - } - - if ($frame_name == 'COM ') { - $info['warning'][] = 'error parsing "'.$frame_name.'" ('.$framedataoffset.' bytes into the ID3v2.'.$id3v2_majorversion.' tag). (ERROR: IsValidID3v2FrameName("'.str_replace("\x00", ' ', $frame_name).'", '.$id3v2_majorversion.'))). [Note: this particular error has been known to happen with tags edited by iTunes (versions "X v2.0.3", "v3.0.1" are known-guilty, probably others too)]'; - $frame_name = 'COMM'; - } - if (($frame_size <= strlen($framedata)) && ($this->IsValidID3v2FrameName($frame_name, $id3v2_majorversion))) { - - unset($parsedFrame); - $parsedFrame['frame_name'] = $frame_name; - $parsedFrame['frame_flags_raw'] = $frame_flags; - $parsedFrame['data'] = substr($framedata, 0, $frame_size); - $parsedFrame['datalength'] = getid3_lib::CastAsInt($frame_size); - $parsedFrame['dataoffset'] = $framedataoffset; - - $this->ParseID3v2Frame($parsedFrame); - $thisfile_id3v2[$frame_name][] = $parsedFrame; - - $framedata = substr($framedata, $frame_size); - - } else { // invalid frame length or FrameID - - if ($frame_size <= strlen($framedata)) { - - if ($this->IsValidID3v2FrameName(substr($framedata, $frame_size, 4), $id3v2_majorversion)) { - - // next frame is valid, just skip the current frame - $framedata = substr($framedata, $frame_size); - $info['warning'][] = 'Next ID3v2 frame is valid, skipping current frame.'; - - } else { - - // next frame is invalid too, abort processing - //unset($framedata); - $framedata = null; - $info['error'][] = 'Next ID3v2 frame is also invalid, aborting processing.'; - - } - - } elseif ($frame_size == strlen($framedata)) { - - // this is the last frame, just skip - $info['warning'][] = 'This was the last ID3v2 frame.'; - - } else { - - // next frame is invalid too, abort processing - //unset($framedata); - $framedata = null; - $info['warning'][] = 'Invalid ID3v2 frame size, aborting.'; - - } - if (!$this->IsValidID3v2FrameName($frame_name, $id3v2_majorversion)) { - - switch ($frame_name) { - case "\x00\x00".'MP': - case "\x00".'MP3': - case ' MP3': - case 'MP3e': - case "\x00".'MP': - case ' MP': - case 'MP3': - $info['warning'][] = 'error parsing "'.$frame_name.'" ('.$framedataoffset.' bytes into the ID3v2.'.$id3v2_majorversion.' tag). (ERROR: !IsValidID3v2FrameName("'.str_replace("\x00", ' ', $frame_name).'", '.$id3v2_majorversion.'))). [Note: this particular error has been known to happen with tags edited by "MP3ext (www.mutschler.de/mp3ext/)"]'; - break; - - default: - $info['warning'][] = 'error parsing "'.$frame_name.'" ('.$framedataoffset.' bytes into the ID3v2.'.$id3v2_majorversion.' tag). (ERROR: !IsValidID3v2FrameName("'.str_replace("\x00", ' ', $frame_name).'", '.$id3v2_majorversion.'))).'; - break; - } - - } elseif (!isset($framedata) || ($frame_size > strlen($framedata))) { - - $info['error'][] = 'error parsing "'.$frame_name.'" ('.$framedataoffset.' bytes into the ID3v2.'.$id3v2_majorversion.' tag). (ERROR: $frame_size ('.$frame_size.') > strlen($framedata) ('.(isset($framedata) ? strlen($framedata) : 'null').')).'; - - } else { - - $info['error'][] = 'error parsing "'.$frame_name.'" ('.$framedataoffset.' bytes into the ID3v2.'.$id3v2_majorversion.' tag).'; - - } - - } - $framedataoffset += ($frame_size + $this->ID3v2HeaderLength($id3v2_majorversion)); - - } - - } - - - // Footer - - // The footer is a copy of the header, but with a different identifier. - // ID3v2 identifier "3DI" - // ID3v2 version $04 00 - // ID3v2 flags %abcd0000 - // ID3v2 size 4 * %0xxxxxxx - - if (isset($thisfile_id3v2_flags['isfooter']) && $thisfile_id3v2_flags['isfooter']) { - $footer = fread($this->getid3->fp, 10); - if (substr($footer, 0, 3) == '3DI') { - $thisfile_id3v2['footer'] = true; - $thisfile_id3v2['majorversion_footer'] = ord($footer{3}); - $thisfile_id3v2['minorversion_footer'] = ord($footer{4}); - } - if ($thisfile_id3v2['majorversion_footer'] <= 4) { - $id3_flags = ord(substr($footer{5})); - $thisfile_id3v2_flags['unsynch_footer'] = (bool) ($id3_flags & 0x80); - $thisfile_id3v2_flags['extfoot_footer'] = (bool) ($id3_flags & 0x40); - $thisfile_id3v2_flags['experim_footer'] = (bool) ($id3_flags & 0x20); - $thisfile_id3v2_flags['isfooter_footer'] = (bool) ($id3_flags & 0x10); - - $thisfile_id3v2['footerlength'] = getid3_lib::BigEndian2Int(substr($footer, 6, 4), 1); - } - } // end footer - - if (isset($thisfile_id3v2['comments']['genre'])) { - foreach ($thisfile_id3v2['comments']['genre'] as $key => $value) { - unset($thisfile_id3v2['comments']['genre'][$key]); - $thisfile_id3v2['comments'] = getid3_lib::array_merge_noclobber($thisfile_id3v2['comments'], array('genre'=>$this->ParseID3v2GenreString($value))); - } - } - - if (isset($thisfile_id3v2['comments']['track'])) { - foreach ($thisfile_id3v2['comments']['track'] as $key => $value) { - if (strstr($value, '/')) { - list($thisfile_id3v2['comments']['tracknum'][$key], $thisfile_id3v2['comments']['totaltracks'][$key]) = explode('/', $thisfile_id3v2['comments']['track'][$key]); - } - } - } - - if (!isset($thisfile_id3v2['comments']['year']) && !empty($thisfile_id3v2['comments']['recording_time'][0]) && preg_match('#^([0-9]{4})#', trim($thisfile_id3v2['comments']['recording_time'][0]), $matches)) { - $thisfile_id3v2['comments']['year'] = array($matches[1]); - } - - - if (!empty($thisfile_id3v2['TXXX'])) { - // MediaMonkey does this, maybe others: write a blank RGAD frame, but put replay-gain adjustment values in TXXX frames - foreach ($thisfile_id3v2['TXXX'] as $txxx_array) { - switch ($txxx_array['description']) { - case 'replaygain_track_gain': - if (empty($info['replay_gain']['track']['adjustment']) && !empty($txxx_array['data'])) { - $info['replay_gain']['track']['adjustment'] = floatval(trim(str_replace('dB', '', $txxx_array['data']))); - } - break; - case 'replaygain_track_peak': - if (empty($info['replay_gain']['track']['peak']) && !empty($txxx_array['data'])) { - $info['replay_gain']['track']['peak'] = floatval($txxx_array['data']); - } - break; - case 'replaygain_album_gain': - if (empty($info['replay_gain']['album']['adjustment']) && !empty($txxx_array['data'])) { - $info['replay_gain']['album']['adjustment'] = floatval(trim(str_replace('dB', '', $txxx_array['data']))); - } - break; - } - } - } - - - // Set avdataoffset - $info['avdataoffset'] = $thisfile_id3v2['headerlength']; - if (isset($thisfile_id3v2['footer'])) { - $info['avdataoffset'] += 10; - } - - return true; - } - - - public function ParseID3v2GenreString($genrestring) { - // Parse genres into arrays of genreName and genreID - // ID3v2.2.x, ID3v2.3.x: '(21)' or '(4)Eurodisco' or '(51)(39)' or '(55)((I think...)' - // ID3v2.4.x: '21' $00 'Eurodisco' $00 - $clean_genres = array(); - if (strpos($genrestring, "\x00") === false) { - $genrestring = preg_replace('#\(([0-9]{1,3})\)#', '$1'."\x00", $genrestring); - } - $genre_elements = explode("\x00", $genrestring); - foreach ($genre_elements as $element) { - $element = trim($element); - if ($element) { - if (preg_match('#^[0-9]{1,3}#', $element)) { - $clean_genres[] = getid3_id3v1::LookupGenreName($element); - } else { - $clean_genres[] = str_replace('((', '(', $element); - } - } - } - return $clean_genres; - } - - - public function ParseID3v2Frame(&$parsedFrame) { - - // shortcuts - $info = &$this->getid3->info; - $id3v2_majorversion = $info['id3v2']['majorversion']; - - $parsedFrame['framenamelong'] = $this->FrameNameLongLookup($parsedFrame['frame_name']); - if (empty($parsedFrame['framenamelong'])) { - unset($parsedFrame['framenamelong']); - } - $parsedFrame['framenameshort'] = $this->FrameNameShortLookup($parsedFrame['frame_name']); - if (empty($parsedFrame['framenameshort'])) { - unset($parsedFrame['framenameshort']); - } - - if ($id3v2_majorversion >= 3) { // frame flags are not part of the ID3v2.2 standard - if ($id3v2_majorversion == 3) { - // Frame Header Flags - // %abc00000 %ijk00000 - $parsedFrame['flags']['TagAlterPreservation'] = (bool) ($parsedFrame['frame_flags_raw'] & 0x8000); // a - Tag alter preservation - $parsedFrame['flags']['FileAlterPreservation'] = (bool) ($parsedFrame['frame_flags_raw'] & 0x4000); // b - File alter preservation - $parsedFrame['flags']['ReadOnly'] = (bool) ($parsedFrame['frame_flags_raw'] & 0x2000); // c - Read only - $parsedFrame['flags']['compression'] = (bool) ($parsedFrame['frame_flags_raw'] & 0x0080); // i - Compression - $parsedFrame['flags']['Encryption'] = (bool) ($parsedFrame['frame_flags_raw'] & 0x0040); // j - Encryption - $parsedFrame['flags']['GroupingIdentity'] = (bool) ($parsedFrame['frame_flags_raw'] & 0x0020); // k - Grouping identity - - } elseif ($id3v2_majorversion == 4) { - // Frame Header Flags - // %0abc0000 %0h00kmnp - $parsedFrame['flags']['TagAlterPreservation'] = (bool) ($parsedFrame['frame_flags_raw'] & 0x4000); // a - Tag alter preservation - $parsedFrame['flags']['FileAlterPreservation'] = (bool) ($parsedFrame['frame_flags_raw'] & 0x2000); // b - File alter preservation - $parsedFrame['flags']['ReadOnly'] = (bool) ($parsedFrame['frame_flags_raw'] & 0x1000); // c - Read only - $parsedFrame['flags']['GroupingIdentity'] = (bool) ($parsedFrame['frame_flags_raw'] & 0x0040); // h - Grouping identity - $parsedFrame['flags']['compression'] = (bool) ($parsedFrame['frame_flags_raw'] & 0x0008); // k - Compression - $parsedFrame['flags']['Encryption'] = (bool) ($parsedFrame['frame_flags_raw'] & 0x0004); // m - Encryption - $parsedFrame['flags']['Unsynchronisation'] = (bool) ($parsedFrame['frame_flags_raw'] & 0x0002); // n - Unsynchronisation - $parsedFrame['flags']['DataLengthIndicator'] = (bool) ($parsedFrame['frame_flags_raw'] & 0x0001); // p - Data length indicator - - // Frame-level de-unsynchronisation - ID3v2.4 - if ($parsedFrame['flags']['Unsynchronisation']) { - $parsedFrame['data'] = $this->DeUnsynchronise($parsedFrame['data']); - } - - if ($parsedFrame['flags']['DataLengthIndicator']) { - $parsedFrame['data_length_indicator'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], 0, 4), 1); - $parsedFrame['data'] = substr($parsedFrame['data'], 4); - } - } - - // Frame-level de-compression - if ($parsedFrame['flags']['compression']) { - $parsedFrame['decompressed_size'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], 0, 4)); - if (!function_exists('gzuncompress')) { - $info['warning'][] = 'gzuncompress() support required to decompress ID3v2 frame "'.$parsedFrame['frame_name'].'"'; - } else { - if ($decompresseddata = @gzuncompress(substr($parsedFrame['data'], 4))) { - //if ($decompresseddata = @gzuncompress($parsedFrame['data'])) { - $parsedFrame['data'] = $decompresseddata; - unset($decompresseddata); - } else { - $info['warning'][] = 'gzuncompress() failed on compressed contents of ID3v2 frame "'.$parsedFrame['frame_name'].'"'; - } - } - } - } - - if (!empty($parsedFrame['flags']['DataLengthIndicator'])) { - if ($parsedFrame['data_length_indicator'] != strlen($parsedFrame['data'])) { - $info['warning'][] = 'ID3v2 frame "'.$parsedFrame['frame_name'].'" should be '.$parsedFrame['data_length_indicator'].' bytes long according to DataLengthIndicator, but found '.strlen($parsedFrame['data']).' bytes of data'; - } - } - - if (isset($parsedFrame['datalength']) && ($parsedFrame['datalength'] == 0)) { - - $warning = 'Frame "'.$parsedFrame['frame_name'].'" at offset '.$parsedFrame['dataoffset'].' has no data portion'; - switch ($parsedFrame['frame_name']) { - case 'WCOM': - $warning .= ' (this is known to happen with files tagged by RioPort)'; - break; - - default: - break; - } - $info['warning'][] = $warning; - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'UFID')) || // 4.1 UFID Unique file identifier - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'UFI'))) { // 4.1 UFI Unique file identifier - // There may be more than one 'UFID' frame in a tag, - // but only one with the same 'Owner identifier'. - //
    - // Owner identifier $00 - // Identifier - $exploded = explode("\x00", $parsedFrame['data'], 2); - $parsedFrame['ownerid'] = (isset($exploded[0]) ? $exploded[0] : ''); - $parsedFrame['data'] = (isset($exploded[1]) ? $exploded[1] : ''); - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'TXXX')) || // 4.2.2 TXXX User defined text information frame - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'TXX'))) { // 4.2.2 TXX User defined text information frame - // There may be more than one 'TXXX' frame in each tag, - // but only one with the same description. - //
    - // Text encoding $xx - // Description $00 (00) - // Value - - $frame_offset = 0; - $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - - if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) { - $info['warning'][] = 'Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding'; - } - $frame_terminatorpos = strpos($parsedFrame['data'], $this->TextEncodingTerminatorLookup($frame_textencoding), $frame_offset); - if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding)), 1)) === 0) { - $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 - } - $frame_description = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_description) === 0) { - $frame_description = ''; - } - $parsedFrame['encodingid'] = $frame_textencoding; - $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); - - $parsedFrame['description'] = $frame_description; - $parsedFrame['data'] = substr($parsedFrame['data'], $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding))); - if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) { - $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = trim(getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $parsedFrame['data'])); - } - //unset($parsedFrame['data']); do not unset, may be needed elsewhere, e.g. for replaygain - - - } elseif ($parsedFrame['frame_name']{0} == 'T') { // 4.2. T??[?] Text information frame - // There may only be one text information frame of its kind in an tag. - //
    - // Text encoding $xx - // Information - - $frame_offset = 0; - $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) { - $info['warning'][] = 'Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding'; - } - - $parsedFrame['data'] = (string) substr($parsedFrame['data'], $frame_offset); - - $parsedFrame['encodingid'] = $frame_textencoding; - $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); - - if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) { - // ID3v2.3 specs say that TPE1 (and others) can contain multiple artist values separated with / - // This of course breaks when an aritst name contains slash character, e.g. "AC/DC" - // MP3tag (maybe others) implement alternative system where multiple artists are null-separated, which makes more sense - // getID3 will split null-separated artists into multiple artists and leave slash-separated ones to the user - switch ($parsedFrame['encoding']) { - case 'UTF-16': - case 'UTF-16BE': - case 'UTF-16LE': - $wordsize = 2; - break; - case 'ISO-8859-1': - case 'UTF-8': - default: - $wordsize = 1; - break; - } - $Txxx_elements = array(); - $Txxx_elements_start_offset = 0; - for ($i = 0; $i < strlen($parsedFrame['data']); $i += $wordsize) { - if (substr($parsedFrame['data'], $i, $wordsize) == str_repeat("\x00", $wordsize)) { - $Txxx_elements[] = substr($parsedFrame['data'], $Txxx_elements_start_offset, $i - $Txxx_elements_start_offset); - $Txxx_elements_start_offset = $i + $wordsize; - } - } - $Txxx_elements[] = substr($parsedFrame['data'], $Txxx_elements_start_offset, $i - $Txxx_elements_start_offset); - foreach ($Txxx_elements as $Txxx_element) { - $string = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $Txxx_element); - if (!empty($string)) { - $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = $string; - } - } - unset($string, $wordsize, $i, $Txxx_elements, $Txxx_element, $Txxx_elements_start_offset); - } - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'WXXX')) || // 4.3.2 WXXX User defined URL link frame - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'WXX'))) { // 4.3.2 WXX User defined URL link frame - // There may be more than one 'WXXX' frame in each tag, - // but only one with the same description - //
    - // Text encoding $xx - // Description $00 (00) - // URL - - $frame_offset = 0; - $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) { - $info['warning'][] = 'Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding'; - } - $frame_terminatorpos = strpos($parsedFrame['data'], $this->TextEncodingTerminatorLookup($frame_textencoding), $frame_offset); - if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding)), 1)) === 0) { - $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 - } - $frame_description = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - - if (ord($frame_description) === 0) { - $frame_description = ''; - } - $parsedFrame['data'] = substr($parsedFrame['data'], $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding))); - - $frame_terminatorpos = strpos($parsedFrame['data'], $this->TextEncodingTerminatorLookup($frame_textencoding)); - if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding)), 1)) === 0) { - $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 - } - if ($frame_terminatorpos) { - // there are null bytes after the data - this is not according to spec - // only use data up to first null byte - $frame_urldata = (string) substr($parsedFrame['data'], 0, $frame_terminatorpos); - } else { - // no null bytes following data, just use all data - $frame_urldata = (string) $parsedFrame['data']; - } - - $parsedFrame['encodingid'] = $frame_textencoding; - $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); - - $parsedFrame['url'] = $frame_urldata; - $parsedFrame['description'] = $frame_description; - if (!empty($parsedFrame['framenameshort']) && $parsedFrame['url']) { - $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $parsedFrame['url']); - } - unset($parsedFrame['data']); - - - } elseif ($parsedFrame['frame_name']{0} == 'W') { // 4.3. W??? URL link frames - // There may only be one URL link frame of its kind in a tag, - // except when stated otherwise in the frame description - //
    - // URL - - $parsedFrame['url'] = trim($parsedFrame['data']); - if (!empty($parsedFrame['framenameshort']) && $parsedFrame['url']) { - $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = $parsedFrame['url']; - } - unset($parsedFrame['data']); - - - } elseif ((($id3v2_majorversion == 3) && ($parsedFrame['frame_name'] == 'IPLS')) || // 4.4 IPLS Involved people list (ID3v2.3 only) - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'IPL'))) { // 4.4 IPL Involved people list (ID3v2.2 only) - // http://id3.org/id3v2.3.0#sec4.4 - // There may only be one 'IPL' frame in each tag - //
    - // Text encoding $xx - // People list strings - - $frame_offset = 0; - $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) { - $info['warning'][] = 'Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding'; - } - $parsedFrame['encodingid'] = $frame_textencoding; - $parsedFrame['encoding'] = $this->TextEncodingNameLookup($parsedFrame['encodingid']); - $parsedFrame['data_raw'] = (string) substr($parsedFrame['data'], $frame_offset); - - // http://www.getid3.org/phpBB3/viewtopic.php?t=1369 - // "this tag typically contains null terminated strings, which are associated in pairs" - // "there are users that use the tag incorrectly" - $IPLS_parts = array(); - if (strpos($parsedFrame['data_raw'], "\x00") !== false) { - $IPLS_parts_unsorted = array(); - if (((strlen($parsedFrame['data_raw']) % 2) == 0) && ((substr($parsedFrame['data_raw'], 0, 2) == "\xFF\xFE") || (substr($parsedFrame['data_raw'], 0, 2) == "\xFE\xFF"))) { - // UTF-16, be careful looking for null bytes since most 2-byte characters may contain one; you need to find twin null bytes, and on even padding - $thisILPS = ''; - for ($i = 0; $i < strlen($parsedFrame['data_raw']); $i += 2) { - $twobytes = substr($parsedFrame['data_raw'], $i, 2); - if ($twobytes === "\x00\x00") { - $IPLS_parts_unsorted[] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $thisILPS); - $thisILPS = ''; - } else { - $thisILPS .= $twobytes; - } - } - if (strlen($thisILPS) > 2) { // 2-byte BOM - $IPLS_parts_unsorted[] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $thisILPS); - } - } else { - // ISO-8859-1 or UTF-8 or other single-byte-null character set - $IPLS_parts_unsorted = explode("\x00", $parsedFrame['data_raw']); - } - if (count($IPLS_parts_unsorted) == 1) { - // just a list of names, e.g. "Dino Baptiste, Jimmy Copley, John Gordon, Bernie Marsden, Sharon Watson" - foreach ($IPLS_parts_unsorted as $key => $value) { - $IPLS_parts_sorted = preg_split('#[;,\\r\\n\\t]#', $value); - $position = ''; - foreach ($IPLS_parts_sorted as $person) { - $IPLS_parts[] = array('position'=>$position, 'person'=>$person); - } - } - } elseif ((count($IPLS_parts_unsorted) % 2) == 0) { - $position = ''; - $person = ''; - foreach ($IPLS_parts_unsorted as $key => $value) { - if (($key % 2) == 0) { - $position = $value; - } else { - $person = $value; - $IPLS_parts[] = array('position'=>$position, 'person'=>$person); - $position = ''; - $person = ''; - } - } - } else { - foreach ($IPLS_parts_unsorted as $key => $value) { - $IPLS_parts[] = array($value); - } - } - - } else { - $IPLS_parts = preg_split('#[;,\\r\\n\\t]#', $parsedFrame['data_raw']); - } - $parsedFrame['data'] = $IPLS_parts; - - if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) { - $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = $parsedFrame['data']; - } - - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'MCDI')) || // 4.4 MCDI Music CD identifier - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'MCI'))) { // 4.5 MCI Music CD identifier - // There may only be one 'MCDI' frame in each tag - //
    - // CD TOC - - if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) { - $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = $parsedFrame['data']; - } - - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'ETCO')) || // 4.5 ETCO Event timing codes - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'ETC'))) { // 4.6 ETC Event timing codes - // There may only be one 'ETCO' frame in each tag - //
    - // Time stamp format $xx - // Where time stamp format is: - // $01 (32-bit value) MPEG frames from beginning of file - // $02 (32-bit value) milliseconds from beginning of file - // Followed by a list of key events in the following format: - // Type of event $xx - // Time stamp $xx (xx ...) - // The 'Time stamp' is set to zero if directly at the beginning of the sound - // or after the previous event. All events MUST be sorted in chronological order. - - $frame_offset = 0; - $parsedFrame['timestampformat'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - - while ($frame_offset < strlen($parsedFrame['data'])) { - $parsedFrame['typeid'] = substr($parsedFrame['data'], $frame_offset++, 1); - $parsedFrame['type'] = $this->ETCOEventLookup($parsedFrame['typeid']); - $parsedFrame['timestamp'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4)); - $frame_offset += 4; - } - unset($parsedFrame['data']); - - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'MLLT')) || // 4.6 MLLT MPEG location lookup table - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'MLL'))) { // 4.7 MLL MPEG location lookup table - // There may only be one 'MLLT' frame in each tag - //
    - // MPEG frames between reference $xx xx - // Bytes between reference $xx xx xx - // Milliseconds between reference $xx xx xx - // Bits for bytes deviation $xx - // Bits for milliseconds dev. $xx - // Then for every reference the following data is included; - // Deviation in bytes %xxx.... - // Deviation in milliseconds %xxx.... - - $frame_offset = 0; - $parsedFrame['framesbetweenreferences'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], 0, 2)); - $parsedFrame['bytesbetweenreferences'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], 2, 3)); - $parsedFrame['msbetweenreferences'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], 5, 3)); - $parsedFrame['bitsforbytesdeviation'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], 8, 1)); - $parsedFrame['bitsformsdeviation'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], 9, 1)); - $parsedFrame['data'] = substr($parsedFrame['data'], 10); - while ($frame_offset < strlen($parsedFrame['data'])) { - $deviationbitstream .= getid3_lib::BigEndian2Bin(substr($parsedFrame['data'], $frame_offset++, 1)); - } - $reference_counter = 0; - while (strlen($deviationbitstream) > 0) { - $parsedFrame[$reference_counter]['bytedeviation'] = bindec(substr($deviationbitstream, 0, $parsedFrame['bitsforbytesdeviation'])); - $parsedFrame[$reference_counter]['msdeviation'] = bindec(substr($deviationbitstream, $parsedFrame['bitsforbytesdeviation'], $parsedFrame['bitsformsdeviation'])); - $deviationbitstream = substr($deviationbitstream, $parsedFrame['bitsforbytesdeviation'] + $parsedFrame['bitsformsdeviation']); - $reference_counter++; - } - unset($parsedFrame['data']); - - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'SYTC')) || // 4.7 SYTC Synchronised tempo codes - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'STC'))) { // 4.8 STC Synchronised tempo codes - // There may only be one 'SYTC' frame in each tag - //
    - // Time stamp format $xx - // Tempo data - // Where time stamp format is: - // $01 (32-bit value) MPEG frames from beginning of file - // $02 (32-bit value) milliseconds from beginning of file - - $frame_offset = 0; - $parsedFrame['timestampformat'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $timestamp_counter = 0; - while ($frame_offset < strlen($parsedFrame['data'])) { - $parsedFrame[$timestamp_counter]['tempo'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - if ($parsedFrame[$timestamp_counter]['tempo'] == 255) { - $parsedFrame[$timestamp_counter]['tempo'] += ord(substr($parsedFrame['data'], $frame_offset++, 1)); - } - $parsedFrame[$timestamp_counter]['timestamp'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4)); - $frame_offset += 4; - $timestamp_counter++; - } - unset($parsedFrame['data']); - - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'USLT')) || // 4.8 USLT Unsynchronised lyric/text transcription - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'ULT'))) { // 4.9 ULT Unsynchronised lyric/text transcription - // There may be more than one 'Unsynchronised lyrics/text transcription' frame - // in each tag, but only one with the same language and content descriptor. - //
    - // Text encoding $xx - // Language $xx xx xx - // Content descriptor $00 (00) - // Lyrics/text - - $frame_offset = 0; - $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) { - $info['warning'][] = 'Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding'; - } - $frame_language = substr($parsedFrame['data'], $frame_offset, 3); - $frame_offset += 3; - $frame_terminatorpos = strpos($parsedFrame['data'], $this->TextEncodingTerminatorLookup($frame_textencoding), $frame_offset); - if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding)), 1)) === 0) { - $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 - } - $frame_description = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_description) === 0) { - $frame_description = ''; - } - $parsedFrame['data'] = substr($parsedFrame['data'], $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding))); - - $parsedFrame['encodingid'] = $frame_textencoding; - $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); - - $parsedFrame['data'] = $parsedFrame['data']; - $parsedFrame['language'] = $frame_language; - $parsedFrame['languagename'] = $this->LanguageLookup($frame_language, false); - $parsedFrame['description'] = $frame_description; - if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) { - $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $parsedFrame['data']); - } - unset($parsedFrame['data']); - - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'SYLT')) || // 4.9 SYLT Synchronised lyric/text - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'SLT'))) { // 4.10 SLT Synchronised lyric/text - // There may be more than one 'SYLT' frame in each tag, - // but only one with the same language and content descriptor. - //
    - // Text encoding $xx - // Language $xx xx xx - // Time stamp format $xx - // $01 (32-bit value) MPEG frames from beginning of file - // $02 (32-bit value) milliseconds from beginning of file - // Content type $xx - // Content descriptor $00 (00) - // Terminated text to be synced (typically a syllable) - // Sync identifier (terminator to above string) $00 (00) - // Time stamp $xx (xx ...) - - $frame_offset = 0; - $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) { - $info['warning'][] = 'Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding'; - } - $frame_language = substr($parsedFrame['data'], $frame_offset, 3); - $frame_offset += 3; - $parsedFrame['timestampformat'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $parsedFrame['contenttypeid'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $parsedFrame['contenttype'] = $this->SYTLContentTypeLookup($parsedFrame['contenttypeid']); - $parsedFrame['encodingid'] = $frame_textencoding; - $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); - - $parsedFrame['language'] = $frame_language; - $parsedFrame['languagename'] = $this->LanguageLookup($frame_language, false); - - $timestampindex = 0; - $frame_remainingdata = substr($parsedFrame['data'], $frame_offset); - while (strlen($frame_remainingdata)) { - $frame_offset = 0; - $frame_terminatorpos = strpos($frame_remainingdata, $this->TextEncodingTerminatorLookup($frame_textencoding)); - if ($frame_terminatorpos === false) { - $frame_remainingdata = ''; - } else { - if (ord(substr($frame_remainingdata, $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding)), 1)) === 0) { - $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 - } - $parsedFrame['lyrics'][$timestampindex]['data'] = substr($frame_remainingdata, $frame_offset, $frame_terminatorpos - $frame_offset); - - $frame_remainingdata = substr($frame_remainingdata, $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding))); - if (($timestampindex == 0) && (ord($frame_remainingdata{0}) != 0)) { - // timestamp probably omitted for first data item - } else { - $parsedFrame['lyrics'][$timestampindex]['timestamp'] = getid3_lib::BigEndian2Int(substr($frame_remainingdata, 0, 4)); - $frame_remainingdata = substr($frame_remainingdata, 4); - } - $timestampindex++; - } - } - unset($parsedFrame['data']); - - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'COMM')) || // 4.10 COMM Comments - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'COM'))) { // 4.11 COM Comments - // There may be more than one comment frame in each tag, - // but only one with the same language and content descriptor. - //
    - // Text encoding $xx - // Language $xx xx xx - // Short content descrip. $00 (00) - // The actual text - - if (strlen($parsedFrame['data']) < 5) { - - $info['warning'][] = 'Invalid data (too short) for "'.$parsedFrame['frame_name'].'" frame at offset '.$parsedFrame['dataoffset']; - - } else { - - $frame_offset = 0; - $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) { - $info['warning'][] = 'Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding'; - } - $frame_language = substr($parsedFrame['data'], $frame_offset, 3); - $frame_offset += 3; - $frame_terminatorpos = strpos($parsedFrame['data'], $this->TextEncodingTerminatorLookup($frame_textencoding), $frame_offset); - if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding)), 1)) === 0) { - $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 - } - $frame_description = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_description) === 0) { - $frame_description = ''; - } - $frame_text = (string) substr($parsedFrame['data'], $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding))); - - $parsedFrame['encodingid'] = $frame_textencoding; - $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); - - $parsedFrame['language'] = $frame_language; - $parsedFrame['languagename'] = $this->LanguageLookup($frame_language, false); - $parsedFrame['description'] = $frame_description; - $parsedFrame['data'] = $frame_text; - if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) { - $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $parsedFrame['data']); - } - - } - - } elseif (($id3v2_majorversion >= 4) && ($parsedFrame['frame_name'] == 'RVA2')) { // 4.11 RVA2 Relative volume adjustment (2) (ID3v2.4+ only) - // There may be more than one 'RVA2' frame in each tag, - // but only one with the same identification string - //
    - // Identification $00 - // The 'identification' string is used to identify the situation and/or - // device where this adjustment should apply. The following is then - // repeated for every channel: - // Type of channel $xx - // Volume adjustment $xx xx - // Bits representing peak $xx - // Peak volume $xx (xx ...) - - $frame_terminatorpos = strpos($parsedFrame['data'], "\x00"); - $frame_idstring = substr($parsedFrame['data'], 0, $frame_terminatorpos); - if (ord($frame_idstring) === 0) { - $frame_idstring = ''; - } - $frame_remainingdata = substr($parsedFrame['data'], $frame_terminatorpos + strlen("\x00")); - $parsedFrame['description'] = $frame_idstring; - $RVA2channelcounter = 0; - while (strlen($frame_remainingdata) >= 5) { - $frame_offset = 0; - $frame_channeltypeid = ord(substr($frame_remainingdata, $frame_offset++, 1)); - $parsedFrame[$RVA2channelcounter]['channeltypeid'] = $frame_channeltypeid; - $parsedFrame[$RVA2channelcounter]['channeltype'] = $this->RVA2ChannelTypeLookup($frame_channeltypeid); - $parsedFrame[$RVA2channelcounter]['volumeadjust'] = getid3_lib::BigEndian2Int(substr($frame_remainingdata, $frame_offset, 2), false, true); // 16-bit signed - $frame_offset += 2; - $parsedFrame[$RVA2channelcounter]['bitspeakvolume'] = ord(substr($frame_remainingdata, $frame_offset++, 1)); - if (($parsedFrame[$RVA2channelcounter]['bitspeakvolume'] < 1) || ($parsedFrame[$RVA2channelcounter]['bitspeakvolume'] > 4)) { - $info['warning'][] = 'ID3v2::RVA2 frame['.$RVA2channelcounter.'] contains invalid '.$parsedFrame[$RVA2channelcounter]['bitspeakvolume'].'-byte bits-representing-peak value'; - break; - } - $frame_bytespeakvolume = ceil($parsedFrame[$RVA2channelcounter]['bitspeakvolume'] / 8); - $parsedFrame[$RVA2channelcounter]['peakvolume'] = getid3_lib::BigEndian2Int(substr($frame_remainingdata, $frame_offset, $frame_bytespeakvolume)); - $frame_remainingdata = substr($frame_remainingdata, $frame_offset + $frame_bytespeakvolume); - $RVA2channelcounter++; - } - unset($parsedFrame['data']); - - - } elseif ((($id3v2_majorversion == 3) && ($parsedFrame['frame_name'] == 'RVAD')) || // 4.12 RVAD Relative volume adjustment (ID3v2.3 only) - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'RVA'))) { // 4.12 RVA Relative volume adjustment (ID3v2.2 only) - // There may only be one 'RVA' frame in each tag - //
    - // ID3v2.2 => Increment/decrement %000000ba - // ID3v2.3 => Increment/decrement %00fedcba - // Bits used for volume descr. $xx - // Relative volume change, right $xx xx (xx ...) // a - // Relative volume change, left $xx xx (xx ...) // b - // Peak volume right $xx xx (xx ...) - // Peak volume left $xx xx (xx ...) - // ID3v2.3 only, optional (not present in ID3v2.2): - // Relative volume change, right back $xx xx (xx ...) // c - // Relative volume change, left back $xx xx (xx ...) // d - // Peak volume right back $xx xx (xx ...) - // Peak volume left back $xx xx (xx ...) - // ID3v2.3 only, optional (not present in ID3v2.2): - // Relative volume change, center $xx xx (xx ...) // e - // Peak volume center $xx xx (xx ...) - // ID3v2.3 only, optional (not present in ID3v2.2): - // Relative volume change, bass $xx xx (xx ...) // f - // Peak volume bass $xx xx (xx ...) - - $frame_offset = 0; - $frame_incrdecrflags = getid3_lib::BigEndian2Bin(substr($parsedFrame['data'], $frame_offset++, 1)); - $parsedFrame['incdec']['right'] = (bool) substr($frame_incrdecrflags, 6, 1); - $parsedFrame['incdec']['left'] = (bool) substr($frame_incrdecrflags, 7, 1); - $parsedFrame['bitsvolume'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $frame_bytesvolume = ceil($parsedFrame['bitsvolume'] / 8); - $parsedFrame['volumechange']['right'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume)); - if ($parsedFrame['incdec']['right'] === false) { - $parsedFrame['volumechange']['right'] *= -1; - } - $frame_offset += $frame_bytesvolume; - $parsedFrame['volumechange']['left'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume)); - if ($parsedFrame['incdec']['left'] === false) { - $parsedFrame['volumechange']['left'] *= -1; - } - $frame_offset += $frame_bytesvolume; - $parsedFrame['peakvolume']['right'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume)); - $frame_offset += $frame_bytesvolume; - $parsedFrame['peakvolume']['left'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume)); - $frame_offset += $frame_bytesvolume; - if ($id3v2_majorversion == 3) { - $parsedFrame['data'] = substr($parsedFrame['data'], $frame_offset); - if (strlen($parsedFrame['data']) > 0) { - $parsedFrame['incdec']['rightrear'] = (bool) substr($frame_incrdecrflags, 4, 1); - $parsedFrame['incdec']['leftrear'] = (bool) substr($frame_incrdecrflags, 5, 1); - $parsedFrame['volumechange']['rightrear'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume)); - if ($parsedFrame['incdec']['rightrear'] === false) { - $parsedFrame['volumechange']['rightrear'] *= -1; - } - $frame_offset += $frame_bytesvolume; - $parsedFrame['volumechange']['leftrear'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume)); - if ($parsedFrame['incdec']['leftrear'] === false) { - $parsedFrame['volumechange']['leftrear'] *= -1; - } - $frame_offset += $frame_bytesvolume; - $parsedFrame['peakvolume']['rightrear'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume)); - $frame_offset += $frame_bytesvolume; - $parsedFrame['peakvolume']['leftrear'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume)); - $frame_offset += $frame_bytesvolume; - } - $parsedFrame['data'] = substr($parsedFrame['data'], $frame_offset); - if (strlen($parsedFrame['data']) > 0) { - $parsedFrame['incdec']['center'] = (bool) substr($frame_incrdecrflags, 3, 1); - $parsedFrame['volumechange']['center'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume)); - if ($parsedFrame['incdec']['center'] === false) { - $parsedFrame['volumechange']['center'] *= -1; - } - $frame_offset += $frame_bytesvolume; - $parsedFrame['peakvolume']['center'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume)); - $frame_offset += $frame_bytesvolume; - } - $parsedFrame['data'] = substr($parsedFrame['data'], $frame_offset); - if (strlen($parsedFrame['data']) > 0) { - $parsedFrame['incdec']['bass'] = (bool) substr($frame_incrdecrflags, 2, 1); - $parsedFrame['volumechange']['bass'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume)); - if ($parsedFrame['incdec']['bass'] === false) { - $parsedFrame['volumechange']['bass'] *= -1; - } - $frame_offset += $frame_bytesvolume; - $parsedFrame['peakvolume']['bass'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesvolume)); - $frame_offset += $frame_bytesvolume; - } - } - unset($parsedFrame['data']); - - - } elseif (($id3v2_majorversion >= 4) && ($parsedFrame['frame_name'] == 'EQU2')) { // 4.12 EQU2 Equalisation (2) (ID3v2.4+ only) - // There may be more than one 'EQU2' frame in each tag, - // but only one with the same identification string - //
    - // Interpolation method $xx - // $00 Band - // $01 Linear - // Identification $00 - // The following is then repeated for every adjustment point - // Frequency $xx xx - // Volume adjustment $xx xx - - $frame_offset = 0; - $frame_interpolationmethod = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); - $frame_idstring = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_idstring) === 0) { - $frame_idstring = ''; - } - $parsedFrame['description'] = $frame_idstring; - $frame_remainingdata = substr($parsedFrame['data'], $frame_terminatorpos + strlen("\x00")); - while (strlen($frame_remainingdata)) { - $frame_frequency = getid3_lib::BigEndian2Int(substr($frame_remainingdata, 0, 2)) / 2; - $parsedFrame['data'][$frame_frequency] = getid3_lib::BigEndian2Int(substr($frame_remainingdata, 2, 2), false, true); - $frame_remainingdata = substr($frame_remainingdata, 4); - } - $parsedFrame['interpolationmethod'] = $frame_interpolationmethod; - unset($parsedFrame['data']); - - - } elseif ((($id3v2_majorversion == 3) && ($parsedFrame['frame_name'] == 'EQUA')) || // 4.12 EQUA Equalisation (ID3v2.3 only) - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'EQU'))) { // 4.13 EQU Equalisation (ID3v2.2 only) - // There may only be one 'EQUA' frame in each tag - //
    - // Adjustment bits $xx - // This is followed by 2 bytes + ('adjustment bits' rounded up to the - // nearest byte) for every equalisation band in the following format, - // giving a frequency range of 0 - 32767Hz: - // Increment/decrement %x (MSB of the Frequency) - // Frequency (lower 15 bits) - // Adjustment $xx (xx ...) - - $frame_offset = 0; - $parsedFrame['adjustmentbits'] = substr($parsedFrame['data'], $frame_offset++, 1); - $frame_adjustmentbytes = ceil($parsedFrame['adjustmentbits'] / 8); - - $frame_remainingdata = (string) substr($parsedFrame['data'], $frame_offset); - while (strlen($frame_remainingdata) > 0) { - $frame_frequencystr = getid3_lib::BigEndian2Bin(substr($frame_remainingdata, 0, 2)); - $frame_incdec = (bool) substr($frame_frequencystr, 0, 1); - $frame_frequency = bindec(substr($frame_frequencystr, 1, 15)); - $parsedFrame[$frame_frequency]['incdec'] = $frame_incdec; - $parsedFrame[$frame_frequency]['adjustment'] = getid3_lib::BigEndian2Int(substr($frame_remainingdata, 2, $frame_adjustmentbytes)); - if ($parsedFrame[$frame_frequency]['incdec'] === false) { - $parsedFrame[$frame_frequency]['adjustment'] *= -1; - } - $frame_remainingdata = substr($frame_remainingdata, 2 + $frame_adjustmentbytes); - } - unset($parsedFrame['data']); - - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'RVRB')) || // 4.13 RVRB Reverb - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'REV'))) { // 4.14 REV Reverb - // There may only be one 'RVRB' frame in each tag. - //
    - // Reverb left (ms) $xx xx - // Reverb right (ms) $xx xx - // Reverb bounces, left $xx - // Reverb bounces, right $xx - // Reverb feedback, left to left $xx - // Reverb feedback, left to right $xx - // Reverb feedback, right to right $xx - // Reverb feedback, right to left $xx - // Premix left to right $xx - // Premix right to left $xx - - $frame_offset = 0; - $parsedFrame['left'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 2)); - $frame_offset += 2; - $parsedFrame['right'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 2)); - $frame_offset += 2; - $parsedFrame['bouncesL'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $parsedFrame['bouncesR'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $parsedFrame['feedbackLL'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $parsedFrame['feedbackLR'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $parsedFrame['feedbackRR'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $parsedFrame['feedbackRL'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $parsedFrame['premixLR'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $parsedFrame['premixRL'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - unset($parsedFrame['data']); - - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'APIC')) || // 4.14 APIC Attached picture - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'PIC'))) { // 4.15 PIC Attached picture - // There may be several pictures attached to one file, - // each in their individual 'APIC' frame, but only one - // with the same content descriptor - //
    - // Text encoding $xx - // ID3v2.3+ => MIME type $00 - // ID3v2.2 => Image format $xx xx xx - // Picture type $xx - // Description $00 (00) - // Picture data - - $frame_offset = 0; - $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) { - $info['warning'][] = 'Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding'; - } - - if ($id3v2_majorversion == 2 && strlen($parsedFrame['data']) > $frame_offset) { - $frame_imagetype = substr($parsedFrame['data'], $frame_offset, 3); - if (strtolower($frame_imagetype) == 'ima') { - // complete hack for mp3Rage (www.chaoticsoftware.com) that puts ID3v2.3-formatted - // MIME type instead of 3-char ID3v2.2-format image type (thanks xbhoffØpacbell*net) - $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); - $frame_mimetype = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_mimetype) === 0) { - $frame_mimetype = ''; - } - $frame_imagetype = strtoupper(str_replace('image/', '', strtolower($frame_mimetype))); - if ($frame_imagetype == 'JPEG') { - $frame_imagetype = 'JPG'; - } - $frame_offset = $frame_terminatorpos + strlen("\x00"); - } else { - $frame_offset += 3; - } - } - if ($id3v2_majorversion > 2 && strlen($parsedFrame['data']) > $frame_offset) { - $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); - $frame_mimetype = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_mimetype) === 0) { - $frame_mimetype = ''; - } - $frame_offset = $frame_terminatorpos + strlen("\x00"); - } - - $frame_picturetype = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - - if ($frame_offset >= $parsedFrame['datalength']) { - $info['warning'][] = 'data portion of APIC frame is missing at offset '.($parsedFrame['dataoffset'] + 8 + $frame_offset); - } else { - $frame_terminatorpos = strpos($parsedFrame['data'], $this->TextEncodingTerminatorLookup($frame_textencoding), $frame_offset); - if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding)), 1)) === 0) { - $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 - } - $frame_description = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_description) === 0) { - $frame_description = ''; - } - $parsedFrame['encodingid'] = $frame_textencoding; - $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); - - if ($id3v2_majorversion == 2) { - $parsedFrame['imagetype'] = $frame_imagetype; - } else { - $parsedFrame['mime'] = $frame_mimetype; - } - $parsedFrame['picturetypeid'] = $frame_picturetype; - $parsedFrame['picturetype'] = $this->APICPictureTypeLookup($frame_picturetype); - $parsedFrame['description'] = $frame_description; - $parsedFrame['data'] = substr($parsedFrame['data'], $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding))); - $parsedFrame['datalength'] = strlen($parsedFrame['data']); - - $parsedFrame['image_mime'] = ''; - $imageinfo = array(); - $imagechunkcheck = getid3_lib::GetDataImageSize($parsedFrame['data'], $imageinfo); - if (($imagechunkcheck[2] >= 1) && ($imagechunkcheck[2] <= 3)) { - $parsedFrame['image_mime'] = 'image/'.getid3_lib::ImageTypesLookup($imagechunkcheck[2]); - if ($imagechunkcheck[0]) { - $parsedFrame['image_width'] = $imagechunkcheck[0]; - } - if ($imagechunkcheck[1]) { - $parsedFrame['image_height'] = $imagechunkcheck[1]; - } - } - - do { - if ($this->getid3->option_save_attachments === false) { - // skip entirely - unset($parsedFrame['data']); - break; - } - if ($this->getid3->option_save_attachments === true) { - // great -/* - } elseif (is_int($this->getid3->option_save_attachments)) { - if ($this->getid3->option_save_attachments < $parsedFrame['data_length']) { - // too big, skip - $info['warning'][] = 'attachment at '.$frame_offset.' is too large to process inline ('.number_format($parsedFrame['data_length']).' bytes)'; - unset($parsedFrame['data']); - break; - } -*/ - } elseif (is_string($this->getid3->option_save_attachments)) { - $dir = rtrim(str_replace(array('/', '\\'), DIRECTORY_SEPARATOR, $this->getid3->option_save_attachments), DIRECTORY_SEPARATOR); - if (!is_dir($dir) || !is_writable($dir)) { - // cannot write, skip - $info['warning'][] = 'attachment at '.$frame_offset.' cannot be saved to "'.$dir.'" (not writable)'; - unset($parsedFrame['data']); - break; - } - } - // if we get this far, must be OK - if (is_string($this->getid3->option_save_attachments)) { - $destination_filename = $dir.DIRECTORY_SEPARATOR.md5($info['filenamepath']).'_'.$frame_offset; - if (!file_exists($destination_filename) || is_writable($destination_filename)) { - file_put_contents($destination_filename, $parsedFrame['data']); - } else { - $info['warning'][] = 'attachment at '.$frame_offset.' cannot be saved to "'.$destination_filename.'" (not writable)'; - } - $parsedFrame['data_filename'] = $destination_filename; - unset($parsedFrame['data']); - } else { - if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) { - if (!isset($info['id3v2']['comments']['picture'])) { - $info['id3v2']['comments']['picture'] = array(); - } - $info['id3v2']['comments']['picture'][] = array('data'=>$parsedFrame['data'], 'image_mime'=>$parsedFrame['image_mime']); - } - } - } while (false); - } - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'GEOB')) || // 4.15 GEOB General encapsulated object - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'GEO'))) { // 4.16 GEO General encapsulated object - // There may be more than one 'GEOB' frame in each tag, - // but only one with the same content descriptor - //
    - // Text encoding $xx - // MIME type $00 - // Filename $00 (00) - // Content description $00 (00) - // Encapsulated object - - $frame_offset = 0; - $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) { - $info['warning'][] = 'Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding'; - } - $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); - $frame_mimetype = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_mimetype) === 0) { - $frame_mimetype = ''; - } - $frame_offset = $frame_terminatorpos + strlen("\x00"); - - $frame_terminatorpos = strpos($parsedFrame['data'], $this->TextEncodingTerminatorLookup($frame_textencoding), $frame_offset); - if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding)), 1)) === 0) { - $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 - } - $frame_filename = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_filename) === 0) { - $frame_filename = ''; - } - $frame_offset = $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding)); - - $frame_terminatorpos = strpos($parsedFrame['data'], $this->TextEncodingTerminatorLookup($frame_textencoding), $frame_offset); - if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding)), 1)) === 0) { - $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 - } - $frame_description = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_description) === 0) { - $frame_description = ''; - } - $frame_offset = $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding)); - - $parsedFrame['objectdata'] = (string) substr($parsedFrame['data'], $frame_offset); - $parsedFrame['encodingid'] = $frame_textencoding; - $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); - - $parsedFrame['mime'] = $frame_mimetype; - $parsedFrame['filename'] = $frame_filename; - $parsedFrame['description'] = $frame_description; - unset($parsedFrame['data']); - - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'PCNT')) || // 4.16 PCNT Play counter - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'CNT'))) { // 4.17 CNT Play counter - // There may only be one 'PCNT' frame in each tag. - // When the counter reaches all one's, one byte is inserted in - // front of the counter thus making the counter eight bits bigger - //
    - // Counter $xx xx xx xx (xx ...) - - $parsedFrame['data'] = getid3_lib::BigEndian2Int($parsedFrame['data']); - - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'POPM')) || // 4.17 POPM Popularimeter - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'POP'))) { // 4.18 POP Popularimeter - // There may be more than one 'POPM' frame in each tag, - // but only one with the same email address - //
    - // Email to user $00 - // Rating $xx - // Counter $xx xx xx xx (xx ...) - - $frame_offset = 0; - $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); - $frame_emailaddress = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_emailaddress) === 0) { - $frame_emailaddress = ''; - } - $frame_offset = $frame_terminatorpos + strlen("\x00"); - $frame_rating = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $parsedFrame['counter'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset)); - $parsedFrame['email'] = $frame_emailaddress; - $parsedFrame['rating'] = $frame_rating; - unset($parsedFrame['data']); - - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'RBUF')) || // 4.18 RBUF Recommended buffer size - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'BUF'))) { // 4.19 BUF Recommended buffer size - // There may only be one 'RBUF' frame in each tag - //
    - // Buffer size $xx xx xx - // Embedded info flag %0000000x - // Offset to next tag $xx xx xx xx - - $frame_offset = 0; - $parsedFrame['buffersize'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 3)); - $frame_offset += 3; - - $frame_embeddedinfoflags = getid3_lib::BigEndian2Bin(substr($parsedFrame['data'], $frame_offset++, 1)); - $parsedFrame['flags']['embededinfo'] = (bool) substr($frame_embeddedinfoflags, 7, 1); - $parsedFrame['nexttagoffset'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4)); - unset($parsedFrame['data']); - - - } elseif (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'CRM')) { // 4.20 Encrypted meta frame (ID3v2.2 only) - // There may be more than one 'CRM' frame in a tag, - // but only one with the same 'owner identifier' - //
    - // Owner identifier $00 (00) - // Content/explanation $00 (00) - // Encrypted datablock - - $frame_offset = 0; - $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); - $frame_ownerid = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - $frame_offset = $frame_terminatorpos + strlen("\x00"); - - $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); - $frame_description = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_description) === 0) { - $frame_description = ''; - } - $frame_offset = $frame_terminatorpos + strlen("\x00"); - - $parsedFrame['ownerid'] = $frame_ownerid; - $parsedFrame['data'] = (string) substr($parsedFrame['data'], $frame_offset); - $parsedFrame['description'] = $frame_description; - unset($parsedFrame['data']); - - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'AENC')) || // 4.19 AENC Audio encryption - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'CRA'))) { // 4.21 CRA Audio encryption - // There may be more than one 'AENC' frames in a tag, - // but only one with the same 'Owner identifier' - //
    - // Owner identifier $00 - // Preview start $xx xx - // Preview length $xx xx - // Encryption info - - $frame_offset = 0; - $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); - $frame_ownerid = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_ownerid) === 0) { - $frame_ownerid == ''; - } - $frame_offset = $frame_terminatorpos + strlen("\x00"); - $parsedFrame['ownerid'] = $frame_ownerid; - $parsedFrame['previewstart'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 2)); - $frame_offset += 2; - $parsedFrame['previewlength'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 2)); - $frame_offset += 2; - $parsedFrame['encryptioninfo'] = (string) substr($parsedFrame['data'], $frame_offset); - unset($parsedFrame['data']); - - - } elseif ((($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'LINK')) || // 4.20 LINK Linked information - (($id3v2_majorversion == 2) && ($parsedFrame['frame_name'] == 'LNK'))) { // 4.22 LNK Linked information - // There may be more than one 'LINK' frame in a tag, - // but only one with the same contents - //
    - // ID3v2.3+ => Frame identifier $xx xx xx xx - // ID3v2.2 => Frame identifier $xx xx xx - // URL $00 - // ID and additional data - - $frame_offset = 0; - if ($id3v2_majorversion == 2) { - $parsedFrame['frameid'] = substr($parsedFrame['data'], $frame_offset, 3); - $frame_offset += 3; - } else { - $parsedFrame['frameid'] = substr($parsedFrame['data'], $frame_offset, 4); - $frame_offset += 4; - } - - $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); - $frame_url = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_url) === 0) { - $frame_url = ''; - } - $frame_offset = $frame_terminatorpos + strlen("\x00"); - $parsedFrame['url'] = $frame_url; - - $parsedFrame['additionaldata'] = (string) substr($parsedFrame['data'], $frame_offset); - if (!empty($parsedFrame['framenameshort']) && $parsedFrame['url']) { - $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = utf8_encode($parsedFrame['url']); - } - unset($parsedFrame['data']); - - - } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'POSS')) { // 4.21 POSS Position synchronisation frame (ID3v2.3+ only) - // There may only be one 'POSS' frame in each tag - //
    - // Time stamp format $xx - // Position $xx (xx ...) - - $frame_offset = 0; - $parsedFrame['timestampformat'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $parsedFrame['position'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset)); - unset($parsedFrame['data']); - - - } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'USER')) { // 4.22 USER Terms of use (ID3v2.3+ only) - // There may be more than one 'Terms of use' frame in a tag, - // but only one with the same 'Language' - //
    - // Text encoding $xx - // Language $xx xx xx - // The actual text - - $frame_offset = 0; - $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) { - $info['warning'][] = 'Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding'; - } - $frame_language = substr($parsedFrame['data'], $frame_offset, 3); - $frame_offset += 3; - $parsedFrame['language'] = $frame_language; - $parsedFrame['languagename'] = $this->LanguageLookup($frame_language, false); - $parsedFrame['encodingid'] = $frame_textencoding; - $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); - - $parsedFrame['data'] = (string) substr($parsedFrame['data'], $frame_offset); - if (!empty($parsedFrame['framenameshort']) && !empty($parsedFrame['data'])) { - $info['id3v2']['comments'][$parsedFrame['framenameshort']][] = getid3_lib::iconv_fallback($parsedFrame['encoding'], $info['id3v2']['encoding'], $parsedFrame['data']); - } - unset($parsedFrame['data']); - - - } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'OWNE')) { // 4.23 OWNE Ownership frame (ID3v2.3+ only) - // There may only be one 'OWNE' frame in a tag - //
    - // Text encoding $xx - // Price paid $00 - // Date of purch. - // Seller - - $frame_offset = 0; - $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) { - $info['warning'][] = 'Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding'; - } - $parsedFrame['encodingid'] = $frame_textencoding; - $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); - - $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); - $frame_pricepaid = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - $frame_offset = $frame_terminatorpos + strlen("\x00"); - - $parsedFrame['pricepaid']['currencyid'] = substr($frame_pricepaid, 0, 3); - $parsedFrame['pricepaid']['currency'] = $this->LookupCurrencyUnits($parsedFrame['pricepaid']['currencyid']); - $parsedFrame['pricepaid']['value'] = substr($frame_pricepaid, 3); - - $parsedFrame['purchasedate'] = substr($parsedFrame['data'], $frame_offset, 8); - if (!$this->IsValidDateStampString($parsedFrame['purchasedate'])) { - $parsedFrame['purchasedateunix'] = mktime (0, 0, 0, substr($parsedFrame['purchasedate'], 4, 2), substr($parsedFrame['purchasedate'], 6, 2), substr($parsedFrame['purchasedate'], 0, 4)); - } - $frame_offset += 8; - - $parsedFrame['seller'] = (string) substr($parsedFrame['data'], $frame_offset); - unset($parsedFrame['data']); - - - } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'COMR')) { // 4.24 COMR Commercial frame (ID3v2.3+ only) - // There may be more than one 'commercial frame' in a tag, - // but no two may be identical - //
    - // Text encoding $xx - // Price string $00 - // Valid until - // Contact URL $00 - // Received as $xx - // Name of seller $00 (00) - // Description $00 (00) - // Picture MIME type $00 - // Seller logo - - $frame_offset = 0; - $frame_textencoding = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - if ((($id3v2_majorversion <= 3) && ($frame_textencoding > 1)) || (($id3v2_majorversion == 4) && ($frame_textencoding > 3))) { - $info['warning'][] = 'Invalid text encoding byte ('.$frame_textencoding.') in frame "'.$parsedFrame['frame_name'].'" - defaulting to ISO-8859-1 encoding'; - } - - $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); - $frame_pricestring = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - $frame_offset = $frame_terminatorpos + strlen("\x00"); - $frame_rawpricearray = explode('/', $frame_pricestring); - foreach ($frame_rawpricearray as $key => $val) { - $frame_currencyid = substr($val, 0, 3); - $parsedFrame['price'][$frame_currencyid]['currency'] = $this->LookupCurrencyUnits($frame_currencyid); - $parsedFrame['price'][$frame_currencyid]['value'] = substr($val, 3); - } - - $frame_datestring = substr($parsedFrame['data'], $frame_offset, 8); - $frame_offset += 8; - - $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); - $frame_contacturl = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - $frame_offset = $frame_terminatorpos + strlen("\x00"); - - $frame_receivedasid = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - - $frame_terminatorpos = strpos($parsedFrame['data'], $this->TextEncodingTerminatorLookup($frame_textencoding), $frame_offset); - if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding)), 1)) === 0) { - $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 - } - $frame_sellername = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_sellername) === 0) { - $frame_sellername = ''; - } - $frame_offset = $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding)); - - $frame_terminatorpos = strpos($parsedFrame['data'], $this->TextEncodingTerminatorLookup($frame_textencoding), $frame_offset); - if (ord(substr($parsedFrame['data'], $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding)), 1)) === 0) { - $frame_terminatorpos++; // strpos() fooled because 2nd byte of Unicode chars are often 0x00 - } - $frame_description = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_description) === 0) { - $frame_description = ''; - } - $frame_offset = $frame_terminatorpos + strlen($this->TextEncodingTerminatorLookup($frame_textencoding)); - - $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); - $frame_mimetype = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - $frame_offset = $frame_terminatorpos + strlen("\x00"); - - $frame_sellerlogo = substr($parsedFrame['data'], $frame_offset); - - $parsedFrame['encodingid'] = $frame_textencoding; - $parsedFrame['encoding'] = $this->TextEncodingNameLookup($frame_textencoding); - - $parsedFrame['pricevaliduntil'] = $frame_datestring; - $parsedFrame['contacturl'] = $frame_contacturl; - $parsedFrame['receivedasid'] = $frame_receivedasid; - $parsedFrame['receivedas'] = $this->COMRReceivedAsLookup($frame_receivedasid); - $parsedFrame['sellername'] = $frame_sellername; - $parsedFrame['description'] = $frame_description; - $parsedFrame['mime'] = $frame_mimetype; - $parsedFrame['logo'] = $frame_sellerlogo; - unset($parsedFrame['data']); - - - } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'ENCR')) { // 4.25 ENCR Encryption method registration (ID3v2.3+ only) - // There may be several 'ENCR' frames in a tag, - // but only one containing the same symbol - // and only one containing the same owner identifier - //
    - // Owner identifier $00 - // Method symbol $xx - // Encryption data - - $frame_offset = 0; - $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); - $frame_ownerid = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_ownerid) === 0) { - $frame_ownerid = ''; - } - $frame_offset = $frame_terminatorpos + strlen("\x00"); - - $parsedFrame['ownerid'] = $frame_ownerid; - $parsedFrame['methodsymbol'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $parsedFrame['data'] = (string) substr($parsedFrame['data'], $frame_offset); - - - } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'GRID')) { // 4.26 GRID Group identification registration (ID3v2.3+ only) - - // There may be several 'GRID' frames in a tag, - // but only one containing the same symbol - // and only one containing the same owner identifier - //
    - // Owner identifier $00 - // Group symbol $xx - // Group dependent data - - $frame_offset = 0; - $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); - $frame_ownerid = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_ownerid) === 0) { - $frame_ownerid = ''; - } - $frame_offset = $frame_terminatorpos + strlen("\x00"); - - $parsedFrame['ownerid'] = $frame_ownerid; - $parsedFrame['groupsymbol'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $parsedFrame['data'] = (string) substr($parsedFrame['data'], $frame_offset); - - - } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'PRIV')) { // 4.27 PRIV Private frame (ID3v2.3+ only) - // The tag may contain more than one 'PRIV' frame - // but only with different contents - //
    - // Owner identifier $00 - // The private data - - $frame_offset = 0; - $frame_terminatorpos = strpos($parsedFrame['data'], "\x00", $frame_offset); - $frame_ownerid = substr($parsedFrame['data'], $frame_offset, $frame_terminatorpos - $frame_offset); - if (ord($frame_ownerid) === 0) { - $frame_ownerid = ''; - } - $frame_offset = $frame_terminatorpos + strlen("\x00"); - - $parsedFrame['ownerid'] = $frame_ownerid; - $parsedFrame['data'] = (string) substr($parsedFrame['data'], $frame_offset); - - - } elseif (($id3v2_majorversion >= 4) && ($parsedFrame['frame_name'] == 'SIGN')) { // 4.28 SIGN Signature frame (ID3v2.4+ only) - // There may be more than one 'signature frame' in a tag, - // but no two may be identical - //
    - // Group symbol $xx - // Signature - - $frame_offset = 0; - $parsedFrame['groupsymbol'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $parsedFrame['data'] = (string) substr($parsedFrame['data'], $frame_offset); - - - } elseif (($id3v2_majorversion >= 4) && ($parsedFrame['frame_name'] == 'SEEK')) { // 4.29 SEEK Seek frame (ID3v2.4+ only) - // There may only be one 'seek frame' in a tag - //
    - // Minimum offset to next tag $xx xx xx xx - - $frame_offset = 0; - $parsedFrame['data'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4)); - - - } elseif (($id3v2_majorversion >= 4) && ($parsedFrame['frame_name'] == 'ASPI')) { // 4.30 ASPI Audio seek point index (ID3v2.4+ only) - // There may only be one 'audio seek point index' frame in a tag - //
    - // Indexed data start (S) $xx xx xx xx - // Indexed data length (L) $xx xx xx xx - // Number of index points (N) $xx xx - // Bits per index point (b) $xx - // Then for every index point the following data is included: - // Fraction at index (Fi) $xx (xx) - - $frame_offset = 0; - $parsedFrame['datastart'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4)); - $frame_offset += 4; - $parsedFrame['indexeddatalength'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 4)); - $frame_offset += 4; - $parsedFrame['indexpoints'] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, 2)); - $frame_offset += 2; - $parsedFrame['bitsperpoint'] = ord(substr($parsedFrame['data'], $frame_offset++, 1)); - $frame_bytesperpoint = ceil($parsedFrame['bitsperpoint'] / 8); - for ($i = 0; $i < $frame_indexpoints; $i++) { - $parsedFrame['indexes'][$i] = getid3_lib::BigEndian2Int(substr($parsedFrame['data'], $frame_offset, $frame_bytesperpoint)); - $frame_offset += $frame_bytesperpoint; - } - unset($parsedFrame['data']); - - } elseif (($id3v2_majorversion >= 3) && ($parsedFrame['frame_name'] == 'RGAD')) { // Replay Gain Adjustment - // http://privatewww.essex.ac.uk/~djmrob/replaygain/file_format_id3v2.html - // There may only be one 'RGAD' frame in a tag - //
    - // Peak Amplitude $xx $xx $xx $xx - // Radio Replay Gain Adjustment %aaabbbcd %dddddddd - // Audiophile Replay Gain Adjustment %aaabbbcd %dddddddd - // a - name code - // b - originator code - // c - sign bit - // d - replay gain adjustment - - $frame_offset = 0; - $parsedFrame['peakamplitude'] = getid3_lib::BigEndian2Float(substr($parsedFrame['data'], $frame_offset, 4)); - $frame_offset += 4; - $rg_track_adjustment = getid3_lib::Dec2Bin(substr($parsedFrame['data'], $frame_offset, 2)); - $frame_offset += 2; - $rg_album_adjustment = getid3_lib::Dec2Bin(substr($parsedFrame['data'], $frame_offset, 2)); - $frame_offset += 2; - $parsedFrame['raw']['track']['name'] = getid3_lib::Bin2Dec(substr($rg_track_adjustment, 0, 3)); - $parsedFrame['raw']['track']['originator'] = getid3_lib::Bin2Dec(substr($rg_track_adjustment, 3, 3)); - $parsedFrame['raw']['track']['signbit'] = getid3_lib::Bin2Dec(substr($rg_track_adjustment, 6, 1)); - $parsedFrame['raw']['track']['adjustment'] = getid3_lib::Bin2Dec(substr($rg_track_adjustment, 7, 9)); - $parsedFrame['raw']['album']['name'] = getid3_lib::Bin2Dec(substr($rg_album_adjustment, 0, 3)); - $parsedFrame['raw']['album']['originator'] = getid3_lib::Bin2Dec(substr($rg_album_adjustment, 3, 3)); - $parsedFrame['raw']['album']['signbit'] = getid3_lib::Bin2Dec(substr($rg_album_adjustment, 6, 1)); - $parsedFrame['raw']['album']['adjustment'] = getid3_lib::Bin2Dec(substr($rg_album_adjustment, 7, 9)); - $parsedFrame['track']['name'] = getid3_lib::RGADnameLookup($parsedFrame['raw']['track']['name']); - $parsedFrame['track']['originator'] = getid3_lib::RGADoriginatorLookup($parsedFrame['raw']['track']['originator']); - $parsedFrame['track']['adjustment'] = getid3_lib::RGADadjustmentLookup($parsedFrame['raw']['track']['adjustment'], $parsedFrame['raw']['track']['signbit']); - $parsedFrame['album']['name'] = getid3_lib::RGADnameLookup($parsedFrame['raw']['album']['name']); - $parsedFrame['album']['originator'] = getid3_lib::RGADoriginatorLookup($parsedFrame['raw']['album']['originator']); - $parsedFrame['album']['adjustment'] = getid3_lib::RGADadjustmentLookup($parsedFrame['raw']['album']['adjustment'], $parsedFrame['raw']['album']['signbit']); - - $info['replay_gain']['track']['peak'] = $parsedFrame['peakamplitude']; - $info['replay_gain']['track']['originator'] = $parsedFrame['track']['originator']; - $info['replay_gain']['track']['adjustment'] = $parsedFrame['track']['adjustment']; - $info['replay_gain']['album']['originator'] = $parsedFrame['album']['originator']; - $info['replay_gain']['album']['adjustment'] = $parsedFrame['album']['adjustment']; - - unset($parsedFrame['data']); - - } - - return true; - } - - - public function DeUnsynchronise($data) { - return str_replace("\xFF\x00", "\xFF", $data); - } - - public function LookupExtendedHeaderRestrictionsTagSizeLimits($index) { - static $LookupExtendedHeaderRestrictionsTagSizeLimits = array( - 0x00 => 'No more than 128 frames and 1 MB total tag size', - 0x01 => 'No more than 64 frames and 128 KB total tag size', - 0x02 => 'No more than 32 frames and 40 KB total tag size', - 0x03 => 'No more than 32 frames and 4 KB total tag size', - ); - return (isset($LookupExtendedHeaderRestrictionsTagSizeLimits[$index]) ? $LookupExtendedHeaderRestrictionsTagSizeLimits[$index] : ''); - } - - public function LookupExtendedHeaderRestrictionsTextEncodings($index) { - static $LookupExtendedHeaderRestrictionsTextEncodings = array( - 0x00 => 'No restrictions', - 0x01 => 'Strings are only encoded with ISO-8859-1 or UTF-8', - ); - return (isset($LookupExtendedHeaderRestrictionsTextEncodings[$index]) ? $LookupExtendedHeaderRestrictionsTextEncodings[$index] : ''); - } - - public function LookupExtendedHeaderRestrictionsTextFieldSize($index) { - static $LookupExtendedHeaderRestrictionsTextFieldSize = array( - 0x00 => 'No restrictions', - 0x01 => 'No string is longer than 1024 characters', - 0x02 => 'No string is longer than 128 characters', - 0x03 => 'No string is longer than 30 characters', - ); - return (isset($LookupExtendedHeaderRestrictionsTextFieldSize[$index]) ? $LookupExtendedHeaderRestrictionsTextFieldSize[$index] : ''); - } - - public function LookupExtendedHeaderRestrictionsImageEncoding($index) { - static $LookupExtendedHeaderRestrictionsImageEncoding = array( - 0x00 => 'No restrictions', - 0x01 => 'Images are encoded only with PNG or JPEG', - ); - return (isset($LookupExtendedHeaderRestrictionsImageEncoding[$index]) ? $LookupExtendedHeaderRestrictionsImageEncoding[$index] : ''); - } - - public function LookupExtendedHeaderRestrictionsImageSizeSize($index) { - static $LookupExtendedHeaderRestrictionsImageSizeSize = array( - 0x00 => 'No restrictions', - 0x01 => 'All images are 256x256 pixels or smaller', - 0x02 => 'All images are 64x64 pixels or smaller', - 0x03 => 'All images are exactly 64x64 pixels, unless required otherwise', - ); - return (isset($LookupExtendedHeaderRestrictionsImageSizeSize[$index]) ? $LookupExtendedHeaderRestrictionsImageSizeSize[$index] : ''); - } - - public function LookupCurrencyUnits($currencyid) { - - $begin = __LINE__; - - /** This is not a comment! - - - AED Dirhams - AFA Afghanis - ALL Leke - AMD Drams - ANG Guilders - AOA Kwanza - ARS Pesos - ATS Schillings - AUD Dollars - AWG Guilders - AZM Manats - BAM Convertible Marka - BBD Dollars - BDT Taka - BEF Francs - BGL Leva - BHD Dinars - BIF Francs - BMD Dollars - BND Dollars - BOB Bolivianos - BRL Brazil Real - BSD Dollars - BTN Ngultrum - BWP Pulas - BYR Rubles - BZD Dollars - CAD Dollars - CDF Congolese Francs - CHF Francs - CLP Pesos - CNY Yuan Renminbi - COP Pesos - CRC Colones - CUP Pesos - CVE Escudos - CYP Pounds - CZK Koruny - DEM Deutsche Marks - DJF Francs - DKK Kroner - DOP Pesos - DZD Algeria Dinars - EEK Krooni - EGP Pounds - ERN Nakfa - ESP Pesetas - ETB Birr - EUR Euro - FIM Markkaa - FJD Dollars - FKP Pounds - FRF Francs - GBP Pounds - GEL Lari - GGP Pounds - GHC Cedis - GIP Pounds - GMD Dalasi - GNF Francs - GRD Drachmae - GTQ Quetzales - GYD Dollars - HKD Dollars - HNL Lempiras - HRK Kuna - HTG Gourdes - HUF Forints - IDR Rupiahs - IEP Pounds - ILS New Shekels - IMP Pounds - INR Rupees - IQD Dinars - IRR Rials - ISK Kronur - ITL Lire - JEP Pounds - JMD Dollars - JOD Dinars - JPY Yen - KES Shillings - KGS Soms - KHR Riels - KMF Francs - KPW Won - KWD Dinars - KYD Dollars - KZT Tenge - LAK Kips - LBP Pounds - LKR Rupees - LRD Dollars - LSL Maloti - LTL Litai - LUF Francs - LVL Lati - LYD Dinars - MAD Dirhams - MDL Lei - MGF Malagasy Francs - MKD Denars - MMK Kyats - MNT Tugriks - MOP Patacas - MRO Ouguiyas - MTL Liri - MUR Rupees - MVR Rufiyaa - MWK Kwachas - MXN Pesos - MYR Ringgits - MZM Meticais - NAD Dollars - NGN Nairas - NIO Gold Cordobas - NLG Guilders - NOK Krone - NPR Nepal Rupees - NZD Dollars - OMR Rials - PAB Balboa - PEN Nuevos Soles - PGK Kina - PHP Pesos - PKR Rupees - PLN Zlotych - PTE Escudos - PYG Guarani - QAR Rials - ROL Lei - RUR Rubles - RWF Rwanda Francs - SAR Riyals - SBD Dollars - SCR Rupees - SDD Dinars - SEK Kronor - SGD Dollars - SHP Pounds - SIT Tolars - SKK Koruny - SLL Leones - SOS Shillings - SPL Luigini - SRG Guilders - STD Dobras - SVC Colones - SYP Pounds - SZL Emalangeni - THB Baht - TJR Rubles - TMM Manats - TND Dinars - TOP Pa'anga - TRL Liras - TTD Dollars - TVD Tuvalu Dollars - TWD New Dollars - TZS Shillings - UAH Hryvnia - UGX Shillings - USD Dollars - UYU Pesos - UZS Sums - VAL Lire - VEB Bolivares - VND Dong - VUV Vatu - WST Tala - XAF Francs - XAG Ounces - XAU Ounces - XCD Dollars - XDR Special Drawing Rights - XPD Ounces - XPF Francs - XPT Ounces - YER Rials - YUM New Dinars - ZAR Rand - ZMK Kwacha - ZWD Zimbabwe Dollars - - */ - - return getid3_lib::EmbeddedLookup($currencyid, $begin, __LINE__, __FILE__, 'id3v2-currency-units'); - } - - - public function LookupCurrencyCountry($currencyid) { - - $begin = __LINE__; - - /** This is not a comment! - - AED United Arab Emirates - AFA Afghanistan - ALL Albania - AMD Armenia - ANG Netherlands Antilles - AOA Angola - ARS Argentina - ATS Austria - AUD Australia - AWG Aruba - AZM Azerbaijan - BAM Bosnia and Herzegovina - BBD Barbados - BDT Bangladesh - BEF Belgium - BGL Bulgaria - BHD Bahrain - BIF Burundi - BMD Bermuda - BND Brunei Darussalam - BOB Bolivia - BRL Brazil - BSD Bahamas - BTN Bhutan - BWP Botswana - BYR Belarus - BZD Belize - CAD Canada - CDF Congo/Kinshasa - CHF Switzerland - CLP Chile - CNY China - COP Colombia - CRC Costa Rica - CUP Cuba - CVE Cape Verde - CYP Cyprus - CZK Czech Republic - DEM Germany - DJF Djibouti - DKK Denmark - DOP Dominican Republic - DZD Algeria - EEK Estonia - EGP Egypt - ERN Eritrea - ESP Spain - ETB Ethiopia - EUR Euro Member Countries - FIM Finland - FJD Fiji - FKP Falkland Islands (Malvinas) - FRF France - GBP United Kingdom - GEL Georgia - GGP Guernsey - GHC Ghana - GIP Gibraltar - GMD Gambia - GNF Guinea - GRD Greece - GTQ Guatemala - GYD Guyana - HKD Hong Kong - HNL Honduras - HRK Croatia - HTG Haiti - HUF Hungary - IDR Indonesia - IEP Ireland (Eire) - ILS Israel - IMP Isle of Man - INR India - IQD Iraq - IRR Iran - ISK Iceland - ITL Italy - JEP Jersey - JMD Jamaica - JOD Jordan - JPY Japan - KES Kenya - KGS Kyrgyzstan - KHR Cambodia - KMF Comoros - KPW Korea - KWD Kuwait - KYD Cayman Islands - KZT Kazakstan - LAK Laos - LBP Lebanon - LKR Sri Lanka - LRD Liberia - LSL Lesotho - LTL Lithuania - LUF Luxembourg - LVL Latvia - LYD Libya - MAD Morocco - MDL Moldova - MGF Madagascar - MKD Macedonia - MMK Myanmar (Burma) - MNT Mongolia - MOP Macau - MRO Mauritania - MTL Malta - MUR Mauritius - MVR Maldives (Maldive Islands) - MWK Malawi - MXN Mexico - MYR Malaysia - MZM Mozambique - NAD Namibia - NGN Nigeria - NIO Nicaragua - NLG Netherlands (Holland) - NOK Norway - NPR Nepal - NZD New Zealand - OMR Oman - PAB Panama - PEN Peru - PGK Papua New Guinea - PHP Philippines - PKR Pakistan - PLN Poland - PTE Portugal - PYG Paraguay - QAR Qatar - ROL Romania - RUR Russia - RWF Rwanda - SAR Saudi Arabia - SBD Solomon Islands - SCR Seychelles - SDD Sudan - SEK Sweden - SGD Singapore - SHP Saint Helena - SIT Slovenia - SKK Slovakia - SLL Sierra Leone - SOS Somalia - SPL Seborga - SRG Suriname - STD São Tome and Principe - SVC El Salvador - SYP Syria - SZL Swaziland - THB Thailand - TJR Tajikistan - TMM Turkmenistan - TND Tunisia - TOP Tonga - TRL Turkey - TTD Trinidad and Tobago - TVD Tuvalu - TWD Taiwan - TZS Tanzania - UAH Ukraine - UGX Uganda - USD United States of America - UYU Uruguay - UZS Uzbekistan - VAL Vatican City - VEB Venezuela - VND Viet Nam - VUV Vanuatu - WST Samoa - XAF Communauté Financière Africaine - XAG Silver - XAU Gold - XCD East Caribbean - XDR International Monetary Fund - XPD Palladium - XPF Comptoirs Français du Pacifique - XPT Platinum - YER Yemen - YUM Yugoslavia - ZAR South Africa - ZMK Zambia - ZWD Zimbabwe - - */ - - return getid3_lib::EmbeddedLookup($currencyid, $begin, __LINE__, __FILE__, 'id3v2-currency-country'); - } - - - - public static function LanguageLookup($languagecode, $casesensitive=false) { - - if (!$casesensitive) { - $languagecode = strtolower($languagecode); - } - - // http://www.id3.org/id3v2.4.0-structure.txt - // [4. ID3v2 frame overview] - // The three byte language field, present in several frames, is used to - // describe the language of the frame's content, according to ISO-639-2 - // [ISO-639-2]. The language should be represented in lower case. If the - // language is not known the string "XXX" should be used. - - - // ISO 639-2 - http://www.id3.org/iso639-2.html - - $begin = __LINE__; - - /** This is not a comment! - - XXX unknown - xxx unknown - aar Afar - abk Abkhazian - ace Achinese - ach Acoli - ada Adangme - afa Afro-Asiatic (Other) - afh Afrihili - afr Afrikaans - aka Akan - akk Akkadian - alb Albanian - ale Aleut - alg Algonquian Languages - amh Amharic - ang English, Old (ca. 450-1100) - apa Apache Languages - ara Arabic - arc Aramaic - arm Armenian - arn Araucanian - arp Arapaho - art Artificial (Other) - arw Arawak - asm Assamese - ath Athapascan Languages - ava Avaric - ave Avestan - awa Awadhi - aym Aymara - aze Azerbaijani - bad Banda - bai Bamileke Languages - bak Bashkir - bal Baluchi - bam Bambara - ban Balinese - baq Basque - bas Basa - bat Baltic (Other) - bej Beja - bel Byelorussian - bem Bemba - ben Bengali - ber Berber (Other) - bho Bhojpuri - bih Bihari - bik Bikol - bin Bini - bis Bislama - bla Siksika - bnt Bantu (Other) - bod Tibetan - bra Braj - bre Breton - bua Buriat - bug Buginese - bul Bulgarian - bur Burmese - cad Caddo - cai Central American Indian (Other) - car Carib - cat Catalan - cau Caucasian (Other) - ceb Cebuano - cel Celtic (Other) - ces Czech - cha Chamorro - chb Chibcha - che Chechen - chg Chagatai - chi Chinese - chm Mari - chn Chinook jargon - cho Choctaw - chr Cherokee - chu Church Slavic - chv Chuvash - chy Cheyenne - cop Coptic - cor Cornish - cos Corsican - cpe Creoles and Pidgins, English-based (Other) - cpf Creoles and Pidgins, French-based (Other) - cpp Creoles and Pidgins, Portuguese-based (Other) - cre Cree - crp Creoles and Pidgins (Other) - cus Cushitic (Other) - cym Welsh - cze Czech - dak Dakota - dan Danish - del Delaware - deu German - din Dinka - div Divehi - doi Dogri - dra Dravidian (Other) - dua Duala - dum Dutch, Middle (ca. 1050-1350) - dut Dutch - dyu Dyula - dzo Dzongkha - efi Efik - egy Egyptian (Ancient) - eka Ekajuk - ell Greek, Modern (1453-) - elx Elamite - eng English - enm English, Middle (ca. 1100-1500) - epo Esperanto - esk Eskimo (Other) - esl Spanish - est Estonian - eus Basque - ewe Ewe - ewo Ewondo - fan Fang - fao Faroese - fas Persian - fat Fanti - fij Fijian - fin Finnish - fiu Finno-Ugrian (Other) - fon Fon - fra French - fre French - frm French, Middle (ca. 1400-1600) - fro French, Old (842- ca. 1400) - fry Frisian - ful Fulah - gaa Ga - gae Gaelic (Scots) - gai Irish - gay Gayo - gdh Gaelic (Scots) - gem Germanic (Other) - geo Georgian - ger German - gez Geez - gil Gilbertese - glg Gallegan - gmh German, Middle High (ca. 1050-1500) - goh German, Old High (ca. 750-1050) - gon Gondi - got Gothic - grb Grebo - grc Greek, Ancient (to 1453) - gre Greek, Modern (1453-) - grn Guarani - guj Gujarati - hai Haida - hau Hausa - haw Hawaiian - heb Hebrew - her Herero - hil Hiligaynon - him Himachali - hin Hindi - hmo Hiri Motu - hun Hungarian - hup Hupa - hye Armenian - iba Iban - ibo Igbo - ice Icelandic - ijo Ijo - iku Inuktitut - ilo Iloko - ina Interlingua (International Auxiliary language Association) - inc Indic (Other) - ind Indonesian - ine Indo-European (Other) - ine Interlingue - ipk Inupiak - ira Iranian (Other) - iri Irish - iro Iroquoian uages - isl Icelandic - ita Italian - jav Javanese - jaw Javanese - jpn Japanese - jpr Judeo-Persian - jrb Judeo-Arabic - kaa Kara-Kalpak - kab Kabyle - kac Kachin - kal Greenlandic - kam Kamba - kan Kannada - kar Karen - kas Kashmiri - kat Georgian - kau Kanuri - kaw Kawi - kaz Kazakh - kha Khasi - khi Khoisan (Other) - khm Khmer - kho Khotanese - kik Kikuyu - kin Kinyarwanda - kir Kirghiz - kok Konkani - kom Komi - kon Kongo - kor Korean - kpe Kpelle - kro Kru - kru Kurukh - kua Kuanyama - kum Kumyk - kur Kurdish - kus Kusaie - kut Kutenai - lad Ladino - lah Lahnda - lam Lamba - lao Lao - lat Latin - lav Latvian - lez Lezghian - lin Lingala - lit Lithuanian - lol Mongo - loz Lozi - ltz Letzeburgesch - lub Luba-Katanga - lug Ganda - lui Luiseno - lun Lunda - luo Luo (Kenya and Tanzania) - mac Macedonian - mad Madurese - mag Magahi - mah Marshall - mai Maithili - mak Macedonian - mak Makasar - mal Malayalam - man Mandingo - mao Maori - map Austronesian (Other) - mar Marathi - mas Masai - max Manx - may Malay - men Mende - mga Irish, Middle (900 - 1200) - mic Micmac - min Minangkabau - mis Miscellaneous (Other) - mkh Mon-Kmer (Other) - mlg Malagasy - mlt Maltese - mni Manipuri - mno Manobo Languages - moh Mohawk - mol Moldavian - mon Mongolian - mos Mossi - mri Maori - msa Malay - mul Multiple Languages - mun Munda Languages - mus Creek - mwr Marwari - mya Burmese - myn Mayan Languages - nah Aztec - nai North American Indian (Other) - nau Nauru - nav Navajo - nbl Ndebele, South - nde Ndebele, North - ndo Ndongo - nep Nepali - new Newari - nic Niger-Kordofanian (Other) - niu Niuean - nla Dutch - nno Norwegian (Nynorsk) - non Norse, Old - nor Norwegian - nso Sotho, Northern - nub Nubian Languages - nya Nyanja - nym Nyamwezi - nyn Nyankole - nyo Nyoro - nzi Nzima - oci Langue d'Oc (post 1500) - oji Ojibwa - ori Oriya - orm Oromo - osa Osage - oss Ossetic - ota Turkish, Ottoman (1500 - 1928) - oto Otomian Languages - paa Papuan-Australian (Other) - pag Pangasinan - pal Pahlavi - pam Pampanga - pan Panjabi - pap Papiamento - pau Palauan - peo Persian, Old (ca 600 - 400 B.C.) - per Persian - phn Phoenician - pli Pali - pol Polish - pon Ponape - por Portuguese - pra Prakrit uages - pro Provencal, Old (to 1500) - pus Pushto - que Quechua - raj Rajasthani - rar Rarotongan - roa Romance (Other) - roh Rhaeto-Romance - rom Romany - ron Romanian - rum Romanian - run Rundi - rus Russian - sad Sandawe - sag Sango - sah Yakut - sai South American Indian (Other) - sal Salishan Languages - sam Samaritan Aramaic - san Sanskrit - sco Scots - scr Serbo-Croatian - sel Selkup - sem Semitic (Other) - sga Irish, Old (to 900) - shn Shan - sid Sidamo - sin Singhalese - sio Siouan Languages - sit Sino-Tibetan (Other) - sla Slavic (Other) - slk Slovak - slo Slovak - slv Slovenian - smi Sami Languages - smo Samoan - sna Shona - snd Sindhi - sog Sogdian - som Somali - son Songhai - sot Sotho, Southern - spa Spanish - sqi Albanian - srd Sardinian - srr Serer - ssa Nilo-Saharan (Other) - ssw Siswant - ssw Swazi - suk Sukuma - sun Sudanese - sus Susu - sux Sumerian - sve Swedish - swa Swahili - swe Swedish - syr Syriac - tah Tahitian - tam Tamil - tat Tatar - tel Telugu - tem Timne - ter Tereno - tgk Tajik - tgl Tagalog - tha Thai - tib Tibetan - tig Tigre - tir Tigrinya - tiv Tivi - tli Tlingit - tmh Tamashek - tog Tonga (Nyasa) - ton Tonga (Tonga Islands) - tru Truk - tsi Tsimshian - tsn Tswana - tso Tsonga - tuk Turkmen - tum Tumbuka - tur Turkish - tut Altaic (Other) - twi Twi - tyv Tuvinian - uga Ugaritic - uig Uighur - ukr Ukrainian - umb Umbundu - und Undetermined - urd Urdu - uzb Uzbek - vai Vai - ven Venda - vie Vietnamese - vol Volapük - vot Votic - wak Wakashan Languages - wal Walamo - war Waray - was Washo - wel Welsh - wen Sorbian Languages - wol Wolof - xho Xhosa - yao Yao - yap Yap - yid Yiddish - yor Yoruba - zap Zapotec - zen Zenaga - zha Zhuang - zho Chinese - zul Zulu - zun Zuni - - */ - - return getid3_lib::EmbeddedLookup($languagecode, $begin, __LINE__, __FILE__, 'id3v2-languagecode'); - } - - - public static function ETCOEventLookup($index) { - if (($index >= 0x17) && ($index <= 0xDF)) { - return 'reserved for future use'; - } - if (($index >= 0xE0) && ($index <= 0xEF)) { - return 'not predefined synch 0-F'; - } - if (($index >= 0xF0) && ($index <= 0xFC)) { - return 'reserved for future use'; - } - - static $EventLookup = array( - 0x00 => 'padding (has no meaning)', - 0x01 => 'end of initial silence', - 0x02 => 'intro start', - 0x03 => 'main part start', - 0x04 => 'outro start', - 0x05 => 'outro end', - 0x06 => 'verse start', - 0x07 => 'refrain start', - 0x08 => 'interlude start', - 0x09 => 'theme start', - 0x0A => 'variation start', - 0x0B => 'key change', - 0x0C => 'time change', - 0x0D => 'momentary unwanted noise (Snap, Crackle & Pop)', - 0x0E => 'sustained noise', - 0x0F => 'sustained noise end', - 0x10 => 'intro end', - 0x11 => 'main part end', - 0x12 => 'verse end', - 0x13 => 'refrain end', - 0x14 => 'theme end', - 0x15 => 'profanity', - 0x16 => 'profanity end', - 0xFD => 'audio end (start of silence)', - 0xFE => 'audio file ends', - 0xFF => 'one more byte of events follows' - ); - - return (isset($EventLookup[$index]) ? $EventLookup[$index] : ''); - } - - public static function SYTLContentTypeLookup($index) { - static $SYTLContentTypeLookup = array( - 0x00 => 'other', - 0x01 => 'lyrics', - 0x02 => 'text transcription', - 0x03 => 'movement/part name', // (e.g. 'Adagio') - 0x04 => 'events', // (e.g. 'Don Quijote enters the stage') - 0x05 => 'chord', // (e.g. 'Bb F Fsus') - 0x06 => 'trivia/\'pop up\' information', - 0x07 => 'URLs to webpages', - 0x08 => 'URLs to images' - ); - - return (isset($SYTLContentTypeLookup[$index]) ? $SYTLContentTypeLookup[$index] : ''); - } - - public static function APICPictureTypeLookup($index, $returnarray=false) { - static $APICPictureTypeLookup = array( - 0x00 => 'Other', - 0x01 => '32x32 pixels \'file icon\' (PNG only)', - 0x02 => 'Other file icon', - 0x03 => 'Cover (front)', - 0x04 => 'Cover (back)', - 0x05 => 'Leaflet page', - 0x06 => 'Media (e.g. label side of CD)', - 0x07 => 'Lead artist/lead performer/soloist', - 0x08 => 'Artist/performer', - 0x09 => 'Conductor', - 0x0A => 'Band/Orchestra', - 0x0B => 'Composer', - 0x0C => 'Lyricist/text writer', - 0x0D => 'Recording Location', - 0x0E => 'During recording', - 0x0F => 'During performance', - 0x10 => 'Movie/video screen capture', - 0x11 => 'A bright coloured fish', - 0x12 => 'Illustration', - 0x13 => 'Band/artist logotype', - 0x14 => 'Publisher/Studio logotype' - ); - if ($returnarray) { - return $APICPictureTypeLookup; - } - return (isset($APICPictureTypeLookup[$index]) ? $APICPictureTypeLookup[$index] : ''); - } - - public static function COMRReceivedAsLookup($index) { - static $COMRReceivedAsLookup = array( - 0x00 => 'Other', - 0x01 => 'Standard CD album with other songs', - 0x02 => 'Compressed audio on CD', - 0x03 => 'File over the Internet', - 0x04 => 'Stream over the Internet', - 0x05 => 'As note sheets', - 0x06 => 'As note sheets in a book with other sheets', - 0x07 => 'Music on other media', - 0x08 => 'Non-musical merchandise' - ); - - return (isset($COMRReceivedAsLookup[$index]) ? $COMRReceivedAsLookup[$index] : ''); - } - - public static function RVA2ChannelTypeLookup($index) { - static $RVA2ChannelTypeLookup = array( - 0x00 => 'Other', - 0x01 => 'Master volume', - 0x02 => 'Front right', - 0x03 => 'Front left', - 0x04 => 'Back right', - 0x05 => 'Back left', - 0x06 => 'Front centre', - 0x07 => 'Back centre', - 0x08 => 'Subwoofer' - ); - - return (isset($RVA2ChannelTypeLookup[$index]) ? $RVA2ChannelTypeLookup[$index] : ''); - } - - public static function FrameNameLongLookup($framename) { - - $begin = __LINE__; - - /** This is not a comment! - - AENC Audio encryption - APIC Attached picture - ASPI Audio seek point index - BUF Recommended buffer size - CNT Play counter - COM Comments - COMM Comments - COMR Commercial frame - CRA Audio encryption - CRM Encrypted meta frame - ENCR Encryption method registration - EQU Equalisation - EQU2 Equalisation (2) - EQUA Equalisation - ETC Event timing codes - ETCO Event timing codes - GEO General encapsulated object - GEOB General encapsulated object - GRID Group identification registration - IPL Involved people list - IPLS Involved people list - LINK Linked information - LNK Linked information - MCDI Music CD identifier - MCI Music CD Identifier - MLL MPEG location lookup table - MLLT MPEG location lookup table - OWNE Ownership frame - PCNT Play counter - PIC Attached picture - POP Popularimeter - POPM Popularimeter - POSS Position synchronisation frame - PRIV Private frame - RBUF Recommended buffer size - REV Reverb - RVA Relative volume adjustment - RVA2 Relative volume adjustment (2) - RVAD Relative volume adjustment - RVRB Reverb - SEEK Seek frame - SIGN Signature frame - SLT Synchronised lyric/text - STC Synced tempo codes - SYLT Synchronised lyric/text - SYTC Synchronised tempo codes - TAL Album/Movie/Show title - TALB Album/Movie/Show title - TBP BPM (Beats Per Minute) - TBPM BPM (beats per minute) - TCM Composer - TCMP Part of a compilation - TCO Content type - TCOM Composer - TCON Content type - TCOP Copyright message - TCP Part of a compilation - TCR Copyright message - TDA Date - TDAT Date - TDEN Encoding time - TDLY Playlist delay - TDOR Original release time - TDRC Recording time - TDRL Release time - TDTG Tagging time - TDY Playlist delay - TEN Encoded by - TENC Encoded by - TEXT Lyricist/Text writer - TFLT File type - TFT File type - TIM Time - TIME Time - TIPL Involved people list - TIT1 Content group description - TIT2 Title/songname/content description - TIT3 Subtitle/Description refinement - TKE Initial key - TKEY Initial key - TLA Language(s) - TLAN Language(s) - TLE Length - TLEN Length - TMCL Musician credits list - TMED Media type - TMOO Mood - TMT Media type - TOA Original artist(s)/performer(s) - TOAL Original album/movie/show title - TOF Original filename - TOFN Original filename - TOL Original Lyricist(s)/text writer(s) - TOLY Original lyricist(s)/text writer(s) - TOPE Original artist(s)/performer(s) - TOR Original release year - TORY Original release year - TOT Original album/Movie/Show title - TOWN File owner/licensee - TP1 Lead artist(s)/Lead performer(s)/Soloist(s)/Performing group - TP2 Band/Orchestra/Accompaniment - TP3 Conductor/Performer refinement - TP4 Interpreted, remixed, or otherwise modified by - TPA Part of a set - TPB Publisher - TPE1 Lead performer(s)/Soloist(s) - TPE2 Band/orchestra/accompaniment - TPE3 Conductor/performer refinement - TPE4 Interpreted, remixed, or otherwise modified by - TPOS Part of a set - TPRO Produced notice - TPUB Publisher - TRC ISRC (International Standard Recording Code) - TRCK Track number/Position in set - TRD Recording dates - TRDA Recording dates - TRK Track number/Position in set - TRSN Internet radio station name - TRSO Internet radio station owner - TS2 Album-Artist sort order - TSA Album sort order - TSC Composer sort order - TSI Size - TSIZ Size - TSO2 Album-Artist sort order - TSOA Album sort order - TSOC Composer sort order - TSOP Performer sort order - TSOT Title sort order - TSP Performer sort order - TSRC ISRC (international standard recording code) - TSS Software/hardware and settings used for encoding - TSSE Software/Hardware and settings used for encoding - TSST Set subtitle - TST Title sort order - TT1 Content group description - TT2 Title/Songname/Content description - TT3 Subtitle/Description refinement - TXT Lyricist/text writer - TXX User defined text information frame - TXXX User defined text information frame - TYE Year - TYER Year - UFI Unique file identifier - UFID Unique file identifier - ULT Unsychronised lyric/text transcription - USER Terms of use - USLT Unsynchronised lyric/text transcription - WAF Official audio file webpage - WAR Official artist/performer webpage - WAS Official audio source webpage - WCM Commercial information - WCOM Commercial information - WCOP Copyright/Legal information - WCP Copyright/Legal information - WOAF Official audio file webpage - WOAR Official artist/performer webpage - WOAS Official audio source webpage - WORS Official Internet radio station homepage - WPAY Payment - WPB Publishers official webpage - WPUB Publishers official webpage - WXX User defined URL link frame - WXXX User defined URL link frame - TFEA Featured Artist - TSTU Recording Studio - rgad Replay Gain Adjustment - - */ - - return getid3_lib::EmbeddedLookup($framename, $begin, __LINE__, __FILE__, 'id3v2-framename_long'); - - // Last three: - // from Helium2 [www.helium2.com] - // from http://privatewww.essex.ac.uk/~djmrob/replaygain/file_format_id3v2.html - } - - - public static function FrameNameShortLookup($framename) { - - $begin = __LINE__; - - /** This is not a comment! - - AENC audio_encryption - APIC attached_picture - ASPI audio_seek_point_index - BUF recommended_buffer_size - CNT play_counter - COM comment - COMM comment - COMR commercial_frame - CRA audio_encryption - CRM encrypted_meta_frame - ENCR encryption_method_registration - EQU equalisation - EQU2 equalisation - EQUA equalisation - ETC event_timing_codes - ETCO event_timing_codes - GEO general_encapsulated_object - GEOB general_encapsulated_object - GRID group_identification_registration - IPL involved_people_list - IPLS involved_people_list - LINK linked_information - LNK linked_information - MCDI music_cd_identifier - MCI music_cd_identifier - MLL mpeg_location_lookup_table - MLLT mpeg_location_lookup_table - OWNE ownership_frame - PCNT play_counter - PIC attached_picture - POP popularimeter - POPM popularimeter - POSS position_synchronisation_frame - PRIV private_frame - RBUF recommended_buffer_size - REV reverb - RVA relative_volume_adjustment - RVA2 relative_volume_adjustment - RVAD relative_volume_adjustment - RVRB reverb - SEEK seek_frame - SIGN signature_frame - SLT synchronised_lyric - STC synced_tempo_codes - SYLT synchronised_lyric - SYTC synchronised_tempo_codes - TAL album - TALB album - TBP bpm - TBPM bpm - TCM composer - TCMP part_of_a_compilation - TCO genre - TCOM composer - TCON genre - TCOP copyright_message - TCP part_of_a_compilation - TCR copyright_message - TDA date - TDAT date - TDEN encoding_time - TDLY playlist_delay - TDOR original_release_time - TDRC recording_time - TDRL release_time - TDTG tagging_time - TDY playlist_delay - TEN encoded_by - TENC encoded_by - TEXT lyricist - TFLT file_type - TFT file_type - TIM time - TIME time - TIPL involved_people_list - TIT1 content_group_description - TIT2 title - TIT3 subtitle - TKE initial_key - TKEY initial_key - TLA language - TLAN language - TLE length - TLEN length - TMCL musician_credits_list - TMED media_type - TMOO mood - TMT media_type - TOA original_artist - TOAL original_album - TOF original_filename - TOFN original_filename - TOL original_lyricist - TOLY original_lyricist - TOPE original_artist - TOR original_year - TORY original_year - TOT original_album - TOWN file_owner - TP1 artist - TP2 band - TP3 conductor - TP4 remixer - TPA part_of_a_set - TPB publisher - TPE1 artist - TPE2 band - TPE3 conductor - TPE4 remixer - TPOS part_of_a_set - TPRO produced_notice - TPUB publisher - TRC isrc - TRCK track_number - TRD recording_dates - TRDA recording_dates - TRK track_number - TRSN internet_radio_station_name - TRSO internet_radio_station_owner - TS2 album_artist_sort_order - TSA album_sort_order - TSC composer_sort_order - TSI size - TSIZ size - TSO2 album_artist_sort_order - TSOA album_sort_order - TSOC composer_sort_order - TSOP performer_sort_order - TSOT title_sort_order - TSP performer_sort_order - TSRC isrc - TSS encoder_settings - TSSE encoder_settings - TSST set_subtitle - TST title_sort_order - TT1 content_group_description - TT2 title - TT3 subtitle - TXT lyricist - TXX text - TXXX text - TYE year - TYER year - UFI unique_file_identifier - UFID unique_file_identifier - ULT unsychronised_lyric - USER terms_of_use - USLT unsynchronised_lyric - WAF url_file - WAR url_artist - WAS url_source - WCM commercial_information - WCOM commercial_information - WCOP copyright - WCP copyright - WOAF url_file - WOAR url_artist - WOAS url_source - WORS url_station - WPAY url_payment - WPB url_publisher - WPUB url_publisher - WXX url_user - WXXX url_user - TFEA featured_artist - TSTU recording_studio - rgad replay_gain_adjustment - - */ - - return getid3_lib::EmbeddedLookup($framename, $begin, __LINE__, __FILE__, 'id3v2-framename_short'); - } - - public static function TextEncodingTerminatorLookup($encoding) { - // http://www.id3.org/id3v2.4.0-structure.txt - // Frames that allow different types of text encoding contains a text encoding description byte. Possible encodings: - static $TextEncodingTerminatorLookup = array( - 0 => "\x00", // $00 ISO-8859-1. Terminated with $00. - 1 => "\x00\x00", // $01 UTF-16 encoded Unicode with BOM. All strings in the same frame SHALL have the same byteorder. Terminated with $00 00. - 2 => "\x00\x00", // $02 UTF-16BE encoded Unicode without BOM. Terminated with $00 00. - 3 => "\x00", // $03 UTF-8 encoded Unicode. Terminated with $00. - 255 => "\x00\x00" - ); - return (isset($TextEncodingTerminatorLookup[$encoding]) ? $TextEncodingTerminatorLookup[$encoding] : ''); - } - - public static function TextEncodingNameLookup($encoding) { - // http://www.id3.org/id3v2.4.0-structure.txt - // Frames that allow different types of text encoding contains a text encoding description byte. Possible encodings: - static $TextEncodingNameLookup = array( - 0 => 'ISO-8859-1', // $00 ISO-8859-1. Terminated with $00. - 1 => 'UTF-16', // $01 UTF-16 encoded Unicode with BOM. All strings in the same frame SHALL have the same byteorder. Terminated with $00 00. - 2 => 'UTF-16BE', // $02 UTF-16BE encoded Unicode without BOM. Terminated with $00 00. - 3 => 'UTF-8', // $03 UTF-8 encoded Unicode. Terminated with $00. - 255 => 'UTF-16BE' - ); - return (isset($TextEncodingNameLookup[$encoding]) ? $TextEncodingNameLookup[$encoding] : 'ISO-8859-1'); - } - - public static function IsValidID3v2FrameName($framename, $id3v2majorversion) { - switch ($id3v2majorversion) { - case 2: - return preg_match('#[A-Z][A-Z0-9]{2}#', $framename); - break; - - case 3: - case 4: - return preg_match('#[A-Z][A-Z0-9]{3}#', $framename); - break; - } - return false; - } - - public static function IsANumber($numberstring, $allowdecimal=false, $allownegative=false) { - for ($i = 0; $i < strlen($numberstring); $i++) { - if ((chr($numberstring{$i}) < chr('0')) || (chr($numberstring{$i}) > chr('9'))) { - if (($numberstring{$i} == '.') && $allowdecimal) { - // allowed - } elseif (($numberstring{$i} == '-') && $allownegative && ($i == 0)) { - // allowed - } else { - return false; - } - } - } - return true; - } - - public static function IsValidDateStampString($datestamp) { - if (strlen($datestamp) != 8) { - return false; - } - if (!self::IsANumber($datestamp, false)) { - return false; - } - $year = substr($datestamp, 0, 4); - $month = substr($datestamp, 4, 2); - $day = substr($datestamp, 6, 2); - if (($year == 0) || ($month == 0) || ($day == 0)) { - return false; - } - if ($month > 12) { - return false; - } - if ($day > 31) { - return false; - } - if (($day > 30) && (($month == 4) || ($month == 6) || ($month == 9) || ($month == 11))) { - return false; - } - if (($day > 29) && ($month == 2)) { - return false; - } - return true; - } - - public static function ID3v2HeaderLength($majorversion) { - return (($majorversion == 2) ? 6 : 10); - } - -} - diff --git a/src/Classes/Vendor/getid3/module.tag.lyrics3.php b/src/Classes/Vendor/getid3/module.tag.lyrics3.php deleted file mode 100755 index 108d7aeea..000000000 --- a/src/Classes/Vendor/getid3/module.tag.lyrics3.php +++ /dev/null @@ -1,294 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -/// // -// module.tag.lyrics3.php // -// module for analyzing Lyrics3 tags // -// dependencies: module.tag.apetag.php (optional) // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_lyrics3 extends getid3_handler -{ - - public function Analyze() { - $info = &$this->getid3->info; - - // http://www.volweb.cz/str/tags.htm - - if (!getid3_lib::intValueSupported($info['filesize'])) { - $info['warning'][] = 'Unable to check for Lyrics3 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB'; - return false; - } - - fseek($this->getid3->fp, (0 - 128 - 9 - 6), SEEK_END); // end - ID3v1 - "LYRICSEND" - [Lyrics3size] - $lyrics3_id3v1 = fread($this->getid3->fp, 128 + 9 + 6); - $lyrics3lsz = substr($lyrics3_id3v1, 0, 6); // Lyrics3size - $lyrics3end = substr($lyrics3_id3v1, 6, 9); // LYRICSEND or LYRICS200 - $id3v1tag = substr($lyrics3_id3v1, 15, 128); // ID3v1 - - if ($lyrics3end == 'LYRICSEND') { - // Lyrics3v1, ID3v1, no APE - - $lyrics3size = 5100; - $lyrics3offset = $info['filesize'] - 128 - $lyrics3size; - $lyrics3version = 1; - - } elseif ($lyrics3end == 'LYRICS200') { - // Lyrics3v2, ID3v1, no APE - - // LSZ = lyrics + 'LYRICSBEGIN'; add 6-byte size field; add 'LYRICS200' - $lyrics3size = $lyrics3lsz + 6 + strlen('LYRICS200'); - $lyrics3offset = $info['filesize'] - 128 - $lyrics3size; - $lyrics3version = 2; - - } elseif (substr(strrev($lyrics3_id3v1), 0, 9) == strrev('LYRICSEND')) { - // Lyrics3v1, no ID3v1, no APE - - $lyrics3size = 5100; - $lyrics3offset = $info['filesize'] - $lyrics3size; - $lyrics3version = 1; - $lyrics3offset = $info['filesize'] - $lyrics3size; - - } elseif (substr(strrev($lyrics3_id3v1), 0, 9) == strrev('LYRICS200')) { - - // Lyrics3v2, no ID3v1, no APE - - $lyrics3size = strrev(substr(strrev($lyrics3_id3v1), 9, 6)) + 6 + strlen('LYRICS200'); // LSZ = lyrics + 'LYRICSBEGIN'; add 6-byte size field; add 'LYRICS200' - $lyrics3offset = $info['filesize'] - $lyrics3size; - $lyrics3version = 2; - - } else { - - if (isset($info['ape']['tag_offset_start']) && ($info['ape']['tag_offset_start'] > 15)) { - - fseek($this->getid3->fp, $info['ape']['tag_offset_start'] - 15, SEEK_SET); - $lyrics3lsz = fread($this->getid3->fp, 6); - $lyrics3end = fread($this->getid3->fp, 9); - - if ($lyrics3end == 'LYRICSEND') { - // Lyrics3v1, APE, maybe ID3v1 - - $lyrics3size = 5100; - $lyrics3offset = $info['ape']['tag_offset_start'] - $lyrics3size; - $info['avdataend'] = $lyrics3offset; - $lyrics3version = 1; - $info['warning'][] = 'APE tag located after Lyrics3, will probably break Lyrics3 compatability'; - - } elseif ($lyrics3end == 'LYRICS200') { - // Lyrics3v2, APE, maybe ID3v1 - - $lyrics3size = $lyrics3lsz + 6 + strlen('LYRICS200'); // LSZ = lyrics + 'LYRICSBEGIN'; add 6-byte size field; add 'LYRICS200' - $lyrics3offset = $info['ape']['tag_offset_start'] - $lyrics3size; - $lyrics3version = 2; - $info['warning'][] = 'APE tag located after Lyrics3, will probably break Lyrics3 compatability'; - - } - - } - - } - - if (isset($lyrics3offset)) { - $info['avdataend'] = $lyrics3offset; - $this->getLyrics3Data($lyrics3offset, $lyrics3version, $lyrics3size); - - if (!isset($info['ape'])) { - $GETID3_ERRORARRAY = &$info['warning']; - if (getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.apetag.php', __FILE__, false)) { - $getid3_temp = new getID3(); - $getid3_temp->openfile($this->getid3->filename); - $getid3_apetag = new getid3_apetag($getid3_temp); - $getid3_apetag->overrideendoffset = $info['lyrics3']['tag_offset_start']; - $getid3_apetag->Analyze(); - if (!empty($getid3_temp->info['ape'])) { - $info['ape'] = $getid3_temp->info['ape']; - } - if (!empty($getid3_temp->info['replay_gain'])) { - $info['replay_gain'] = $getid3_temp->info['replay_gain']; - } - unset($getid3_temp, $getid3_apetag); - } - } - - } - - return true; - } - - public function getLyrics3Data($endoffset, $version, $length) { - // http://www.volweb.cz/str/tags.htm - - $info = &$this->getid3->info; - - if (!getid3_lib::intValueSupported($endoffset)) { - $info['warning'][] = 'Unable to check for Lyrics3 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB'; - return false; - } - - fseek($this->getid3->fp, $endoffset, SEEK_SET); - if ($length <= 0) { - return false; - } - $rawdata = fread($this->getid3->fp, $length); - - $ParsedLyrics3['raw']['lyrics3version'] = $version; - $ParsedLyrics3['raw']['lyrics3tagsize'] = $length; - $ParsedLyrics3['tag_offset_start'] = $endoffset; - $ParsedLyrics3['tag_offset_end'] = $endoffset + $length - 1; - - if (substr($rawdata, 0, 11) != 'LYRICSBEGIN') { - if (strpos($rawdata, 'LYRICSBEGIN') !== false) { - - $info['warning'][] = '"LYRICSBEGIN" expected at '.$endoffset.' but actually found at '.($endoffset + strpos($rawdata, 'LYRICSBEGIN')).' - this is invalid for Lyrics3 v'.$version; - $info['avdataend'] = $endoffset + strpos($rawdata, 'LYRICSBEGIN'); - $rawdata = substr($rawdata, strpos($rawdata, 'LYRICSBEGIN')); - $length = strlen($rawdata); - $ParsedLyrics3['tag_offset_start'] = $info['avdataend']; - $ParsedLyrics3['raw']['lyrics3tagsize'] = $length; - - } else { - - $info['error'][] = '"LYRICSBEGIN" expected at '.$endoffset.' but found "'.substr($rawdata, 0, 11).'" instead'; - return false; - - } - - } - - switch ($version) { - - case 1: - if (substr($rawdata, strlen($rawdata) - 9, 9) == 'LYRICSEND') { - $ParsedLyrics3['raw']['LYR'] = trim(substr($rawdata, 11, strlen($rawdata) - 11 - 9)); - $this->Lyrics3LyricsTimestampParse($ParsedLyrics3); - } else { - $info['error'][] = '"LYRICSEND" expected at '.(ftell($this->getid3->fp) - 11 + $length - 9).' but found "'.substr($rawdata, strlen($rawdata) - 9, 9).'" instead'; - return false; - } - break; - - case 2: - if (substr($rawdata, strlen($rawdata) - 9, 9) == 'LYRICS200') { - $ParsedLyrics3['raw']['unparsed'] = substr($rawdata, 11, strlen($rawdata) - 11 - 9 - 6); // LYRICSBEGIN + LYRICS200 + LSZ - $rawdata = $ParsedLyrics3['raw']['unparsed']; - while (strlen($rawdata) > 0) { - $fieldname = substr($rawdata, 0, 3); - $fieldsize = (int) substr($rawdata, 3, 5); - $ParsedLyrics3['raw'][$fieldname] = substr($rawdata, 8, $fieldsize); - $rawdata = substr($rawdata, 3 + 5 + $fieldsize); - } - - if (isset($ParsedLyrics3['raw']['IND'])) { - $i = 0; - $flagnames = array('lyrics', 'timestamps', 'inhibitrandom'); - foreach ($flagnames as $flagname) { - if (strlen($ParsedLyrics3['raw']['IND']) > $i++) { - $ParsedLyrics3['flags'][$flagname] = $this->IntString2Bool(substr($ParsedLyrics3['raw']['IND'], $i, 1 - 1)); - } - } - } - - $fieldnametranslation = array('ETT'=>'title', 'EAR'=>'artist', 'EAL'=>'album', 'INF'=>'comment', 'AUT'=>'author'); - foreach ($fieldnametranslation as $key => $value) { - if (isset($ParsedLyrics3['raw'][$key])) { - $ParsedLyrics3['comments'][$value][] = trim($ParsedLyrics3['raw'][$key]); - } - } - - if (isset($ParsedLyrics3['raw']['IMG'])) { - $imagestrings = explode("\r\n", $ParsedLyrics3['raw']['IMG']); - foreach ($imagestrings as $key => $imagestring) { - if (strpos($imagestring, '||') !== false) { - $imagearray = explode('||', $imagestring); - $ParsedLyrics3['images'][$key]['filename'] = (isset($imagearray[0]) ? $imagearray[0] : ''); - $ParsedLyrics3['images'][$key]['description'] = (isset($imagearray[1]) ? $imagearray[1] : ''); - $ParsedLyrics3['images'][$key]['timestamp'] = $this->Lyrics3Timestamp2Seconds(isset($imagearray[2]) ? $imagearray[2] : ''); - } - } - } - if (isset($ParsedLyrics3['raw']['LYR'])) { - $this->Lyrics3LyricsTimestampParse($ParsedLyrics3); - } - } else { - $info['error'][] = '"LYRICS200" expected at '.(ftell($this->getid3->fp) - 11 + $length - 9).' but found "'.substr($rawdata, strlen($rawdata) - 9, 9).'" instead'; - return false; - } - break; - - default: - $info['error'][] = 'Cannot process Lyrics3 version '.$version.' (only v1 and v2)'; - return false; - break; - } - - - if (isset($info['id3v1']['tag_offset_start']) && ($info['id3v1']['tag_offset_start'] <= $ParsedLyrics3['tag_offset_end'])) { - $info['warning'][] = 'ID3v1 tag information ignored since it appears to be a false synch in Lyrics3 tag data'; - unset($info['id3v1']); - foreach ($info['warning'] as $key => $value) { - if ($value == 'Some ID3v1 fields do not use NULL characters for padding') { - unset($info['warning'][$key]); - sort($info['warning']); - break; - } - } - } - - $info['lyrics3'] = $ParsedLyrics3; - - return true; - } - - public function Lyrics3Timestamp2Seconds($rawtimestamp) { - if (preg_match('#^\\[([0-9]{2}):([0-9]{2})\\]$#', $rawtimestamp, $regs)) { - return (int) (($regs[1] * 60) + $regs[2]); - } - return false; - } - - public function Lyrics3LyricsTimestampParse(&$Lyrics3data) { - $lyricsarray = explode("\r\n", $Lyrics3data['raw']['LYR']); - foreach ($lyricsarray as $key => $lyricline) { - $regs = array(); - unset($thislinetimestamps); - while (preg_match('#^(\\[[0-9]{2}:[0-9]{2}\\])#', $lyricline, $regs)) { - $thislinetimestamps[] = $this->Lyrics3Timestamp2Seconds($regs[0]); - $lyricline = str_replace($regs[0], '', $lyricline); - } - $notimestamplyricsarray[$key] = $lyricline; - if (isset($thislinetimestamps) && is_array($thislinetimestamps)) { - sort($thislinetimestamps); - foreach ($thislinetimestamps as $timestampkey => $timestamp) { - if (isset($Lyrics3data['synchedlyrics'][$timestamp])) { - // timestamps only have a 1-second resolution, it's possible that multiple lines - // could have the same timestamp, if so, append - $Lyrics3data['synchedlyrics'][$timestamp] .= "\r\n".$lyricline; - } else { - $Lyrics3data['synchedlyrics'][$timestamp] = $lyricline; - } - } - } - } - $Lyrics3data['unsynchedlyrics'] = implode("\r\n", $notimestamplyricsarray); - if (isset($Lyrics3data['synchedlyrics']) && is_array($Lyrics3data['synchedlyrics'])) { - ksort($Lyrics3data['synchedlyrics']); - } - return true; - } - - public function IntString2Bool($char) { - if ($char == '1') { - return true; - } elseif ($char == '0') { - return false; - } - return null; - } -} diff --git a/src/Classes/Vendor/getid3/module.tag.xmp.php b/src/Classes/Vendor/getid3/module.tag.xmp.php deleted file mode 100755 index d5ec5adca..000000000 --- a/src/Classes/Vendor/getid3/module.tag.xmp.php +++ /dev/null @@ -1,767 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// module.tag.xmp.php // -// module for analyzing XMP metadata (e.g. in JPEG files) // -// dependencies: NONE // -// // -///////////////////////////////////////////////////////////////// -// // -// Module originally written [2009-Mar-26] by // -// Nigel Barnes // -// Bundled into getID3 with permission // -// called by getID3 in module.graphic.jpg.php // -// /// -///////////////////////////////////////////////////////////////// - -/************************************************************************************************** - * SWISScenter Source Nigel Barnes - * - * Provides functions for reading information from the 'APP1' Extensible Metadata - * Platform (XMP) segment of JPEG format files. - * This XMP segment is XML based and contains the Resource Description Framework (RDF) - * data, which itself can contain the Dublin Core Metadata Initiative (DCMI) information. - * - * This code uses segments from the JPEG Metadata Toolkit project by Evan Hunter. - *************************************************************************************************/ -class Image_XMP -{ - /** - * @var string - * The name of the image file that contains the XMP fields to extract and modify. - * @see Image_XMP() - */ - public $_sFilename = null; - - /** - * @var array - * The XMP fields that were extracted from the image or updated by this class. - * @see getAllTags() - */ - public $_aXMP = array(); - - /** - * @var boolean - * True if an APP1 segment was found to contain XMP metadata. - * @see isValid() - */ - public $_bXMPParse = false; - - /** - * Returns the status of XMP parsing during instantiation - * - * You'll normally want to call this method before trying to get XMP fields. - * - * @return boolean - * Returns true if an APP1 segment was found to contain XMP metadata. - */ - public function isValid() - { - return $this->_bXMPParse; - } - - /** - * Get a copy of all XMP tags extracted from the image - * - * @return array - An array of XMP fields as it extracted by the XMPparse() function - */ - public function getAllTags() - { - return $this->_aXMP; - } - - /** - * Reads all the JPEG header segments from an JPEG image file into an array - * - * @param string $filename - the filename of the JPEG file to read - * @return array $headerdata - Array of JPEG header segments - * @return boolean FALSE - if headers could not be read - */ - public function _get_jpeg_header_data($filename) - { - // prevent refresh from aborting file operations and hosing file - ignore_user_abort(true); - - // Attempt to open the jpeg file - the at symbol supresses the error message about - // not being able to open files. The file_exists would have been used, but it - // does not work with files fetched over http or ftp. - if (is_readable($filename) && is_file($filename) && ($filehnd = fopen($filename, 'rb'))) { - // great - } else { - return false; - } - - // Read the first two characters - $data = fread($filehnd, 2); - - // Check that the first two characters are 0xFF 0xD8 (SOI - Start of image) - if ($data != "\xFF\xD8") - { - // No SOI (FF D8) at start of file - This probably isn't a JPEG file - close file and return; - echo '

    This probably is not a JPEG file

    '."\n"; - fclose($filehnd); - return false; - } - - // Read the third character - $data = fread($filehnd, 2); - - // Check that the third character is 0xFF (Start of first segment header) - if ($data{0} != "\xFF") - { - // NO FF found - close file and return - JPEG is probably corrupted - fclose($filehnd); - return false; - } - - // Flag that we havent yet hit the compressed image data - $hit_compressed_image_data = false; - - // Cycle through the file until, one of: 1) an EOI (End of image) marker is hit, - // 2) we have hit the compressed image data (no more headers are allowed after data) - // 3) or end of file is hit - - while (($data{1} != "\xD9") && (!$hit_compressed_image_data) && (!feof($filehnd))) - { - // Found a segment to look at. - // Check that the segment marker is not a Restart marker - restart markers don't have size or data after them - if ((ord($data{1}) < 0xD0) || (ord($data{1}) > 0xD7)) - { - // Segment isn't a Restart marker - // Read the next two bytes (size) - $sizestr = fread($filehnd, 2); - - // convert the size bytes to an integer - $decodedsize = unpack('nsize', $sizestr); - - // Save the start position of the data - $segdatastart = ftell($filehnd); - - // Read the segment data with length indicated by the previously read size - $segdata = fread($filehnd, $decodedsize['size'] - 2); - - // Store the segment information in the output array - $headerdata[] = array( - 'SegType' => ord($data{1}), - 'SegName' => $GLOBALS['JPEG_Segment_Names'][ord($data{1})], - 'SegDataStart' => $segdatastart, - 'SegData' => $segdata, - ); - } - - // If this is a SOS (Start Of Scan) segment, then there is no more header data - the compressed image data follows - if ($data{1} == "\xDA") - { - // Flag that we have hit the compressed image data - exit loop as no more headers available. - $hit_compressed_image_data = true; - } - else - { - // Not an SOS - Read the next two bytes - should be the segment marker for the next segment - $data = fread($filehnd, 2); - - // Check that the first byte of the two is 0xFF as it should be for a marker - if ($data{0} != "\xFF") - { - // NO FF found - close file and return - JPEG is probably corrupted - fclose($filehnd); - return false; - } - } - } - - // Close File - fclose($filehnd); - // Alow the user to abort from now on - ignore_user_abort(false); - - // Return the header data retrieved - return $headerdata; - } - - - /** - * Retrieves XMP information from an APP1 JPEG segment and returns the raw XML text as a string. - * - * @param string $filename - the filename of the JPEG file to read - * @return string $xmp_data - the string of raw XML text - * @return boolean FALSE - if an APP 1 XMP segment could not be found, or if an error occured - */ - public function _get_XMP_text($filename) - { - //Get JPEG header data - $jpeg_header_data = $this->_get_jpeg_header_data($filename); - - //Cycle through the header segments - for ($i = 0; $i < count($jpeg_header_data); $i++) - { - // If we find an APP1 header, - if (strcmp($jpeg_header_data[$i]['SegName'], 'APP1') == 0) - { - // And if it has the Adobe XMP/RDF label (http://ns.adobe.com/xap/1.0/\x00) , - if (strncmp($jpeg_header_data[$i]['SegData'], 'http://ns.adobe.com/xap/1.0/'."\x00", 29) == 0) - { - // Found a XMP/RDF block - // Return the XMP text - $xmp_data = substr($jpeg_header_data[$i]['SegData'], 29); - - return trim($xmp_data); // trim() should not be neccesary, but some files found in the wild with null-terminated block (known samples from Apple Aperture) causes problems elsewhere (see http://www.getid3.org/phpBB3/viewtopic.php?f=4&t=1153) - } - } - } - return false; - } - - /** - * Parses a string containing XMP data (XML), and returns an array - * which contains all the XMP (XML) information. - * - * @param string $xml_text - a string containing the XMP data (XML) to be parsed - * @return array $xmp_array - an array containing all xmp details retrieved. - * @return boolean FALSE - couldn't parse the XMP data - */ - public function read_XMP_array_from_text($xmltext) - { - // Check if there actually is any text to parse - if (trim($xmltext) == '') - { - return false; - } - - // Create an instance of a xml parser to parse the XML text - $xml_parser = xml_parser_create('UTF-8'); - - // Change: Fixed problem that caused the whitespace (especially newlines) to be destroyed when converting xml text to an xml array, as of revision 1.10 - - // We would like to remove unneccessary white space, but this will also - // remove things like newlines ( ) in the XML values, so white space - // will have to be removed later - if (xml_parser_set_option($xml_parser, XML_OPTION_SKIP_WHITE, 0) == false) - { - // Error setting case folding - destroy the parser and return - xml_parser_free($xml_parser); - return false; - } - - // to use XML code correctly we have to turn case folding - // (uppercasing) off. XML is case sensitive and upper - // casing is in reality XML standards violation - if (xml_parser_set_option($xml_parser, XML_OPTION_CASE_FOLDING, 0) == false) - { - // Error setting case folding - destroy the parser and return - xml_parser_free($xml_parser); - return false; - } - - // Parse the XML text into a array structure - if (xml_parse_into_struct($xml_parser, $xmltext, $values, $tags) == 0) - { - // Error Parsing XML - destroy the parser and return - xml_parser_free($xml_parser); - return false; - } - - // Destroy the xml parser - xml_parser_free($xml_parser); - - // Clear the output array - $xmp_array = array(); - - // The XMP data has now been parsed into an array ... - - // Cycle through each of the array elements - $current_property = ''; // current property being processed - $container_index = -1; // -1 = no container open, otherwise index of container content - foreach ($values as $xml_elem) - { - // Syntax and Class names - switch ($xml_elem['tag']) - { - case 'x:xmpmeta': - // only defined attribute is x:xmptk written by Adobe XMP Toolkit; value is the version of the toolkit - break; - - case 'rdf:RDF': - // required element immediately within x:xmpmeta; no data here - break; - - case 'rdf:Description': - switch ($xml_elem['type']) - { - case 'open': - case 'complete': - if (array_key_exists('attributes', $xml_elem)) - { - // rdf:Description may contain wanted attributes - foreach (array_keys($xml_elem['attributes']) as $key) - { - // Check whether we want this details from this attribute -// if (in_array($key, $GLOBALS['XMP_tag_captions'])) - if (true) - { - // Attribute wanted - $xmp_array[$key] = $xml_elem['attributes'][$key]; - } - } - } - case 'cdata': - case 'close': - break; - } - - case 'rdf:ID': - case 'rdf:nodeID': - // Attributes are ignored - break; - - case 'rdf:li': - // Property member - if ($xml_elem['type'] == 'complete') - { - if (array_key_exists('attributes', $xml_elem)) - { - // If Lang Alt (language alternatives) then ensure we take the default language - if (isset($xml_elem['attributes']['xml:lang']) && ($xml_elem['attributes']['xml:lang'] != 'x-default')) - { - break; - } - } - if ($current_property != '') - { - $xmp_array[$current_property][$container_index] = (isset($xml_elem['value']) ? $xml_elem['value'] : ''); - $container_index += 1; - } - //else unidentified attribute!! - } - break; - - case 'rdf:Seq': - case 'rdf:Bag': - case 'rdf:Alt': - // Container found - switch ($xml_elem['type']) - { - case 'open': - $container_index = 0; - break; - case 'close': - $container_index = -1; - break; - case 'cdata': - break; - } - break; - - default: - // Check whether we want the details from this attribute -// if (in_array($xml_elem['tag'], $GLOBALS['XMP_tag_captions'])) - if (true) - { - switch ($xml_elem['type']) - { - case 'open': - // open current element - $current_property = $xml_elem['tag']; - break; - - case 'close': - // close current element - $current_property = ''; - break; - - case 'complete': - // store attribute value - $xmp_array[$xml_elem['tag']] = (isset($xml_elem['attributes']) ? $xml_elem['attributes'] : (isset($xml_elem['value']) ? $xml_elem['value'] : '')); - break; - - case 'cdata': - // ignore - break; - } - } - break; - } - - } - return $xmp_array; - } - - - /** - * Constructor - * - * @param string - Name of the image file to access and extract XMP information from. - */ - public function Image_XMP($sFilename) - { - $this->_sFilename = $sFilename; - - if (is_file($this->_sFilename)) - { - // Get XMP data - $xmp_data = $this->_get_XMP_text($sFilename); - if ($xmp_data) - { - $this->_aXMP = $this->read_XMP_array_from_text($xmp_data); - $this->_bXMPParse = true; - } - } - } - -} - -/** -* Global Variable: XMP_tag_captions -* -* The Property names of all known XMP fields. -* Note: this is a full list with unrequired properties commented out. -*/ -/* -$GLOBALS['XMP_tag_captions'] = array( -// IPTC Core - 'Iptc4xmpCore:CiAdrCity', - 'Iptc4xmpCore:CiAdrCtry', - 'Iptc4xmpCore:CiAdrExtadr', - 'Iptc4xmpCore:CiAdrPcode', - 'Iptc4xmpCore:CiAdrRegion', - 'Iptc4xmpCore:CiEmailWork', - 'Iptc4xmpCore:CiTelWork', - 'Iptc4xmpCore:CiUrlWork', - 'Iptc4xmpCore:CountryCode', - 'Iptc4xmpCore:CreatorContactInfo', - 'Iptc4xmpCore:IntellectualGenre', - 'Iptc4xmpCore:Location', - 'Iptc4xmpCore:Scene', - 'Iptc4xmpCore:SubjectCode', -// Dublin Core Schema - 'dc:contributor', - 'dc:coverage', - 'dc:creator', - 'dc:date', - 'dc:description', - 'dc:format', - 'dc:identifier', - 'dc:language', - 'dc:publisher', - 'dc:relation', - 'dc:rights', - 'dc:source', - 'dc:subject', - 'dc:title', - 'dc:type', -// XMP Basic Schema - 'xmp:Advisory', - 'xmp:BaseURL', - 'xmp:CreateDate', - 'xmp:CreatorTool', - 'xmp:Identifier', - 'xmp:Label', - 'xmp:MetadataDate', - 'xmp:ModifyDate', - 'xmp:Nickname', - 'xmp:Rating', - 'xmp:Thumbnails', - 'xmpidq:Scheme', -// XMP Rights Management Schema - 'xmpRights:Certificate', - 'xmpRights:Marked', - 'xmpRights:Owner', - 'xmpRights:UsageTerms', - 'xmpRights:WebStatement', -// These are not in spec but Photoshop CS seems to use them - 'xap:Advisory', - 'xap:BaseURL', - 'xap:CreateDate', - 'xap:CreatorTool', - 'xap:Identifier', - 'xap:MetadataDate', - 'xap:ModifyDate', - 'xap:Nickname', - 'xap:Rating', - 'xap:Thumbnails', - 'xapidq:Scheme', - 'xapRights:Certificate', - 'xapRights:Copyright', - 'xapRights:Marked', - 'xapRights:Owner', - 'xapRights:UsageTerms', - 'xapRights:WebStatement', -// XMP Media Management Schema - 'xapMM:DerivedFrom', - 'xapMM:DocumentID', - 'xapMM:History', - 'xapMM:InstanceID', - 'xapMM:ManagedFrom', - 'xapMM:Manager', - 'xapMM:ManageTo', - 'xapMM:ManageUI', - 'xapMM:ManagerVariant', - 'xapMM:RenditionClass', - 'xapMM:RenditionParams', - 'xapMM:VersionID', - 'xapMM:Versions', - 'xapMM:LastURL', - 'xapMM:RenditionOf', - 'xapMM:SaveID', -// XMP Basic Job Ticket Schema - 'xapBJ:JobRef', -// XMP Paged-Text Schema - 'xmpTPg:MaxPageSize', - 'xmpTPg:NPages', - 'xmpTPg:Fonts', - 'xmpTPg:Colorants', - 'xmpTPg:PlateNames', -// Adobe PDF Schema - 'pdf:Keywords', - 'pdf:PDFVersion', - 'pdf:Producer', -// Photoshop Schema - 'photoshop:AuthorsPosition', - 'photoshop:CaptionWriter', - 'photoshop:Category', - 'photoshop:City', - 'photoshop:Country', - 'photoshop:Credit', - 'photoshop:DateCreated', - 'photoshop:Headline', - 'photoshop:History', -// Not in XMP spec - 'photoshop:Instructions', - 'photoshop:Source', - 'photoshop:State', - 'photoshop:SupplementalCategories', - 'photoshop:TransmissionReference', - 'photoshop:Urgency', -// EXIF Schemas - 'tiff:ImageWidth', - 'tiff:ImageLength', - 'tiff:BitsPerSample', - 'tiff:Compression', - 'tiff:PhotometricInterpretation', - 'tiff:Orientation', - 'tiff:SamplesPerPixel', - 'tiff:PlanarConfiguration', - 'tiff:YCbCrSubSampling', - 'tiff:YCbCrPositioning', - 'tiff:XResolution', - 'tiff:YResolution', - 'tiff:ResolutionUnit', - 'tiff:TransferFunction', - 'tiff:WhitePoint', - 'tiff:PrimaryChromaticities', - 'tiff:YCbCrCoefficients', - 'tiff:ReferenceBlackWhite', - 'tiff:DateTime', - 'tiff:ImageDescription', - 'tiff:Make', - 'tiff:Model', - 'tiff:Software', - 'tiff:Artist', - 'tiff:Copyright', - 'exif:ExifVersion', - 'exif:FlashpixVersion', - 'exif:ColorSpace', - 'exif:ComponentsConfiguration', - 'exif:CompressedBitsPerPixel', - 'exif:PixelXDimension', - 'exif:PixelYDimension', - 'exif:MakerNote', - 'exif:UserComment', - 'exif:RelatedSoundFile', - 'exif:DateTimeOriginal', - 'exif:DateTimeDigitized', - 'exif:ExposureTime', - 'exif:FNumber', - 'exif:ExposureProgram', - 'exif:SpectralSensitivity', - 'exif:ISOSpeedRatings', - 'exif:OECF', - 'exif:ShutterSpeedValue', - 'exif:ApertureValue', - 'exif:BrightnessValue', - 'exif:ExposureBiasValue', - 'exif:MaxApertureValue', - 'exif:SubjectDistance', - 'exif:MeteringMode', - 'exif:LightSource', - 'exif:Flash', - 'exif:FocalLength', - 'exif:SubjectArea', - 'exif:FlashEnergy', - 'exif:SpatialFrequencyResponse', - 'exif:FocalPlaneXResolution', - 'exif:FocalPlaneYResolution', - 'exif:FocalPlaneResolutionUnit', - 'exif:SubjectLocation', - 'exif:SensingMethod', - 'exif:FileSource', - 'exif:SceneType', - 'exif:CFAPattern', - 'exif:CustomRendered', - 'exif:ExposureMode', - 'exif:WhiteBalance', - 'exif:DigitalZoomRatio', - 'exif:FocalLengthIn35mmFilm', - 'exif:SceneCaptureType', - 'exif:GainControl', - 'exif:Contrast', - 'exif:Saturation', - 'exif:Sharpness', - 'exif:DeviceSettingDescription', - 'exif:SubjectDistanceRange', - 'exif:ImageUniqueID', - 'exif:GPSVersionID', - 'exif:GPSLatitude', - 'exif:GPSLongitude', - 'exif:GPSAltitudeRef', - 'exif:GPSAltitude', - 'exif:GPSTimeStamp', - 'exif:GPSSatellites', - 'exif:GPSStatus', - 'exif:GPSMeasureMode', - 'exif:GPSDOP', - 'exif:GPSSpeedRef', - 'exif:GPSSpeed', - 'exif:GPSTrackRef', - 'exif:GPSTrack', - 'exif:GPSImgDirectionRef', - 'exif:GPSImgDirection', - 'exif:GPSMapDatum', - 'exif:GPSDestLatitude', - 'exif:GPSDestLongitude', - 'exif:GPSDestBearingRef', - 'exif:GPSDestBearing', - 'exif:GPSDestDistanceRef', - 'exif:GPSDestDistance', - 'exif:GPSProcessingMethod', - 'exif:GPSAreaInformation', - 'exif:GPSDifferential', - 'stDim:w', - 'stDim:h', - 'stDim:unit', - 'xapGImg:height', - 'xapGImg:width', - 'xapGImg:format', - 'xapGImg:image', - 'stEvt:action', - 'stEvt:instanceID', - 'stEvt:parameters', - 'stEvt:softwareAgent', - 'stEvt:when', - 'stRef:instanceID', - 'stRef:documentID', - 'stRef:versionID', - 'stRef:renditionClass', - 'stRef:renditionParams', - 'stRef:manager', - 'stRef:managerVariant', - 'stRef:manageTo', - 'stRef:manageUI', - 'stVer:comments', - 'stVer:event', - 'stVer:modifyDate', - 'stVer:modifier', - 'stVer:version', - 'stJob:name', - 'stJob:id', - 'stJob:url', -// Exif Flash - 'exif:Fired', - 'exif:Return', - 'exif:Mode', - 'exif:Function', - 'exif:RedEyeMode', -// Exif OECF/SFR - 'exif:Columns', - 'exif:Rows', - 'exif:Names', - 'exif:Values', -// Exif CFAPattern - 'exif:Columns', - 'exif:Rows', - 'exif:Values', -// Exif DeviceSettings - 'exif:Columns', - 'exif:Rows', - 'exif:Settings', -); -*/ - -/** -* Global Variable: JPEG_Segment_Names -* -* The names of the JPEG segment markers, indexed by their marker number -*/ -$GLOBALS['JPEG_Segment_Names'] = array( - 0x01 => 'TEM', - 0x02 => 'RES', - 0xC0 => 'SOF0', - 0xC1 => 'SOF1', - 0xC2 => 'SOF2', - 0xC3 => 'SOF4', - 0xC4 => 'DHT', - 0xC5 => 'SOF5', - 0xC6 => 'SOF6', - 0xC7 => 'SOF7', - 0xC8 => 'JPG', - 0xC9 => 'SOF9', - 0xCA => 'SOF10', - 0xCB => 'SOF11', - 0xCC => 'DAC', - 0xCD => 'SOF13', - 0xCE => 'SOF14', - 0xCF => 'SOF15', - 0xD0 => 'RST0', - 0xD1 => 'RST1', - 0xD2 => 'RST2', - 0xD3 => 'RST3', - 0xD4 => 'RST4', - 0xD5 => 'RST5', - 0xD6 => 'RST6', - 0xD7 => 'RST7', - 0xD8 => 'SOI', - 0xD9 => 'EOI', - 0xDA => 'SOS', - 0xDB => 'DQT', - 0xDC => 'DNL', - 0xDD => 'DRI', - 0xDE => 'DHP', - 0xDF => 'EXP', - 0xE0 => 'APP0', - 0xE1 => 'APP1', - 0xE2 => 'APP2', - 0xE3 => 'APP3', - 0xE4 => 'APP4', - 0xE5 => 'APP5', - 0xE6 => 'APP6', - 0xE7 => 'APP7', - 0xE8 => 'APP8', - 0xE9 => 'APP9', - 0xEA => 'APP10', - 0xEB => 'APP11', - 0xEC => 'APP12', - 0xED => 'APP13', - 0xEE => 'APP14', - 0xEF => 'APP15', - 0xF0 => 'JPG0', - 0xF1 => 'JPG1', - 0xF2 => 'JPG2', - 0xF3 => 'JPG3', - 0xF4 => 'JPG4', - 0xF5 => 'JPG5', - 0xF6 => 'JPG6', - 0xF7 => 'JPG7', - 0xF8 => 'JPG8', - 0xF9 => 'JPG9', - 0xFA => 'JPG10', - 0xFB => 'JPG11', - 0xFC => 'JPG12', - 0xFD => 'JPG13', - 0xFE => 'COM', -); diff --git a/src/Classes/Vendor/getid3/write.apetag.php b/src/Classes/Vendor/getid3/write.apetag.php deleted file mode 100755 index 110c4b84f..000000000 --- a/src/Classes/Vendor/getid3/write.apetag.php +++ /dev/null @@ -1,223 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// write.apetag.php // -// module for writing APE tags // -// dependencies: module.tag.apetag.php // -// /// -///////////////////////////////////////////////////////////////// - - -getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.apetag.php', __FILE__, true); - -class getid3_write_apetag -{ - - public $filename; - public $tag_data; - public $always_preserve_replaygain = true; // ReplayGain / MP3gain tags will be copied from old tag even if not passed in data - public $warnings = array(); // any non-critical errors will be stored here - public $errors = array(); // any critical errors will be stored here - - public function getid3_write_apetag() { - return true; - } - - public function WriteAPEtag() { - // NOTE: All data passed to this function must be UTF-8 format - - $getID3 = new getID3; - $ThisFileInfo = $getID3->analyze($this->filename); - - if (isset($ThisFileInfo['ape']['tag_offset_start']) && isset($ThisFileInfo['lyrics3']['tag_offset_end'])) { - if ($ThisFileInfo['ape']['tag_offset_start'] >= $ThisFileInfo['lyrics3']['tag_offset_end']) { - // Current APE tag between Lyrics3 and ID3v1/EOF - // This break Lyrics3 functionality - if (!$this->DeleteAPEtag()) { - return false; - } - $ThisFileInfo = $getID3->analyze($this->filename); - } - } - - if ($this->always_preserve_replaygain) { - $ReplayGainTagsToPreserve = array('mp3gain_minmax', 'mp3gain_album_minmax', 'mp3gain_undo', 'replaygain_track_peak', 'replaygain_track_gain', 'replaygain_album_peak', 'replaygain_album_gain'); - foreach ($ReplayGainTagsToPreserve as $rg_key) { - if (isset($ThisFileInfo['ape']['items'][strtolower($rg_key)]['data'][0]) && !isset($this->tag_data[strtoupper($rg_key)][0])) { - $this->tag_data[strtoupper($rg_key)][0] = $ThisFileInfo['ape']['items'][strtolower($rg_key)]['data'][0]; - } - } - } - - if ($APEtag = $this->GenerateAPEtag()) { - if (is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'a+b'))) { - $oldignoreuserabort = ignore_user_abort(true); - flock($fp, LOCK_EX); - - $PostAPEdataOffset = $ThisFileInfo['avdataend']; - if (isset($ThisFileInfo['ape']['tag_offset_end'])) { - $PostAPEdataOffset = max($PostAPEdataOffset, $ThisFileInfo['ape']['tag_offset_end']); - } - if (isset($ThisFileInfo['lyrics3']['tag_offset_start'])) { - $PostAPEdataOffset = max($PostAPEdataOffset, $ThisFileInfo['lyrics3']['tag_offset_start']); - } - fseek($fp, $PostAPEdataOffset, SEEK_SET); - $PostAPEdata = ''; - if ($ThisFileInfo['filesize'] > $PostAPEdataOffset) { - $PostAPEdata = fread($fp, $ThisFileInfo['filesize'] - $PostAPEdataOffset); - } - - fseek($fp, $PostAPEdataOffset, SEEK_SET); - if (isset($ThisFileInfo['ape']['tag_offset_start'])) { - fseek($fp, $ThisFileInfo['ape']['tag_offset_start'], SEEK_SET); - } - ftruncate($fp, ftell($fp)); - fwrite($fp, $APEtag, strlen($APEtag)); - if (!empty($PostAPEdata)) { - fwrite($fp, $PostAPEdata, strlen($PostAPEdata)); - } - flock($fp, LOCK_UN); - fclose($fp); - ignore_user_abort($oldignoreuserabort); - return true; - } - } - return false; - } - - public function DeleteAPEtag() { - $getID3 = new getID3; - $ThisFileInfo = $getID3->analyze($this->filename); - if (isset($ThisFileInfo['ape']['tag_offset_start']) && isset($ThisFileInfo['ape']['tag_offset_end'])) { - if (is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'a+b'))) { - - flock($fp, LOCK_EX); - $oldignoreuserabort = ignore_user_abort(true); - - fseek($fp, $ThisFileInfo['ape']['tag_offset_end'], SEEK_SET); - $DataAfterAPE = ''; - if ($ThisFileInfo['filesize'] > $ThisFileInfo['ape']['tag_offset_end']) { - $DataAfterAPE = fread($fp, $ThisFileInfo['filesize'] - $ThisFileInfo['ape']['tag_offset_end']); - } - - ftruncate($fp, $ThisFileInfo['ape']['tag_offset_start']); - fseek($fp, $ThisFileInfo['ape']['tag_offset_start'], SEEK_SET); - - if (!empty($DataAfterAPE)) { - fwrite($fp, $DataAfterAPE, strlen($DataAfterAPE)); - } - - flock($fp, LOCK_UN); - fclose($fp); - ignore_user_abort($oldignoreuserabort); - - return true; - } - return false; - } - return true; - } - - - public function GenerateAPEtag() { - // NOTE: All data passed to this function must be UTF-8 format - - $items = array(); - if (!is_array($this->tag_data)) { - return false; - } - foreach ($this->tag_data as $key => $arrayofvalues) { - if (!is_array($arrayofvalues)) { - return false; - } - - $valuestring = ''; - foreach ($arrayofvalues as $value) { - $valuestring .= str_replace("\x00", '', $value)."\x00"; - } - $valuestring = rtrim($valuestring, "\x00"); - - // Length of the assigned value in bytes - $tagitem = getid3_lib::LittleEndian2String(strlen($valuestring), 4); - - //$tagitem .= $this->GenerateAPEtagFlags(true, true, false, 0, false); - $tagitem .= "\x00\x00\x00\x00"; - - $tagitem .= $this->CleanAPEtagItemKey($key)."\x00"; - $tagitem .= $valuestring; - - $items[] = $tagitem; - - } - - return $this->GenerateAPEtagHeaderFooter($items, true).implode('', $items).$this->GenerateAPEtagHeaderFooter($items, false); - } - - public function GenerateAPEtagHeaderFooter(&$items, $isheader=false) { - $tagdatalength = 0; - foreach ($items as $itemdata) { - $tagdatalength += strlen($itemdata); - } - - $APEheader = 'APETAGEX'; - $APEheader .= getid3_lib::LittleEndian2String(2000, 4); - $APEheader .= getid3_lib::LittleEndian2String(32 + $tagdatalength, 4); - $APEheader .= getid3_lib::LittleEndian2String(count($items), 4); - $APEheader .= $this->GenerateAPEtagFlags(true, true, $isheader, 0, false); - $APEheader .= str_repeat("\x00", 8); - - return $APEheader; - } - - public function GenerateAPEtagFlags($header=true, $footer=true, $isheader=false, $encodingid=0, $readonly=false) { - $APEtagFlags = array_fill(0, 4, 0); - if ($header) { - $APEtagFlags[0] |= 0x80; // Tag contains a header - } - if (!$footer) { - $APEtagFlags[0] |= 0x40; // Tag contains no footer - } - if ($isheader) { - $APEtagFlags[0] |= 0x20; // This is the header, not the footer - } - - // 0: Item contains text information coded in UTF-8 - // 1: Item contains binary information °) - // 2: Item is a locator of external stored information °°) - // 3: reserved - $APEtagFlags[3] |= ($encodingid << 1); - - if ($readonly) { - $APEtagFlags[3] |= 0x01; // Tag or Item is Read Only - } - - return chr($APEtagFlags[3]).chr($APEtagFlags[2]).chr($APEtagFlags[1]).chr($APEtagFlags[0]); - } - - public function CleanAPEtagItemKey($itemkey) { - $itemkey = preg_replace("#[^\x20-\x7E]#i", '', $itemkey); - - // http://www.personal.uni-jena.de/~pfk/mpp/sv8/apekey.html - switch (strtoupper($itemkey)) { - case 'EAN/UPC': - case 'ISBN': - case 'LC': - case 'ISRC': - $itemkey = strtoupper($itemkey); - break; - - default: - $itemkey = ucwords($itemkey); - break; - } - return $itemkey; - - } - -} diff --git a/src/Classes/Vendor/getid3/write.id3v1.php b/src/Classes/Vendor/getid3/write.id3v1.php deleted file mode 100755 index 86d509209..000000000 --- a/src/Classes/Vendor/getid3/write.id3v1.php +++ /dev/null @@ -1,136 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// write.id3v1.php // -// module for writing ID3v1 tags // -// dependencies: module.tag.id3v1.php // -// /// -///////////////////////////////////////////////////////////////// - -getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v1.php', __FILE__, true); - -class getid3_write_id3v1 -{ - public $filename; - public $filesize; - public $tag_data; - public $warnings = array(); // any non-critical errors will be stored here - public $errors = array(); // any critical errors will be stored here - - public function getid3_write_id3v1() { - return true; - } - - public function WriteID3v1() { - // File MUST be writeable - CHMOD(646) at least - if (!empty($this->filename) && is_readable($this->filename) && is_writable($this->filename) && is_file($this->filename)) { - $this->setRealFileSize(); - if (($this->filesize <= 0) || !getid3_lib::intValueSupported($this->filesize)) { - $this->errors[] = 'Unable to WriteID3v1('.$this->filename.') because filesize ('.$this->filesize.') is larger than '.round(PHP_INT_MAX / 1073741824).'GB'; - return false; - } - if ($fp_source = fopen($this->filename, 'r+b')) { - fseek($fp_source, -128, SEEK_END); - if (fread($fp_source, 3) == 'TAG') { - fseek($fp_source, -128, SEEK_END); // overwrite existing ID3v1 tag - } else { - fseek($fp_source, 0, SEEK_END); // append new ID3v1 tag - } - $this->tag_data['track'] = (isset($this->tag_data['track']) ? $this->tag_data['track'] : (isset($this->tag_data['track_number']) ? $this->tag_data['track_number'] : (isset($this->tag_data['tracknumber']) ? $this->tag_data['tracknumber'] : ''))); - - $new_id3v1_tag_data = getid3_id3v1::GenerateID3v1Tag( - (isset($this->tag_data['title'] ) ? $this->tag_data['title'] : ''), - (isset($this->tag_data['artist'] ) ? $this->tag_data['artist'] : ''), - (isset($this->tag_data['album'] ) ? $this->tag_data['album'] : ''), - (isset($this->tag_data['year'] ) ? $this->tag_data['year'] : ''), - (isset($this->tag_data['genreid']) ? $this->tag_data['genreid'] : ''), - (isset($this->tag_data['comment']) ? $this->tag_data['comment'] : ''), - (isset($this->tag_data['track'] ) ? $this->tag_data['track'] : '')); - fwrite($fp_source, $new_id3v1_tag_data, 128); - fclose($fp_source); - return true; - - } else { - $this->errors[] = 'Could not fopen('.$this->filename.', "r+b")'; - return false; - } - } - $this->errors[] = 'File is not writeable: '.$this->filename; - return false; - } - - public function FixID3v1Padding() { - // ID3v1 data is supposed to be padded with NULL characters, but some taggers incorrectly use spaces - // This function rewrites the ID3v1 tag with correct padding - - // Initialize getID3 engine - $getID3 = new getID3; - $getID3->option_tag_id3v2 = false; - $getID3->option_tag_apetag = false; - $getID3->option_tags_html = false; - $getID3->option_extra_info = false; - $getID3->option_tag_id3v1 = true; - $ThisFileInfo = $getID3->analyze($this->filename); - if (isset($ThisFileInfo['tags']['id3v1'])) { - foreach ($ThisFileInfo['tags']['id3v1'] as $key => $value) { - $id3v1data[$key] = implode(',', $value); - } - $this->tag_data = $id3v1data; - return $this->WriteID3v1(); - } - return false; - } - - public function RemoveID3v1() { - // File MUST be writeable - CHMOD(646) at least - if (!empty($this->filename) && is_readable($this->filename) && is_writable($this->filename) && is_file($this->filename)) { - $this->setRealFileSize(); - if (($this->filesize <= 0) || !getid3_lib::intValueSupported($this->filesize)) { - $this->errors[] = 'Unable to RemoveID3v1('.$this->filename.') because filesize ('.$this->filesize.') is larger than '.round(PHP_INT_MAX / 1073741824).'GB'; - return false; - } - if ($fp_source = fopen($this->filename, 'r+b')) { - - fseek($fp_source, -128, SEEK_END); - if (fread($fp_source, 3) == 'TAG') { - ftruncate($fp_source, $this->filesize - 128); - } else { - // no ID3v1 tag to begin with - do nothing - } - fclose($fp_source); - return true; - - } else { - $this->errors[] = 'Could not fopen('.$this->filename.', "r+b")'; - } - } else { - $this->errors[] = $this->filename.' is not writeable'; - } - return false; - } - - public function setRealFileSize() { - if (PHP_INT_MAX > 2147483647) { - $this->filesize = filesize($this->filename); - return true; - } - // 32-bit PHP will not return correct values for filesize() if file is >=2GB - // but getID3->analyze() has workarounds to get actual filesize - $getID3 = new getID3; - $getID3->option_tag_id3v1 = false; - $getID3->option_tag_id3v2 = false; - $getID3->option_tag_apetag = false; - $getID3->option_tags_html = false; - $getID3->option_extra_info = false; - $ThisFileInfo = $getID3->analyze($this->filename); - $this->filesize = $ThisFileInfo['filesize']; - return true; - } - -} diff --git a/src/Classes/Vendor/getid3/write.id3v2.php b/src/Classes/Vendor/getid3/write.id3v2.php deleted file mode 100755 index 54b8aaeda..000000000 --- a/src/Classes/Vendor/getid3/write.id3v2.php +++ /dev/null @@ -1,2049 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -/// // -// write.id3v2.php // -// module for writing ID3v2 tags // -// dependencies: module.tag.id3v2.php // -// /// -///////////////////////////////////////////////////////////////// - -getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'module.tag.id3v2.php', __FILE__, true); - -class getid3_write_id3v2 -{ - public $filename; - public $tag_data; - public $fread_buffer_size = 32768; // read buffer size in bytes - public $paddedlength = 4096; // minimum length of ID3v2 tag in bytes - public $majorversion = 3; // ID3v2 major version (2, 3 (recommended), 4) - public $minorversion = 0; // ID3v2 minor version - always 0 - public $merge_existing_data = false; // if true, merge new data with existing tags; if false, delete old tag data and only write new tags - public $id3v2_default_encodingid = 0; // default text encoding (ISO-8859-1) if not explicitly passed - public $id3v2_use_unsynchronisation = false; // the specs say it should be TRUE, but most other ID3v2-aware programs are broken if unsynchronization is used, so by default don't use it. - public $warnings = array(); // any non-critical errors will be stored here - public $errors = array(); // any critical errors will be stored here - - public function getid3_write_id3v2() { - return true; - } - - public function WriteID3v2() { - // File MUST be writeable - CHMOD(646) at least. It's best if the - // directory is also writeable, because that method is both faster and less susceptible to errors. - - if (!empty($this->filename) && (is_writeable($this->filename) || (!file_exists($this->filename) && is_writeable(dirname($this->filename))))) { - // Initialize getID3 engine - $getID3 = new getID3; - $OldThisFileInfo = $getID3->analyze($this->filename); - if (!getid3_lib::intValueSupported($OldThisFileInfo['filesize'])) { - $this->errors[] = 'Unable to write ID3v2 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB'; - fclose($fp_source); - return false; - } - if ($this->merge_existing_data) { - // merge with existing data - if (!empty($OldThisFileInfo['id3v2'])) { - $this->tag_data = $this->array_join_merge($OldThisFileInfo['id3v2'], $this->tag_data); - } - } - $this->paddedlength = (isset($OldThisFileInfo['id3v2']['headerlength']) ? max($OldThisFileInfo['id3v2']['headerlength'], $this->paddedlength) : $this->paddedlength); - - if ($NewID3v2Tag = $this->GenerateID3v2Tag()) { - - if (file_exists($this->filename) && is_writeable($this->filename) && isset($OldThisFileInfo['id3v2']['headerlength']) && ($OldThisFileInfo['id3v2']['headerlength'] == strlen($NewID3v2Tag))) { - - // best and fastest method - insert-overwrite existing tag (padded to length of old tag if neccesary) - if (file_exists($this->filename)) { - - if (is_readable($this->filename) && is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'r+b'))) { - rewind($fp); - fwrite($fp, $NewID3v2Tag, strlen($NewID3v2Tag)); - fclose($fp); - } else { - $this->errors[] = 'Could not fopen("'.$this->filename.'", "r+b")'; - } - - } else { - - if (is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'wb'))) { - rewind($fp); - fwrite($fp, $NewID3v2Tag, strlen($NewID3v2Tag)); - fclose($fp); - } else { - $this->errors[] = 'Could not fopen("'.$this->filename.'", "wb")'; - } - - } - - } else { - - if ($tempfilename = tempnam(GETID3_TEMP_DIR, 'getID3')) { - if (is_readable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'rb'))) { - if (is_writable($tempfilename) && is_file($tempfilename) && ($fp_temp = fopen($tempfilename, 'wb'))) { - - fwrite($fp_temp, $NewID3v2Tag, strlen($NewID3v2Tag)); - - rewind($fp_source); - if (!empty($OldThisFileInfo['avdataoffset'])) { - fseek($fp_source, $OldThisFileInfo['avdataoffset'], SEEK_SET); - } - - while ($buffer = fread($fp_source, $this->fread_buffer_size)) { - fwrite($fp_temp, $buffer, strlen($buffer)); - } - - fclose($fp_temp); - fclose($fp_source); - copy($tempfilename, $this->filename); - unlink($tempfilename); - return true; - - } else { - $this->errors[] = 'Could not fopen("'.$tempfilename.'", "wb")'; - } - fclose($fp_source); - - } else { - $this->errors[] = 'Could not fopen("'.$this->filename.'", "rb")'; - } - } - return false; - - } - - } else { - - $this->errors[] = '$this->GenerateID3v2Tag() failed'; - - } - - if (!empty($this->errors)) { - return false; - } - return true; - } else { - $this->errors[] = 'WriteID3v2() failed: !is_writeable('.$this->filename.')'; - } - return false; - } - - public function RemoveID3v2() { - // File MUST be writeable - CHMOD(646) at least. It's best if the - // directory is also writeable, because that method is both faster and less susceptible to errors. - if (is_writeable(dirname($this->filename))) { - - // preferred method - only one copying operation, minimal chance of corrupting - // original file if script is interrupted, but required directory to be writeable - if (is_readable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'rb'))) { - - // Initialize getID3 engine - $getID3 = new getID3; - $OldThisFileInfo = $getID3->analyze($this->filename); - if (!getid3_lib::intValueSupported($OldThisFileInfo['filesize'])) { - $this->errors[] = 'Unable to remove ID3v2 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB'; - fclose($fp_source); - return false; - } - rewind($fp_source); - if ($OldThisFileInfo['avdataoffset'] !== false) { - fseek($fp_source, $OldThisFileInfo['avdataoffset'], SEEK_SET); - } - if (is_writable($this->filename) && is_file($this->filename) && ($fp_temp = fopen($this->filename.'getid3tmp', 'w+b'))) { - while ($buffer = fread($fp_source, $this->fread_buffer_size)) { - fwrite($fp_temp, $buffer, strlen($buffer)); - } - fclose($fp_temp); - } else { - $this->errors[] = 'Could not fopen("'.$this->filename.'getid3tmp", "w+b")'; - } - fclose($fp_source); - } else { - $this->errors[] = 'Could not fopen("'.$this->filename.'", "rb")'; - } - if (file_exists($this->filename)) { - unlink($this->filename); - } - rename($this->filename.'getid3tmp', $this->filename); - - } elseif (is_writable($this->filename)) { - - // less desirable alternate method - double-copies the file, overwrites original file - // and could corrupt source file if the script is interrupted or an error occurs. - if (is_readable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'rb'))) { - - // Initialize getID3 engine - $getID3 = new getID3; - $OldThisFileInfo = $getID3->analyze($this->filename); - if (!getid3_lib::intValueSupported($OldThisFileInfo['filesize'])) { - $this->errors[] = 'Unable to remove ID3v2 because file is larger than '.round(PHP_INT_MAX / 1073741824).'GB'; - fclose($fp_source); - return false; - } - rewind($fp_source); - if ($OldThisFileInfo['avdataoffset'] !== false) { - fseek($fp_source, $OldThisFileInfo['avdataoffset'], SEEK_SET); - } - if ($fp_temp = tmpfile()) { - while ($buffer = fread($fp_source, $this->fread_buffer_size)) { - fwrite($fp_temp, $buffer, strlen($buffer)); - } - fclose($fp_source); - if (is_writable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'wb'))) { - rewind($fp_temp); - while ($buffer = fread($fp_temp, $this->fread_buffer_size)) { - fwrite($fp_source, $buffer, strlen($buffer)); - } - fseek($fp_temp, -128, SEEK_END); - fclose($fp_source); - } else { - $this->errors[] = 'Could not fopen("'.$this->filename.'", "wb")'; - } - fclose($fp_temp); - } else { - $this->errors[] = 'Could not create tmpfile()'; - } - } else { - $this->errors[] = 'Could not fopen("'.$this->filename.'", "rb")'; - } - - } else { - - $this->errors[] = 'Directory and file both not writeable'; - - } - - if (!empty($this->errors)) { - return false; - } - return true; - } - - - public function GenerateID3v2TagFlags($flags) { - switch ($this->majorversion) { - case 4: - // %abcd0000 - $flag = (!empty($flags['unsynchronisation']) ? '1' : '0'); // a - Unsynchronisation - $flag .= (!empty($flags['extendedheader'] ) ? '1' : '0'); // b - Extended header - $flag .= (!empty($flags['experimental'] ) ? '1' : '0'); // c - Experimental indicator - $flag .= (!empty($flags['footer'] ) ? '1' : '0'); // d - Footer present - $flag .= '0000'; - break; - - case 3: - // %abc00000 - $flag = (!empty($flags['unsynchronisation']) ? '1' : '0'); // a - Unsynchronisation - $flag .= (!empty($flags['extendedheader'] ) ? '1' : '0'); // b - Extended header - $flag .= (!empty($flags['experimental'] ) ? '1' : '0'); // c - Experimental indicator - $flag .= '00000'; - break; - - case 2: - // %ab000000 - $flag = (!empty($flags['unsynchronisation']) ? '1' : '0'); // a - Unsynchronisation - $flag .= (!empty($flags['compression'] ) ? '1' : '0'); // b - Compression - $flag .= '000000'; - break; - - default: - return false; - break; - } - return chr(bindec($flag)); - } - - - public function GenerateID3v2FrameFlags($TagAlter=false, $FileAlter=false, $ReadOnly=false, $Compression=false, $Encryption=false, $GroupingIdentity=false, $Unsynchronisation=false, $DataLengthIndicator=false) { - switch ($this->majorversion) { - case 4: - // %0abc0000 %0h00kmnp - $flag1 = '0'; - $flag1 .= $TagAlter ? '1' : '0'; // a - Tag alter preservation (true == discard) - $flag1 .= $FileAlter ? '1' : '0'; // b - File alter preservation (true == discard) - $flag1 .= $ReadOnly ? '1' : '0'; // c - Read only (true == read only) - $flag1 .= '0000'; - - $flag2 = '0'; - $flag2 .= $GroupingIdentity ? '1' : '0'; // h - Grouping identity (true == contains group information) - $flag2 .= '00'; - $flag2 .= $Compression ? '1' : '0'; // k - Compression (true == compressed) - $flag2 .= $Encryption ? '1' : '0'; // m - Encryption (true == encrypted) - $flag2 .= $Unsynchronisation ? '1' : '0'; // n - Unsynchronisation (true == unsynchronised) - $flag2 .= $DataLengthIndicator ? '1' : '0'; // p - Data length indicator (true == data length indicator added) - break; - - case 3: - // %abc00000 %ijk00000 - $flag1 = $TagAlter ? '1' : '0'; // a - Tag alter preservation (true == discard) - $flag1 .= $FileAlter ? '1' : '0'; // b - File alter preservation (true == discard) - $flag1 .= $ReadOnly ? '1' : '0'; // c - Read only (true == read only) - $flag1 .= '00000'; - - $flag2 = $Compression ? '1' : '0'; // i - Compression (true == compressed) - $flag2 .= $Encryption ? '1' : '0'; // j - Encryption (true == encrypted) - $flag2 .= $GroupingIdentity ? '1' : '0'; // k - Grouping identity (true == contains group information) - $flag2 .= '00000'; - break; - - default: - return false; - break; - - } - return chr(bindec($flag1)).chr(bindec($flag2)); - } - - public function GenerateID3v2FrameData($frame_name, $source_data_array) { - if (!getid3_id3v2::IsValidID3v2FrameName($frame_name, $this->majorversion)) { - return false; - } - $framedata = ''; - - if (($this->majorversion < 3) || ($this->majorversion > 4)) { - - $this->errors[] = 'Only ID3v2.3 and ID3v2.4 are supported in GenerateID3v2FrameData()'; - - } else { // $this->majorversion 3 or 4 - - switch ($frame_name) { - case 'UFID': - // 4.1 UFID Unique file identifier - // Owner identifier $00 - // Identifier - if (strlen($source_data_array['data']) > 64) { - $this->errors[] = 'Identifier not allowed to be longer than 64 bytes in '.$frame_name.' (supplied data was '.strlen($source_data_array['data']).' bytes long)'; - } else { - $framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00"; - $framedata .= substr($source_data_array['data'], 0, 64); // max 64 bytes - truncate anything longer - } - break; - - case 'TXXX': - // 4.2.2 TXXX User defined text information frame - // Text encoding $xx - // Description $00 (00) - // Value - $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid); - if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'], $this->majorversion)) { - $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion; - } else { - $framedata .= chr($source_data_array['encodingid']); - $framedata .= $source_data_array['description'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']); - $framedata .= $source_data_array['data']; - } - break; - - case 'WXXX': - // 4.3.2 WXXX User defined URL link frame - // Text encoding $xx - // Description $00 (00) - // URL - $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid); - if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'], $this->majorversion)) { - $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion; - } elseif (!isset($source_data_array['data']) || !$this->IsValidURL($source_data_array['data'], false, false)) { - //$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')'; - // probably should be an error, need to rewrite IsValidURL() to handle other encodings - $this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')'; - } else { - $framedata .= chr($source_data_array['encodingid']); - $framedata .= $source_data_array['description'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']); - $framedata .= $source_data_array['data']; - } - break; - - case 'IPLS': - // 4.4 IPLS Involved people list (ID3v2.3 only) - // Text encoding $xx - // People list strings - $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid); - if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'], $this->majorversion)) { - $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion; - } else { - $framedata .= chr($source_data_array['encodingid']); - $framedata .= $source_data_array['data']; - } - break; - - case 'MCDI': - // 4.4 MCDI Music CD identifier - // CD TOC - $framedata .= $source_data_array['data']; - break; - - case 'ETCO': - // 4.5 ETCO Event timing codes - // Time stamp format $xx - // Where time stamp format is: - // $01 (32-bit value) MPEG frames from beginning of file - // $02 (32-bit value) milliseconds from beginning of file - // Followed by a list of key events in the following format: - // Type of event $xx - // Time stamp $xx (xx ...) - // The 'Time stamp' is set to zero if directly at the beginning of the sound - // or after the previous event. All events MUST be sorted in chronological order. - if (($source_data_array['timestampformat'] > 2) || ($source_data_array['timestampformat'] < 1)) { - $this->errors[] = 'Invalid Time Stamp Format byte in '.$frame_name.' ('.$source_data_array['timestampformat'].')'; - } else { - $framedata .= chr($source_data_array['timestampformat']); - foreach ($source_data_array as $key => $val) { - if (!$this->ID3v2IsValidETCOevent($val['typeid'])) { - $this->errors[] = 'Invalid Event Type byte in '.$frame_name.' ('.$val['typeid'].')'; - } elseif (($key != 'timestampformat') && ($key != 'flags')) { - if (($val['timestamp'] > 0) && ($previousETCOtimestamp >= $val['timestamp'])) { - // The 'Time stamp' is set to zero if directly at the beginning of the sound - // or after the previous event. All events MUST be sorted in chronological order. - $this->errors[] = 'Out-of-order timestamp in '.$frame_name.' ('.$val['timestamp'].') for Event Type ('.$val['typeid'].')'; - } else { - $framedata .= chr($val['typeid']); - $framedata .= getid3_lib::BigEndian2String($val['timestamp'], 4, false); - } - } - } - } - break; - - case 'MLLT': - // 4.6 MLLT MPEG location lookup table - // MPEG frames between reference $xx xx - // Bytes between reference $xx xx xx - // Milliseconds between reference $xx xx xx - // Bits for bytes deviation $xx - // Bits for milliseconds dev. $xx - // Then for every reference the following data is included; - // Deviation in bytes %xxx.... - // Deviation in milliseconds %xxx.... - if (($source_data_array['framesbetweenreferences'] > 0) && ($source_data_array['framesbetweenreferences'] <= 65535)) { - $framedata .= getid3_lib::BigEndian2String($source_data_array['framesbetweenreferences'], 2, false); - } else { - $this->errors[] = 'Invalid MPEG Frames Between References in '.$frame_name.' ('.$source_data_array['framesbetweenreferences'].')'; - } - if (($source_data_array['bytesbetweenreferences'] > 0) && ($source_data_array['bytesbetweenreferences'] <= 16777215)) { - $framedata .= getid3_lib::BigEndian2String($source_data_array['bytesbetweenreferences'], 3, false); - } else { - $this->errors[] = 'Invalid bytes Between References in '.$frame_name.' ('.$source_data_array['bytesbetweenreferences'].')'; - } - if (($source_data_array['msbetweenreferences'] > 0) && ($source_data_array['msbetweenreferences'] <= 16777215)) { - $framedata .= getid3_lib::BigEndian2String($source_data_array['msbetweenreferences'], 3, false); - } else { - $this->errors[] = 'Invalid Milliseconds Between References in '.$frame_name.' ('.$source_data_array['msbetweenreferences'].')'; - } - if (!$this->IsWithinBitRange($source_data_array['bitsforbytesdeviation'], 8, false)) { - if (($source_data_array['bitsforbytesdeviation'] % 4) == 0) { - $framedata .= chr($source_data_array['bitsforbytesdeviation']); - } else { - $this->errors[] = 'Bits For Bytes Deviation in '.$frame_name.' ('.$source_data_array['bitsforbytesdeviation'].') must be a multiple of 4.'; - } - } else { - $this->errors[] = 'Invalid Bits For Bytes Deviation in '.$frame_name.' ('.$source_data_array['bitsforbytesdeviation'].')'; - } - if (!$this->IsWithinBitRange($source_data_array['bitsformsdeviation'], 8, false)) { - if (($source_data_array['bitsformsdeviation'] % 4) == 0) { - $framedata .= chr($source_data_array['bitsformsdeviation']); - } else { - $this->errors[] = 'Bits For Milliseconds Deviation in '.$frame_name.' ('.$source_data_array['bitsforbytesdeviation'].') must be a multiple of 4.'; - } - } else { - $this->errors[] = 'Invalid Bits For Milliseconds Deviation in '.$frame_name.' ('.$source_data_array['bitsformsdeviation'].')'; - } - foreach ($source_data_array as $key => $val) { - if (($key != 'framesbetweenreferences') && ($key != 'bytesbetweenreferences') && ($key != 'msbetweenreferences') && ($key != 'bitsforbytesdeviation') && ($key != 'bitsformsdeviation') && ($key != 'flags')) { - $unwrittenbitstream .= str_pad(getid3_lib::Dec2Bin($val['bytedeviation']), $source_data_array['bitsforbytesdeviation'], '0', STR_PAD_LEFT); - $unwrittenbitstream .= str_pad(getid3_lib::Dec2Bin($val['msdeviation']), $source_data_array['bitsformsdeviation'], '0', STR_PAD_LEFT); - } - } - for ($i = 0; $i < strlen($unwrittenbitstream); $i += 8) { - $highnibble = bindec(substr($unwrittenbitstream, $i, 4)) << 4; - $lownibble = bindec(substr($unwrittenbitstream, $i + 4, 4)); - $framedata .= chr($highnibble & $lownibble); - } - break; - - case 'SYTC': - // 4.7 SYTC Synchronised tempo codes - // Time stamp format $xx - // Tempo data - // Where time stamp format is: - // $01 (32-bit value) MPEG frames from beginning of file - // $02 (32-bit value) milliseconds from beginning of file - if (($source_data_array['timestampformat'] > 2) || ($source_data_array['timestampformat'] < 1)) { - $this->errors[] = 'Invalid Time Stamp Format byte in '.$frame_name.' ('.$source_data_array['timestampformat'].')'; - } else { - $framedata .= chr($source_data_array['timestampformat']); - foreach ($source_data_array as $key => $val) { - if (!$this->ID3v2IsValidETCOevent($val['typeid'])) { - $this->errors[] = 'Invalid Event Type byte in '.$frame_name.' ('.$val['typeid'].')'; - } elseif (($key != 'timestampformat') && ($key != 'flags')) { - if (($val['tempo'] < 0) || ($val['tempo'] > 510)) { - $this->errors[] = 'Invalid Tempo (max = 510) in '.$frame_name.' ('.$val['tempo'].') at timestamp ('.$val['timestamp'].')'; - } else { - if ($val['tempo'] > 255) { - $framedata .= chr(255); - $val['tempo'] -= 255; - } - $framedata .= chr($val['tempo']); - $framedata .= getid3_lib::BigEndian2String($val['timestamp'], 4, false); - } - } - } - } - break; - - case 'USLT': - // 4.8 USLT Unsynchronised lyric/text transcription - // Text encoding $xx - // Language $xx xx xx - // Content descriptor $00 (00) - // Lyrics/text - $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid); - if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) { - $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion; - } elseif (getid3_id3v2::LanguageLookup($source_data_array['language'], true) == '') { - $this->errors[] = 'Invalid Language in '.$frame_name.' ('.$source_data_array['language'].')'; - } else { - $framedata .= chr($source_data_array['encodingid']); - $framedata .= strtolower($source_data_array['language']); - $framedata .= $source_data_array['description'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']); - $framedata .= $source_data_array['data']; - } - break; - - case 'SYLT': - // 4.9 SYLT Synchronised lyric/text - // Text encoding $xx - // Language $xx xx xx - // Time stamp format $xx - // $01 (32-bit value) MPEG frames from beginning of file - // $02 (32-bit value) milliseconds from beginning of file - // Content type $xx - // Content descriptor $00 (00) - // Terminated text to be synced (typically a syllable) - // Sync identifier (terminator to above string) $00 (00) - // Time stamp $xx (xx ...) - $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid); - if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) { - $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion; - } elseif (getid3_id3v2::LanguageLookup($source_data_array['language'], true) == '') { - $this->errors[] = 'Invalid Language in '.$frame_name.' ('.$source_data_array['language'].')'; - } elseif (($source_data_array['timestampformat'] > 2) || ($source_data_array['timestampformat'] < 1)) { - $this->errors[] = 'Invalid Time Stamp Format byte in '.$frame_name.' ('.$source_data_array['timestampformat'].')'; - } elseif (!$this->ID3v2IsValidSYLTtype($source_data_array['contenttypeid'])) { - $this->errors[] = 'Invalid Content Type byte in '.$frame_name.' ('.$source_data_array['contenttypeid'].')'; - } elseif (!is_array($source_data_array['data'])) { - $this->errors[] = 'Invalid Lyric/Timestamp data in '.$frame_name.' (must be an array)'; - } else { - $framedata .= chr($source_data_array['encodingid']); - $framedata .= strtolower($source_data_array['language']); - $framedata .= chr($source_data_array['timestampformat']); - $framedata .= chr($source_data_array['contenttypeid']); - $framedata .= $source_data_array['description'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']); - ksort($source_data_array['data']); - foreach ($source_data_array['data'] as $key => $val) { - $framedata .= $val['data'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']); - $framedata .= getid3_lib::BigEndian2String($val['timestamp'], 4, false); - } - } - break; - - case 'COMM': - // 4.10 COMM Comments - // Text encoding $xx - // Language $xx xx xx - // Short content descrip. $00 (00) - // The actual text - $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid); - if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) { - $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion; - } elseif (getid3_id3v2::LanguageLookup($source_data_array['language'], true) == '') { - $this->errors[] = 'Invalid Language in '.$frame_name.' ('.$source_data_array['language'].')'; - } else { - $framedata .= chr($source_data_array['encodingid']); - $framedata .= strtolower($source_data_array['language']); - $framedata .= $source_data_array['description'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']); - $framedata .= $source_data_array['data']; - } - break; - - case 'RVA2': - // 4.11 RVA2 Relative volume adjustment (2) (ID3v2.4+ only) - // Identification $00 - // The 'identification' string is used to identify the situation and/or - // device where this adjustment should apply. The following is then - // repeated for every channel: - // Type of channel $xx - // Volume adjustment $xx xx - // Bits representing peak $xx - // Peak volume $xx (xx ...) - $framedata .= str_replace("\x00", '', $source_data_array['description'])."\x00"; - foreach ($source_data_array as $key => $val) { - if ($key != 'description') { - $framedata .= chr($val['channeltypeid']); - $framedata .= getid3_lib::BigEndian2String($val['volumeadjust'], 2, false, true); // signed 16-bit - if (!$this->IsWithinBitRange($source_data_array['bitspeakvolume'], 8, false)) { - $framedata .= chr($val['bitspeakvolume']); - if ($val['bitspeakvolume'] > 0) { - $framedata .= getid3_lib::BigEndian2String($val['peakvolume'], ceil($val['bitspeakvolume'] / 8), false, false); - } - } else { - $this->errors[] = 'Invalid Bits Representing Peak Volume in '.$frame_name.' ('.$val['bitspeakvolume'].') (range = 0 to 255)'; - } - } - } - break; - - case 'RVAD': - // 4.12 RVAD Relative volume adjustment (ID3v2.3 only) - // Increment/decrement %00fedcba - // Bits used for volume descr. $xx - // Relative volume change, right $xx xx (xx ...) // a - // Relative volume change, left $xx xx (xx ...) // b - // Peak volume right $xx xx (xx ...) - // Peak volume left $xx xx (xx ...) - // Relative volume change, right back $xx xx (xx ...) // c - // Relative volume change, left back $xx xx (xx ...) // d - // Peak volume right back $xx xx (xx ...) - // Peak volume left back $xx xx (xx ...) - // Relative volume change, center $xx xx (xx ...) // e - // Peak volume center $xx xx (xx ...) - // Relative volume change, bass $xx xx (xx ...) // f - // Peak volume bass $xx xx (xx ...) - if (!$this->IsWithinBitRange($source_data_array['bitsvolume'], 8, false)) { - $this->errors[] = 'Invalid Bits For Volume Description byte in '.$frame_name.' ('.$source_data_array['bitsvolume'].') (range = 1 to 255)'; - } else { - $incdecflag .= '00'; - $incdecflag .= $source_data_array['incdec']['right'] ? '1' : '0'; // a - Relative volume change, right - $incdecflag .= $source_data_array['incdec']['left'] ? '1' : '0'; // b - Relative volume change, left - $incdecflag .= $source_data_array['incdec']['rightrear'] ? '1' : '0'; // c - Relative volume change, right back - $incdecflag .= $source_data_array['incdec']['leftrear'] ? '1' : '0'; // d - Relative volume change, left back - $incdecflag .= $source_data_array['incdec']['center'] ? '1' : '0'; // e - Relative volume change, center - $incdecflag .= $source_data_array['incdec']['bass'] ? '1' : '0'; // f - Relative volume change, bass - $framedata .= chr(bindec($incdecflag)); - $framedata .= chr($source_data_array['bitsvolume']); - $framedata .= getid3_lib::BigEndian2String($source_data_array['volumechange']['right'], ceil($source_data_array['bitsvolume'] / 8), false); - $framedata .= getid3_lib::BigEndian2String($source_data_array['volumechange']['left'], ceil($source_data_array['bitsvolume'] / 8), false); - $framedata .= getid3_lib::BigEndian2String($source_data_array['peakvolume']['right'], ceil($source_data_array['bitsvolume'] / 8), false); - $framedata .= getid3_lib::BigEndian2String($source_data_array['peakvolume']['left'], ceil($source_data_array['bitsvolume'] / 8), false); - if ($source_data_array['volumechange']['rightrear'] || $source_data_array['volumechange']['leftrear'] || - $source_data_array['peakvolume']['rightrear'] || $source_data_array['peakvolume']['leftrear'] || - $source_data_array['volumechange']['center'] || $source_data_array['peakvolume']['center'] || - $source_data_array['volumechange']['bass'] || $source_data_array['peakvolume']['bass']) { - $framedata .= getid3_lib::BigEndian2String($source_data_array['volumechange']['rightrear'], ceil($source_data_array['bitsvolume']/8), false); - $framedata .= getid3_lib::BigEndian2String($source_data_array['volumechange']['leftrear'], ceil($source_data_array['bitsvolume']/8), false); - $framedata .= getid3_lib::BigEndian2String($source_data_array['peakvolume']['rightrear'], ceil($source_data_array['bitsvolume']/8), false); - $framedata .= getid3_lib::BigEndian2String($source_data_array['peakvolume']['leftrear'], ceil($source_data_array['bitsvolume']/8), false); - } - if ($source_data_array['volumechange']['center'] || $source_data_array['peakvolume']['center'] || - $source_data_array['volumechange']['bass'] || $source_data_array['peakvolume']['bass']) { - $framedata .= getid3_lib::BigEndian2String($source_data_array['volumechange']['center'], ceil($source_data_array['bitsvolume']/8), false); - $framedata .= getid3_lib::BigEndian2String($source_data_array['peakvolume']['center'], ceil($source_data_array['bitsvolume']/8), false); - } - if ($source_data_array['volumechange']['bass'] || $source_data_array['peakvolume']['bass']) { - $framedata .= getid3_lib::BigEndian2String($source_data_array['volumechange']['bass'], ceil($source_data_array['bitsvolume']/8), false); - $framedata .= getid3_lib::BigEndian2String($source_data_array['peakvolume']['bass'], ceil($source_data_array['bitsvolume']/8), false); - } - } - break; - - case 'EQU2': - // 4.12 EQU2 Equalisation (2) (ID3v2.4+ only) - // Interpolation method $xx - // $00 Band - // $01 Linear - // Identification $00 - // The following is then repeated for every adjustment point - // Frequency $xx xx - // Volume adjustment $xx xx - if (($source_data_array['interpolationmethod'] < 0) || ($source_data_array['interpolationmethod'] > 1)) { - $this->errors[] = 'Invalid Interpolation Method byte in '.$frame_name.' ('.$source_data_array['interpolationmethod'].') (valid = 0 or 1)'; - } else { - $framedata .= chr($source_data_array['interpolationmethod']); - $framedata .= str_replace("\x00", '', $source_data_array['description'])."\x00"; - foreach ($source_data_array['data'] as $key => $val) { - $framedata .= getid3_lib::BigEndian2String(intval(round($key * 2)), 2, false); - $framedata .= getid3_lib::BigEndian2String($val, 2, false, true); // signed 16-bit - } - } - break; - - case 'EQUA': - // 4.12 EQUA Equalisation (ID3v2.3 only) - // Adjustment bits $xx - // This is followed by 2 bytes + ('adjustment bits' rounded up to the - // nearest byte) for every equalisation band in the following format, - // giving a frequency range of 0 - 32767Hz: - // Increment/decrement %x (MSB of the Frequency) - // Frequency (lower 15 bits) - // Adjustment $xx (xx ...) - if (!$this->IsWithinBitRange($source_data_array['bitsvolume'], 8, false)) { - $this->errors[] = 'Invalid Adjustment Bits byte in '.$frame_name.' ('.$source_data_array['bitsvolume'].') (range = 1 to 255)'; - } else { - $framedata .= chr($source_data_array['adjustmentbits']); - foreach ($source_data_array as $key => $val) { - if ($key != 'bitsvolume') { - if (($key > 32767) || ($key < 0)) { - $this->errors[] = 'Invalid Frequency in '.$frame_name.' ('.$key.') (range = 0 to 32767)'; - } else { - if ($val >= 0) { - // put MSB of frequency to 1 if increment, 0 if decrement - $key |= 0x8000; - } - $framedata .= getid3_lib::BigEndian2String($key, 2, false); - $framedata .= getid3_lib::BigEndian2String($val, ceil($source_data_array['adjustmentbits'] / 8), false); - } - } - } - } - break; - - case 'RVRB': - // 4.13 RVRB Reverb - // Reverb left (ms) $xx xx - // Reverb right (ms) $xx xx - // Reverb bounces, left $xx - // Reverb bounces, right $xx - // Reverb feedback, left to left $xx - // Reverb feedback, left to right $xx - // Reverb feedback, right to right $xx - // Reverb feedback, right to left $xx - // Premix left to right $xx - // Premix right to left $xx - if (!$this->IsWithinBitRange($source_data_array['left'], 16, false)) { - $this->errors[] = 'Invalid Reverb Left in '.$frame_name.' ('.$source_data_array['left'].') (range = 0 to 65535)'; - } elseif (!$this->IsWithinBitRange($source_data_array['right'], 16, false)) { - $this->errors[] = 'Invalid Reverb Left in '.$frame_name.' ('.$source_data_array['right'].') (range = 0 to 65535)'; - } elseif (!$this->IsWithinBitRange($source_data_array['bouncesL'], 8, false)) { - $this->errors[] = 'Invalid Reverb Bounces, Left in '.$frame_name.' ('.$source_data_array['bouncesL'].') (range = 0 to 255)'; - } elseif (!$this->IsWithinBitRange($source_data_array['bouncesR'], 8, false)) { - $this->errors[] = 'Invalid Reverb Bounces, Right in '.$frame_name.' ('.$source_data_array['bouncesR'].') (range = 0 to 255)'; - } elseif (!$this->IsWithinBitRange($source_data_array['feedbackLL'], 8, false)) { - $this->errors[] = 'Invalid Reverb Feedback, Left-To-Left in '.$frame_name.' ('.$source_data_array['feedbackLL'].') (range = 0 to 255)'; - } elseif (!$this->IsWithinBitRange($source_data_array['feedbackLR'], 8, false)) { - $this->errors[] = 'Invalid Reverb Feedback, Left-To-Right in '.$frame_name.' ('.$source_data_array['feedbackLR'].') (range = 0 to 255)'; - } elseif (!$this->IsWithinBitRange($source_data_array['feedbackRR'], 8, false)) { - $this->errors[] = 'Invalid Reverb Feedback, Right-To-Right in '.$frame_name.' ('.$source_data_array['feedbackRR'].') (range = 0 to 255)'; - } elseif (!$this->IsWithinBitRange($source_data_array['feedbackRL'], 8, false)) { - $this->errors[] = 'Invalid Reverb Feedback, Right-To-Left in '.$frame_name.' ('.$source_data_array['feedbackRL'].') (range = 0 to 255)'; - } elseif (!$this->IsWithinBitRange($source_data_array['premixLR'], 8, false)) { - $this->errors[] = 'Invalid Premix, Left-To-Right in '.$frame_name.' ('.$source_data_array['premixLR'].') (range = 0 to 255)'; - } elseif (!$this->IsWithinBitRange($source_data_array['premixRL'], 8, false)) { - $this->errors[] = 'Invalid Premix, Right-To-Left in '.$frame_name.' ('.$source_data_array['premixRL'].') (range = 0 to 255)'; - } else { - $framedata .= getid3_lib::BigEndian2String($source_data_array['left'], 2, false); - $framedata .= getid3_lib::BigEndian2String($source_data_array['right'], 2, false); - $framedata .= chr($source_data_array['bouncesL']); - $framedata .= chr($source_data_array['bouncesR']); - $framedata .= chr($source_data_array['feedbackLL']); - $framedata .= chr($source_data_array['feedbackLR']); - $framedata .= chr($source_data_array['feedbackRR']); - $framedata .= chr($source_data_array['feedbackRL']); - $framedata .= chr($source_data_array['premixLR']); - $framedata .= chr($source_data_array['premixRL']); - } - break; - - case 'APIC': - // 4.14 APIC Attached picture - // Text encoding $xx - // MIME type $00 - // Picture type $xx - // Description $00 (00) - // Picture data - $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid); - if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) { - $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion; - } elseif (!$this->ID3v2IsValidAPICpicturetype($source_data_array['picturetypeid'])) { - $this->errors[] = 'Invalid Picture Type byte in '.$frame_name.' ('.$source_data_array['picturetypeid'].') for ID3v2.'.$this->majorversion; - } elseif (($this->majorversion >= 3) && (!$this->ID3v2IsValidAPICimageformat($source_data_array['mime']))) { - $this->errors[] = 'Invalid MIME Type in '.$frame_name.' ('.$source_data_array['mime'].') for ID3v2.'.$this->majorversion; - } elseif (($source_data_array['mime'] == '-->') && (!$this->IsValidURL($source_data_array['data'], false, false))) { - //$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')'; - // probably should be an error, need to rewrite IsValidURL() to handle other encodings - $this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')'; - } else { - $framedata .= chr($source_data_array['encodingid']); - $framedata .= str_replace("\x00", '', $source_data_array['mime'])."\x00"; - $framedata .= chr($source_data_array['picturetypeid']); - $framedata .= (!empty($source_data_array['description']) ? $source_data_array['description'] : '').getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']); - $framedata .= $source_data_array['data']; - } - break; - - case 'GEOB': - // 4.15 GEOB General encapsulated object - // Text encoding $xx - // MIME type $00 - // Filename $00 (00) - // Content description $00 (00) - // Encapsulated object - $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid); - if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) { - $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion; - } elseif (!$this->IsValidMIMEstring($source_data_array['mime'])) { - $this->errors[] = 'Invalid MIME Type in '.$frame_name.' ('.$source_data_array['mime'].')'; - } elseif (!$source_data_array['description']) { - $this->errors[] = 'Missing Description in '.$frame_name; - } else { - $framedata .= chr($source_data_array['encodingid']); - $framedata .= str_replace("\x00", '', $source_data_array['mime'])."\x00"; - $framedata .= $source_data_array['filename'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']); - $framedata .= $source_data_array['description'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']); - $framedata .= $source_data_array['data']; - } - break; - - case 'PCNT': - // 4.16 PCNT Play counter - // When the counter reaches all one's, one byte is inserted in - // front of the counter thus making the counter eight bits bigger - // Counter $xx xx xx xx (xx ...) - $framedata .= getid3_lib::BigEndian2String($source_data_array['data'], 4, false); - break; - - case 'POPM': - // 4.17 POPM Popularimeter - // When the counter reaches all one's, one byte is inserted in - // front of the counter thus making the counter eight bits bigger - // Email to user $00 - // Rating $xx - // Counter $xx xx xx xx (xx ...) - if (!$this->IsWithinBitRange($source_data_array['rating'], 8, false)) { - $this->errors[] = 'Invalid Rating byte in '.$frame_name.' ('.$source_data_array['rating'].') (range = 0 to 255)'; - } elseif (!IsValidEmail($source_data_array['email'])) { - $this->errors[] = 'Invalid Email in '.$frame_name.' ('.$source_data_array['email'].')'; - } else { - $framedata .= str_replace("\x00", '', $source_data_array['email'])."\x00"; - $framedata .= chr($source_data_array['rating']); - $framedata .= getid3_lib::BigEndian2String($source_data_array['data'], 4, false); - } - break; - - case 'RBUF': - // 4.18 RBUF Recommended buffer size - // Buffer size $xx xx xx - // Embedded info flag %0000000x - // Offset to next tag $xx xx xx xx - if (!$this->IsWithinBitRange($source_data_array['buffersize'], 24, false)) { - $this->errors[] = 'Invalid Buffer Size in '.$frame_name; - } elseif (!$this->IsWithinBitRange($source_data_array['nexttagoffset'], 32, false)) { - $this->errors[] = 'Invalid Offset To Next Tag in '.$frame_name; - } else { - $framedata .= getid3_lib::BigEndian2String($source_data_array['buffersize'], 3, false); - $flag .= '0000000'; - $flag .= $source_data_array['flags']['embededinfo'] ? '1' : '0'; - $framedata .= chr(bindec($flag)); - $framedata .= getid3_lib::BigEndian2String($source_data_array['nexttagoffset'], 4, false); - } - break; - - case 'AENC': - // 4.19 AENC Audio encryption - // Owner identifier $00 - // Preview start $xx xx - // Preview length $xx xx - // Encryption info - if (!$this->IsWithinBitRange($source_data_array['previewstart'], 16, false)) { - $this->errors[] = 'Invalid Preview Start in '.$frame_name.' ('.$source_data_array['previewstart'].')'; - } elseif (!$this->IsWithinBitRange($source_data_array['previewlength'], 16, false)) { - $this->errors[] = 'Invalid Preview Length in '.$frame_name.' ('.$source_data_array['previewlength'].')'; - } else { - $framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00"; - $framedata .= getid3_lib::BigEndian2String($source_data_array['previewstart'], 2, false); - $framedata .= getid3_lib::BigEndian2String($source_data_array['previewlength'], 2, false); - $framedata .= $source_data_array['encryptioninfo']; - } - break; - - case 'LINK': - // 4.20 LINK Linked information - // Frame identifier $xx xx xx xx - // URL $00 - // ID and additional data - if (!getid3_id3v2::IsValidID3v2FrameName($source_data_array['frameid'], $this->majorversion)) { - $this->errors[] = 'Invalid Frame Identifier in '.$frame_name.' ('.$source_data_array['frameid'].')'; - } elseif (!$this->IsValidURL($source_data_array['data'], true, false)) { - //$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')'; - // probably should be an error, need to rewrite IsValidURL() to handle other encodings - $this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')'; - } elseif ((($source_data_array['frameid'] == 'AENC') || ($source_data_array['frameid'] == 'APIC') || ($source_data_array['frameid'] == 'GEOB') || ($source_data_array['frameid'] == 'TXXX')) && ($source_data_array['additionaldata'] == '')) { - $this->errors[] = 'Content Descriptor must be specified as additional data for Frame Identifier of '.$source_data_array['frameid'].' in '.$frame_name; - } elseif (($source_data_array['frameid'] == 'USER') && (getid3_id3v2::LanguageLookup($source_data_array['additionaldata'], true) == '')) { - $this->errors[] = 'Language must be specified as additional data for Frame Identifier of '.$source_data_array['frameid'].' in '.$frame_name; - } elseif (($source_data_array['frameid'] == 'PRIV') && ($source_data_array['additionaldata'] == '')) { - $this->errors[] = 'Owner Identifier must be specified as additional data for Frame Identifier of '.$source_data_array['frameid'].' in '.$frame_name; - } elseif ((($source_data_array['frameid'] == 'COMM') || ($source_data_array['frameid'] == 'SYLT') || ($source_data_array['frameid'] == 'USLT')) && ((getid3_id3v2::LanguageLookup(substr($source_data_array['additionaldata'], 0, 3), true) == '') || (substr($source_data_array['additionaldata'], 3) == ''))) { - $this->errors[] = 'Language followed by Content Descriptor must be specified as additional data for Frame Identifier of '.$source_data_array['frameid'].' in '.$frame_name; - } else { - $framedata .= $source_data_array['frameid']; - $framedata .= str_replace("\x00", '', $source_data_array['data'])."\x00"; - switch ($source_data_array['frameid']) { - case 'COMM': - case 'SYLT': - case 'USLT': - case 'PRIV': - case 'USER': - case 'AENC': - case 'APIC': - case 'GEOB': - case 'TXXX': - $framedata .= $source_data_array['additionaldata']; - break; - case 'ASPI': - case 'ETCO': - case 'EQU2': - case 'MCID': - case 'MLLT': - case 'OWNE': - case 'RVA2': - case 'RVRB': - case 'SYTC': - case 'IPLS': - case 'RVAD': - case 'EQUA': - // no additional data required - break; - case 'RBUF': - if ($this->majorversion == 3) { - // no additional data required - } else { - $this->errors[] = $source_data_array['frameid'].' is not a valid Frame Identifier in '.$frame_name.' (in ID3v2.'.$this->majorversion.')'; - } - - default: - if ((substr($source_data_array['frameid'], 0, 1) == 'T') || (substr($source_data_array['frameid'], 0, 1) == 'W')) { - // no additional data required - } else { - $this->errors[] = $source_data_array['frameid'].' is not a valid Frame Identifier in '.$frame_name.' (in ID3v2.'.$this->majorversion.')'; - } - break; - } - } - break; - - case 'POSS': - // 4.21 POSS Position synchronisation frame (ID3v2.3+ only) - // Time stamp format $xx - // Position $xx (xx ...) - if (($source_data_array['timestampformat'] < 1) || ($source_data_array['timestampformat'] > 2)) { - $this->errors[] = 'Invalid Time Stamp Format in '.$frame_name.' ('.$source_data_array['timestampformat'].') (valid = 1 or 2)'; - } elseif (!$this->IsWithinBitRange($source_data_array['position'], 32, false)) { - $this->errors[] = 'Invalid Position in '.$frame_name.' ('.$source_data_array['position'].') (range = 0 to 4294967295)'; - } else { - $framedata .= chr($source_data_array['timestampformat']); - $framedata .= getid3_lib::BigEndian2String($source_data_array['position'], 4, false); - } - break; - - case 'USER': - // 4.22 USER Terms of use (ID3v2.3+ only) - // Text encoding $xx - // Language $xx xx xx - // The actual text - $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid); - if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) { - $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].')'; - } elseif (getid3_id3v2::LanguageLookup($source_data_array['language'], true) == '') { - $this->errors[] = 'Invalid Language in '.$frame_name.' ('.$source_data_array['language'].')'; - } else { - $framedata .= chr($source_data_array['encodingid']); - $framedata .= strtolower($source_data_array['language']); - $framedata .= $source_data_array['data']; - } - break; - - case 'OWNE': - // 4.23 OWNE Ownership frame (ID3v2.3+ only) - // Text encoding $xx - // Price paid $00 - // Date of purch. - // Seller - $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid); - if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) { - $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].')'; - } elseif (!$this->IsANumber($source_data_array['pricepaid']['value'], false)) { - $this->errors[] = 'Invalid Price Paid in '.$frame_name.' ('.$source_data_array['pricepaid']['value'].')'; - } elseif (!$this->IsValidDateStampString($source_data_array['purchasedate'])) { - $this->errors[] = 'Invalid Date Of Purchase in '.$frame_name.' ('.$source_data_array['purchasedate'].') (format = YYYYMMDD)'; - } else { - $framedata .= chr($source_data_array['encodingid']); - $framedata .= str_replace("\x00", '', $source_data_array['pricepaid']['value'])."\x00"; - $framedata .= $source_data_array['purchasedate']; - $framedata .= $source_data_array['seller']; - } - break; - - case 'COMR': - // 4.24 COMR Commercial frame (ID3v2.3+ only) - // Text encoding $xx - // Price string $00 - // Valid until - // Contact URL $00 - // Received as $xx - // Name of seller $00 (00) - // Description $00 (00) - // Picture MIME type $00 - // Seller logo - $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid); - if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) { - $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].')'; - } elseif (!$this->IsValidDateStampString($source_data_array['pricevaliduntil'])) { - $this->errors[] = 'Invalid Valid Until date in '.$frame_name.' ('.$source_data_array['pricevaliduntil'].') (format = YYYYMMDD)'; - } elseif (!$this->IsValidURL($source_data_array['contacturl'], false, true)) { - $this->errors[] = 'Invalid Contact URL in '.$frame_name.' ('.$source_data_array['contacturl'].') (allowed schemes: http, https, ftp, mailto)'; - } elseif (!$this->ID3v2IsValidCOMRreceivedAs($source_data_array['receivedasid'])) { - $this->errors[] = 'Invalid Received As byte in '.$frame_name.' ('.$source_data_array['contacturl'].') (range = 0 to 8)'; - } elseif (!$this->IsValidMIMEstring($source_data_array['mime'])) { - $this->errors[] = 'Invalid MIME Type in '.$frame_name.' ('.$source_data_array['mime'].')'; - } else { - $framedata .= chr($source_data_array['encodingid']); - unset($pricestring); - foreach ($source_data_array['price'] as $key => $val) { - if ($this->ID3v2IsValidPriceString($key.$val['value'])) { - $pricestrings[] = $key.$val['value']; - } else { - $this->errors[] = 'Invalid Price String in '.$frame_name.' ('.$key.$val['value'].')'; - } - } - $framedata .= implode('/', $pricestrings); - $framedata .= $source_data_array['pricevaliduntil']; - $framedata .= str_replace("\x00", '', $source_data_array['contacturl'])."\x00"; - $framedata .= chr($source_data_array['receivedasid']); - $framedata .= $source_data_array['sellername'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']); - $framedata .= $source_data_array['description'].getid3_id3v2::TextEncodingTerminatorLookup($source_data_array['encodingid']); - $framedata .= $source_data_array['mime']."\x00"; - $framedata .= $source_data_array['logo']; - } - break; - - case 'ENCR': - // 4.25 ENCR Encryption method registration (ID3v2.3+ only) - // Owner identifier $00 - // Method symbol $xx - // Encryption data - if (!$this->IsWithinBitRange($source_data_array['methodsymbol'], 8, false)) { - $this->errors[] = 'Invalid Group Symbol in '.$frame_name.' ('.$source_data_array['methodsymbol'].') (range = 0 to 255)'; - } else { - $framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00"; - $framedata .= ord($source_data_array['methodsymbol']); - $framedata .= $source_data_array['data']; - } - break; - - case 'GRID': - // 4.26 GRID Group identification registration (ID3v2.3+ only) - // Owner identifier $00 - // Group symbol $xx - // Group dependent data - if (!$this->IsWithinBitRange($source_data_array['groupsymbol'], 8, false)) { - $this->errors[] = 'Invalid Group Symbol in '.$frame_name.' ('.$source_data_array['groupsymbol'].') (range = 0 to 255)'; - } else { - $framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00"; - $framedata .= ord($source_data_array['groupsymbol']); - $framedata .= $source_data_array['data']; - } - break; - - case 'PRIV': - // 4.27 PRIV Private frame (ID3v2.3+ only) - // Owner identifier $00 - // The private data - $framedata .= str_replace("\x00", '', $source_data_array['ownerid'])."\x00"; - $framedata .= $source_data_array['data']; - break; - - case 'SIGN': - // 4.28 SIGN Signature frame (ID3v2.4+ only) - // Group symbol $xx - // Signature - if (!$this->IsWithinBitRange($source_data_array['groupsymbol'], 8, false)) { - $this->errors[] = 'Invalid Group Symbol in '.$frame_name.' ('.$source_data_array['groupsymbol'].') (range = 0 to 255)'; - } else { - $framedata .= ord($source_data_array['groupsymbol']); - $framedata .= $source_data_array['data']; - } - break; - - case 'SEEK': - // 4.29 SEEK Seek frame (ID3v2.4+ only) - // Minimum offset to next tag $xx xx xx xx - if (!$this->IsWithinBitRange($source_data_array['data'], 32, false)) { - $this->errors[] = 'Invalid Minimum Offset in '.$frame_name.' ('.$source_data_array['data'].') (range = 0 to 4294967295)'; - } else { - $framedata .= getid3_lib::BigEndian2String($source_data_array['data'], 4, false); - } - break; - - case 'ASPI': - // 4.30 ASPI Audio seek point index (ID3v2.4+ only) - // Indexed data start (S) $xx xx xx xx - // Indexed data length (L) $xx xx xx xx - // Number of index points (N) $xx xx - // Bits per index point (b) $xx - // Then for every index point the following data is included: - // Fraction at index (Fi) $xx (xx) - if (!$this->IsWithinBitRange($source_data_array['datastart'], 32, false)) { - $this->errors[] = 'Invalid Indexed Data Start in '.$frame_name.' ('.$source_data_array['datastart'].') (range = 0 to 4294967295)'; - } elseif (!$this->IsWithinBitRange($source_data_array['datalength'], 32, false)) { - $this->errors[] = 'Invalid Indexed Data Length in '.$frame_name.' ('.$source_data_array['datalength'].') (range = 0 to 4294967295)'; - } elseif (!$this->IsWithinBitRange($source_data_array['indexpoints'], 16, false)) { - $this->errors[] = 'Invalid Number Of Index Points in '.$frame_name.' ('.$source_data_array['indexpoints'].') (range = 0 to 65535)'; - } elseif (!$this->IsWithinBitRange($source_data_array['bitsperpoint'], 8, false)) { - $this->errors[] = 'Invalid Bits Per Index Point in '.$frame_name.' ('.$source_data_array['bitsperpoint'].') (range = 0 to 255)'; - } elseif ($source_data_array['indexpoints'] != count($source_data_array['indexes'])) { - $this->errors[] = 'Number Of Index Points does not match actual supplied data in '.$frame_name; - } else { - $framedata .= getid3_lib::BigEndian2String($source_data_array['datastart'], 4, false); - $framedata .= getid3_lib::BigEndian2String($source_data_array['datalength'], 4, false); - $framedata .= getid3_lib::BigEndian2String($source_data_array['indexpoints'], 2, false); - $framedata .= getid3_lib::BigEndian2String($source_data_array['bitsperpoint'], 1, false); - foreach ($source_data_array['indexes'] as $key => $val) { - $framedata .= getid3_lib::BigEndian2String($val, ceil($source_data_array['bitsperpoint'] / 8), false); - } - } - break; - - case 'RGAD': - // RGAD Replay Gain Adjustment - // http://privatewww.essex.ac.uk/~djmrob/replaygain/ - // Peak Amplitude $xx $xx $xx $xx - // Radio Replay Gain Adjustment %aaabbbcd %dddddddd - // Audiophile Replay Gain Adjustment %aaabbbcd %dddddddd - // a - name code - // b - originator code - // c - sign bit - // d - replay gain adjustment - - if (($source_data_array['track_adjustment'] > 51) || ($source_data_array['track_adjustment'] < -51)) { - $this->errors[] = 'Invalid Track Adjustment in '.$frame_name.' ('.$source_data_array['track_adjustment'].') (range = -51.0 to +51.0)'; - } elseif (($source_data_array['album_adjustment'] > 51) || ($source_data_array['album_adjustment'] < -51)) { - $this->errors[] = 'Invalid Album Adjustment in '.$frame_name.' ('.$source_data_array['album_adjustment'].') (range = -51.0 to +51.0)'; - } elseif (!$this->ID3v2IsValidRGADname($source_data_array['raw']['track_name'])) { - $this->errors[] = 'Invalid Track Name Code in '.$frame_name.' ('.$source_data_array['raw']['track_name'].') (range = 0 to 2)'; - } elseif (!$this->ID3v2IsValidRGADname($source_data_array['raw']['album_name'])) { - $this->errors[] = 'Invalid Album Name Code in '.$frame_name.' ('.$source_data_array['raw']['album_name'].') (range = 0 to 2)'; - } elseif (!$this->ID3v2IsValidRGADoriginator($source_data_array['raw']['track_originator'])) { - $this->errors[] = 'Invalid Track Originator Code in '.$frame_name.' ('.$source_data_array['raw']['track_originator'].') (range = 0 to 3)'; - } elseif (!$this->ID3v2IsValidRGADoriginator($source_data_array['raw']['album_originator'])) { - $this->errors[] = 'Invalid Album Originator Code in '.$frame_name.' ('.$source_data_array['raw']['album_originator'].') (range = 0 to 3)'; - } else { - $framedata .= getid3_lib::Float2String($source_data_array['peakamplitude'], 32); - $framedata .= getid3_lib::RGADgainString($source_data_array['raw']['track_name'], $source_data_array['raw']['track_originator'], $source_data_array['track_adjustment']); - $framedata .= getid3_lib::RGADgainString($source_data_array['raw']['album_name'], $source_data_array['raw']['album_originator'], $source_data_array['album_adjustment']); - } - break; - - default: - if ((($this->majorversion == 2) && (strlen($frame_name) != 3)) || (($this->majorversion > 2) && (strlen($frame_name) != 4))) { - $this->errors[] = 'Invalid frame name "'.$frame_name.'" for ID3v2.'.$this->majorversion; - } elseif ($frame_name{0} == 'T') { - // 4.2. T??? Text information frames - // Text encoding $xx - // Information - $source_data_array['encodingid'] = (isset($source_data_array['encodingid']) ? $source_data_array['encodingid'] : $this->id3v2_default_encodingid); - if (!$this->ID3v2IsValidTextEncoding($source_data_array['encodingid'])) { - $this->errors[] = 'Invalid Text Encoding in '.$frame_name.' ('.$source_data_array['encodingid'].') for ID3v2.'.$this->majorversion; - } else { - $framedata .= chr($source_data_array['encodingid']); - $framedata .= $source_data_array['data']; - } - } elseif ($frame_name{0} == 'W') { - // 4.3. W??? URL link frames - // URL - if (!$this->IsValidURL($source_data_array['data'], false, false)) { - //$this->errors[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')'; - // probably should be an error, need to rewrite IsValidURL() to handle other encodings - $this->warnings[] = 'Invalid URL in '.$frame_name.' ('.$source_data_array['data'].')'; - } else { - $framedata .= $source_data_array['data']; - } - } else { - $this->errors[] = $frame_name.' not yet supported in $this->GenerateID3v2FrameData()'; - } - break; - } - } - if (!empty($this->errors)) { - return false; - } - return $framedata; - } - - public function ID3v2FrameIsAllowed($frame_name, $source_data_array) { - static $PreviousFrames = array(); - - if ($frame_name === null) { - // if the writing functions are called multiple times, the static array needs to be - // cleared - this can be done by calling $this->ID3v2FrameIsAllowed(null, '') - $PreviousFrames = array(); - return true; - } - - if ($this->majorversion == 4) { - switch ($frame_name) { - case 'UFID': - case 'AENC': - case 'ENCR': - case 'GRID': - if (!isset($source_data_array['ownerid'])) { - $this->errors[] = '[ownerid] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['ownerid'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID ('.$source_data_array['ownerid'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['ownerid']; - } - break; - - case 'TXXX': - case 'WXXX': - case 'RVA2': - case 'EQU2': - case 'APIC': - case 'GEOB': - if (!isset($source_data_array['description'])) { - $this->errors[] = '[description] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['description'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Description ('.$source_data_array['description'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['description']; - } - break; - - case 'USER': - if (!isset($source_data_array['language'])) { - $this->errors[] = '[language] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['language'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language ('.$source_data_array['language'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['language']; - } - break; - - case 'USLT': - case 'SYLT': - case 'COMM': - if (!isset($source_data_array['language'])) { - $this->errors[] = '[language] not specified for '.$frame_name; - } elseif (!isset($source_data_array['description'])) { - $this->errors[] = '[description] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['language'].$source_data_array['description'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language + Description ('.$source_data_array['language'].' + '.$source_data_array['description'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['language'].$source_data_array['description']; - } - break; - - case 'POPM': - if (!isset($source_data_array['email'])) { - $this->errors[] = '[email] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['email'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Email ('.$source_data_array['email'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['email']; - } - break; - - case 'IPLS': - case 'MCDI': - case 'ETCO': - case 'MLLT': - case 'SYTC': - case 'RVRB': - case 'PCNT': - case 'RBUF': - case 'POSS': - case 'OWNE': - case 'SEEK': - case 'ASPI': - case 'RGAD': - if (in_array($frame_name, $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed'; - } else { - $PreviousFrames[] = $frame_name; - } - break; - - case 'LINK': - // this isn't implemented quite right (yet) - it should check the target frame data for compliance - // but right now it just allows one linked frame of each type, to be safe. - if (!isset($source_data_array['frameid'])) { - $this->errors[] = '[frameid] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['frameid'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same FrameID ('.$source_data_array['frameid'].')'; - } elseif (in_array($source_data_array['frameid'], $PreviousFrames)) { - // no links to singleton tags - $this->errors[] = 'Cannot specify a '.$frame_name.' tag to a singleton tag that already exists ('.$source_data_array['frameid'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['frameid']; // only one linked tag of this type - $PreviousFrames[] = $source_data_array['frameid']; // no non-linked singleton tags of this type - } - break; - - case 'COMR': - // There may be more than one 'commercial frame' in a tag, but no two may be identical - // Checking isn't implemented at all (yet) - just assumes that it's OK. - break; - - case 'PRIV': - case 'SIGN': - if (!isset($source_data_array['ownerid'])) { - $this->errors[] = '[ownerid] not specified for '.$frame_name; - } elseif (!isset($source_data_array['data'])) { - $this->errors[] = '[data] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['ownerid'].$source_data_array['data'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID + Data ('.$source_data_array['ownerid'].' + '.$source_data_array['data'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['ownerid'].$source_data_array['data']; - } - break; - - default: - if (($frame_name{0} != 'T') && ($frame_name{0} != 'W')) { - $this->errors[] = 'Frame not allowed in ID3v2.'.$this->majorversion.': '.$frame_name; - } - break; - } - - } elseif ($this->majorversion == 3) { - - switch ($frame_name) { - case 'UFID': - case 'AENC': - case 'ENCR': - case 'GRID': - if (!isset($source_data_array['ownerid'])) { - $this->errors[] = '[ownerid] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['ownerid'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID ('.$source_data_array['ownerid'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['ownerid']; - } - break; - - case 'TXXX': - case 'WXXX': - case 'APIC': - case 'GEOB': - if (!isset($source_data_array['description'])) { - $this->errors[] = '[description] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['description'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Description ('.$source_data_array['description'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['description']; - } - break; - - case 'USER': - if (!isset($source_data_array['language'])) { - $this->errors[] = '[language] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['language'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language ('.$source_data_array['language'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['language']; - } - break; - - case 'USLT': - case 'SYLT': - case 'COMM': - if (!isset($source_data_array['language'])) { - $this->errors[] = '[language] not specified for '.$frame_name; - } elseif (!isset($source_data_array['description'])) { - $this->errors[] = '[description] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['language'].$source_data_array['description'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language + Description ('.$source_data_array['language'].' + '.$source_data_array['description'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['language'].$source_data_array['description']; - } - break; - - case 'POPM': - if (!isset($source_data_array['email'])) { - $this->errors[] = '[email] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['email'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Email ('.$source_data_array['email'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['email']; - } - break; - - case 'IPLS': - case 'MCDI': - case 'ETCO': - case 'MLLT': - case 'SYTC': - case 'RVAD': - case 'EQUA': - case 'RVRB': - case 'PCNT': - case 'RBUF': - case 'POSS': - case 'OWNE': - case 'RGAD': - if (in_array($frame_name, $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed'; - } else { - $PreviousFrames[] = $frame_name; - } - break; - - case 'LINK': - // this isn't implemented quite right (yet) - it should check the target frame data for compliance - // but right now it just allows one linked frame of each type, to be safe. - if (!isset($source_data_array['frameid'])) { - $this->errors[] = '[frameid] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['frameid'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same FrameID ('.$source_data_array['frameid'].')'; - } elseif (in_array($source_data_array['frameid'], $PreviousFrames)) { - // no links to singleton tags - $this->errors[] = 'Cannot specify a '.$frame_name.' tag to a singleton tag that already exists ('.$source_data_array['frameid'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['frameid']; // only one linked tag of this type - $PreviousFrames[] = $source_data_array['frameid']; // no non-linked singleton tags of this type - } - break; - - case 'COMR': - // There may be more than one 'commercial frame' in a tag, but no two may be identical - // Checking isn't implemented at all (yet) - just assumes that it's OK. - break; - - case 'PRIV': - if (!isset($source_data_array['ownerid'])) { - $this->errors[] = '[ownerid] not specified for '.$frame_name; - } elseif (!isset($source_data_array['data'])) { - $this->errors[] = '[data] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['ownerid'].$source_data_array['data'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID + Data ('.$source_data_array['ownerid'].' + '.$source_data_array['data'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['ownerid'].$source_data_array['data']; - } - break; - - default: - if (($frame_name{0} != 'T') && ($frame_name{0} != 'W')) { - $this->errors[] = 'Frame not allowed in ID3v2.'.$this->majorversion.': '.$frame_name; - } - break; - } - - } elseif ($this->majorversion == 2) { - - switch ($frame_name) { - case 'UFI': - case 'CRM': - case 'CRA': - if (!isset($source_data_array['ownerid'])) { - $this->errors[] = '[ownerid] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['ownerid'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same OwnerID ('.$source_data_array['ownerid'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['ownerid']; - } - break; - - case 'TXX': - case 'WXX': - case 'PIC': - case 'GEO': - if (!isset($source_data_array['description'])) { - $this->errors[] = '[description] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['description'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Description ('.$source_data_array['description'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['description']; - } - break; - - case 'ULT': - case 'SLT': - case 'COM': - if (!isset($source_data_array['language'])) { - $this->errors[] = '[language] not specified for '.$frame_name; - } elseif (!isset($source_data_array['description'])) { - $this->errors[] = '[description] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['language'].$source_data_array['description'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Language + Description ('.$source_data_array['language'].' + '.$source_data_array['description'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['language'].$source_data_array['description']; - } - break; - - case 'POP': - if (!isset($source_data_array['email'])) { - $this->errors[] = '[email] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['email'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same Email ('.$source_data_array['email'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['email']; - } - break; - - case 'IPL': - case 'MCI': - case 'ETC': - case 'MLL': - case 'STC': - case 'RVA': - case 'EQU': - case 'REV': - case 'CNT': - case 'BUF': - if (in_array($frame_name, $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed'; - } else { - $PreviousFrames[] = $frame_name; - } - break; - - case 'LNK': - // this isn't implemented quite right (yet) - it should check the target frame data for compliance - // but right now it just allows one linked frame of each type, to be safe. - if (!isset($source_data_array['frameid'])) { - $this->errors[] = '[frameid] not specified for '.$frame_name; - } elseif (in_array($frame_name.$source_data_array['frameid'], $PreviousFrames)) { - $this->errors[] = 'Only one '.$frame_name.' tag allowed with the same FrameID ('.$source_data_array['frameid'].')'; - } elseif (in_array($source_data_array['frameid'], $PreviousFrames)) { - // no links to singleton tags - $this->errors[] = 'Cannot specify a '.$frame_name.' tag to a singleton tag that already exists ('.$source_data_array['frameid'].')'; - } else { - $PreviousFrames[] = $frame_name.$source_data_array['frameid']; // only one linked tag of this type - $PreviousFrames[] = $source_data_array['frameid']; // no non-linked singleton tags of this type - } - break; - - default: - if (($frame_name{0} != 'T') && ($frame_name{0} != 'W')) { - $this->errors[] = 'Frame not allowed in ID3v2.'.$this->majorversion.': '.$frame_name; - } - break; - } - } - - if (!empty($this->errors)) { - return false; - } - return true; - } - - public function GenerateID3v2Tag($noerrorsonly=true) { - $this->ID3v2FrameIsAllowed(null, ''); // clear static array in case this isn't the first call to $this->GenerateID3v2Tag() - - $tagstring = ''; - if (is_array($this->tag_data)) { - foreach ($this->tag_data as $frame_name => $frame_rawinputdata) { - foreach ($frame_rawinputdata as $irrelevantindex => $source_data_array) { - if (getid3_id3v2::IsValidID3v2FrameName($frame_name, $this->majorversion)) { - unset($frame_length); - unset($frame_flags); - $frame_data = false; - if ($this->ID3v2FrameIsAllowed($frame_name, $source_data_array)) { - if ($frame_data = $this->GenerateID3v2FrameData($frame_name, $source_data_array)) { - $FrameUnsynchronisation = false; - if ($this->majorversion >= 4) { - // frame-level unsynchronisation - $unsynchdata = $frame_data; - if ($this->id3v2_use_unsynchronisation) { - $unsynchdata = $this->Unsynchronise($frame_data); - } - if (strlen($unsynchdata) != strlen($frame_data)) { - // unsynchronisation needed - $FrameUnsynchronisation = true; - $frame_data = $unsynchdata; - if (isset($TagUnsynchronisation) && $TagUnsynchronisation === false) { - // only set to true if ALL frames are unsynchronised - } else { - $TagUnsynchronisation = true; - } - } else { - if (isset($TagUnsynchronisation)) { - $TagUnsynchronisation = false; - } - } - unset($unsynchdata); - - $frame_length = getid3_lib::BigEndian2String(strlen($frame_data), 4, true); - } else { - $frame_length = getid3_lib::BigEndian2String(strlen($frame_data), 4, false); - } - $frame_flags = $this->GenerateID3v2FrameFlags($this->ID3v2FrameFlagsLookupTagAlter($frame_name), $this->ID3v2FrameFlagsLookupFileAlter($frame_name), false, false, false, false, $FrameUnsynchronisation, false); - } - } else { - $this->errors[] = 'Frame "'.$frame_name.'" is NOT allowed'; - } - if ($frame_data === false) { - $this->errors[] = '$this->GenerateID3v2FrameData() failed for "'.$frame_name.'"'; - if ($noerrorsonly) { - return false; - } else { - unset($frame_name); - } - } - } else { - // ignore any invalid frame names, including 'title', 'header', etc - $this->warnings[] = 'Ignoring invalid ID3v2 frame type: "'.$frame_name.'"'; - unset($frame_name); - unset($frame_length); - unset($frame_flags); - unset($frame_data); - } - if (isset($frame_name) && isset($frame_length) && isset($frame_flags) && isset($frame_data)) { - $tagstring .= $frame_name.$frame_length.$frame_flags.$frame_data; - } - } - } - - if (!isset($TagUnsynchronisation)) { - $TagUnsynchronisation = false; - } - if (($this->majorversion <= 3) && $this->id3v2_use_unsynchronisation) { - // tag-level unsynchronisation - $unsynchdata = $this->Unsynchronise($tagstring); - if (strlen($unsynchdata) != strlen($tagstring)) { - // unsynchronisation needed - $TagUnsynchronisation = true; - $tagstring = $unsynchdata; - } - } - - while ($this->paddedlength < (strlen($tagstring) + getid3_id3v2::ID3v2HeaderLength($this->majorversion))) { - $this->paddedlength += 1024; - } - - $footer = false; // ID3v2 footers not yet supported in getID3() - if (!$footer && ($this->paddedlength > (strlen($tagstring) + getid3_id3v2::ID3v2HeaderLength($this->majorversion)))) { - // pad up to $paddedlength bytes if unpadded tag is shorter than $paddedlength - // "Furthermore it MUST NOT have any padding when a tag footer is added to the tag." - if (($this->paddedlength - strlen($tagstring) - getid3_id3v2::ID3v2HeaderLength($this->majorversion)) > 0) { - $tagstring .= str_repeat("\x00", $this->paddedlength - strlen($tagstring) - getid3_id3v2::ID3v2HeaderLength($this->majorversion)); - } - } - if ($this->id3v2_use_unsynchronisation && (substr($tagstring, strlen($tagstring) - 1, 1) == "\xFF")) { - // special unsynchronisation case: - // if last byte == $FF then appended a $00 - $TagUnsynchronisation = true; - $tagstring .= "\x00"; - } - - $tagheader = 'ID3'; - $tagheader .= chr($this->majorversion); - $tagheader .= chr($this->minorversion); - $tagheader .= $this->GenerateID3v2TagFlags(array('unsynchronisation'=>$TagUnsynchronisation)); - $tagheader .= getid3_lib::BigEndian2String(strlen($tagstring), 4, true); - - return $tagheader.$tagstring; - } - $this->errors[] = 'tag_data is not an array in GenerateID3v2Tag()'; - return false; - } - - public function ID3v2IsValidPriceString($pricestring) { - if (getid3_id3v2::LanguageLookup(substr($pricestring, 0, 3), true) == '') { - return false; - } elseif (!$this->IsANumber(substr($pricestring, 3), true)) { - return false; - } - return true; - } - - public function ID3v2FrameFlagsLookupTagAlter($framename) { - // unfinished - switch ($framename) { - case 'RGAD': - $allow = true; - default: - $allow = false; - break; - } - return $allow; - } - - public function ID3v2FrameFlagsLookupFileAlter($framename) { - // unfinished - switch ($framename) { - case 'RGAD': - return false; - break; - - default: - return false; - break; - } - } - - public function ID3v2IsValidETCOevent($eventid) { - if (($eventid < 0) || ($eventid > 0xFF)) { - // outside range of 1 byte - return false; - } elseif (($eventid >= 0xF0) && ($eventid <= 0xFC)) { - // reserved for future use - return false; - } elseif (($eventid >= 0x17) && ($eventid <= 0xDF)) { - // reserved for future use - return false; - } elseif (($eventid >= 0x0E) && ($eventid <= 0x16) && ($this->majorversion == 2)) { - // not defined in ID3v2.2 - return false; - } elseif (($eventid >= 0x15) && ($eventid <= 0x16) && ($this->majorversion == 3)) { - // not defined in ID3v2.3 - return false; - } - return true; - } - - public function ID3v2IsValidSYLTtype($contenttype) { - if (($contenttype >= 0) && ($contenttype <= 8) && ($this->majorversion == 4)) { - return true; - } elseif (($contenttype >= 0) && ($contenttype <= 6) && ($this->majorversion == 3)) { - return true; - } - return false; - } - - public function ID3v2IsValidRVA2channeltype($channeltype) { - if (($channeltype >= 0) && ($channeltype <= 8) && ($this->majorversion == 4)) { - return true; - } - return false; - } - - public function ID3v2IsValidAPICpicturetype($picturetype) { - if (($picturetype >= 0) && ($picturetype <= 0x14) && ($this->majorversion >= 2) && ($this->majorversion <= 4)) { - return true; - } - return false; - } - - public function ID3v2IsValidAPICimageformat($imageformat) { - if ($imageformat == '-->') { - return true; - } elseif ($this->majorversion == 2) { - if ((strlen($imageformat) == 3) && ($imageformat == strtoupper($imageformat))) { - return true; - } - } elseif (($this->majorversion == 3) || ($this->majorversion == 4)) { - if ($this->IsValidMIMEstring($imageformat)) { - return true; - } - } - return false; - } - - public function ID3v2IsValidCOMRreceivedAs($receivedas) { - if (($this->majorversion >= 3) && ($receivedas >= 0) && ($receivedas <= 8)) { - return true; - } - return false; - } - - public function ID3v2IsValidRGADname($RGADname) { - if (($RGADname >= 0) && ($RGADname <= 2)) { - return true; - } - return false; - } - - public function ID3v2IsValidRGADoriginator($RGADoriginator) { - if (($RGADoriginator >= 0) && ($RGADoriginator <= 3)) { - return true; - } - return false; - } - - public function ID3v2IsValidTextEncoding($textencodingbyte) { - static $ID3v2IsValidTextEncoding_cache = array( - 2 => array(true, true), - 3 => array(true, true), - 4 => array(true, true, true, true)); - return isset($ID3v2IsValidTextEncoding_cache[$this->majorversion][$textencodingbyte]); - } - - public function Unsynchronise($data) { - // Whenever a false synchronisation is found within the tag, one zeroed - // byte is inserted after the first false synchronisation byte. The - // format of a correct sync that should be altered by ID3 encoders is as - // follows: - // %11111111 111xxxxx - // And should be replaced with: - // %11111111 00000000 111xxxxx - // This has the side effect that all $FF 00 combinations have to be - // altered, so they won't be affected by the decoding process. Therefore - // all the $FF 00 combinations have to be replaced with the $FF 00 00 - // combination during the unsynchronisation. - - $data = str_replace("\xFF\x00", "\xFF\x00\x00", $data); - $unsyncheddata = ''; - $datalength = strlen($data); - for ($i = 0; $i < $datalength; $i++) { - $thischar = $data{$i}; - $unsyncheddata .= $thischar; - if ($thischar == "\xFF") { - $nextchar = ord($data{$i + 1}); - if (($nextchar & 0xE0) == 0xE0) { - // previous byte = 11111111, this byte = 111????? - $unsyncheddata .= "\x00"; - } - } - } - return $unsyncheddata; - } - - public function is_hash($var) { - // written by dev-nullØchristophe*vg - // taken from http://www.php.net/manual/en/function.array-merge-recursive.php - if (is_array($var)) { - $keys = array_keys($var); - $all_num = true; - for ($i = 0; $i < count($keys); $i++) { - if (is_string($keys[$i])) { - return true; - } - } - } - return false; - } - - public function array_join_merge($arr1, $arr2) { - // written by dev-nullØchristophe*vg - // taken from http://www.php.net/manual/en/function.array-merge-recursive.php - if (is_array($arr1) && is_array($arr2)) { - // the same -> merge - $new_array = array(); - - if ($this->is_hash($arr1) && $this->is_hash($arr2)) { - // hashes -> merge based on keys - $keys = array_merge(array_keys($arr1), array_keys($arr2)); - foreach ($keys as $key) { - $new_array[$key] = $this->array_join_merge((isset($arr1[$key]) ? $arr1[$key] : ''), (isset($arr2[$key]) ? $arr2[$key] : '')); - } - } else { - // two real arrays -> merge - $new_array = array_reverse(array_unique(array_reverse(array_merge($arr1, $arr2)))); - } - return $new_array; - } else { - // not the same ... take new one if defined, else the old one stays - return $arr2 ? $arr2 : $arr1; - } - } - - public function IsValidMIMEstring($mimestring) { - if ((strlen($mimestring) >= 3) && (strpos($mimestring, '/') > 0) && (strpos($mimestring, '/') < (strlen($mimestring) - 1))) { - return true; - } - return false; - } - - public function IsWithinBitRange($number, $maxbits, $signed=false) { - if ($signed) { - if (($number > (0 - pow(2, $maxbits - 1))) && ($number <= pow(2, $maxbits - 1))) { - return true; - } - } else { - if (($number >= 0) && ($number <= pow(2, $maxbits))) { - return true; - } - } - return false; - } - - public function safe_parse_url($url) { - $parts = @parse_url($url); - $parts['scheme'] = (isset($parts['scheme']) ? $parts['scheme'] : ''); - $parts['host'] = (isset($parts['host']) ? $parts['host'] : ''); - $parts['user'] = (isset($parts['user']) ? $parts['user'] : ''); - $parts['pass'] = (isset($parts['pass']) ? $parts['pass'] : ''); - $parts['path'] = (isset($parts['path']) ? $parts['path'] : ''); - $parts['query'] = (isset($parts['query']) ? $parts['query'] : ''); - return $parts; - } - - public function IsValidURL($url, $allowUserPass=false) { - if ($url == '') { - return false; - } - if ($allowUserPass !== true) { - if (strstr($url, '@')) { - // in the format http://user:pass@example.com or http://user@example.com - // but could easily be somebody incorrectly entering an email address in place of a URL - return false; - } - } - if ($parts = $this->safe_parse_url($url)) { - if (($parts['scheme'] != 'http') && ($parts['scheme'] != 'https') && ($parts['scheme'] != 'ftp') && ($parts['scheme'] != 'gopher')) { - return false; - } elseif (!preg_match('#^[[:alnum:]]([-.]?[0-9a-z])*\\.[a-z]{2,3}$#i', $parts['host'], $regs) && !preg_match('#^[0-9]{1,3}(\\.[0-9]{1,3}){3}$#', $parts['host'])) { - return false; - } elseif (!preg_match('#^([[:alnum:]-]|[\\_])*$#i', $parts['user'], $regs)) { - return false; - } elseif (!preg_match('#^([[:alnum:]-]|[\\_])*$#i', $parts['pass'], $regs)) { - return false; - } elseif (!preg_match('#^[[:alnum:]/_\\.@~-]*$#i', $parts['path'], $regs)) { - return false; - } elseif (!empty($parts['query']) && !preg_match('#^[[:alnum:]?&=+:;_()%\\#/,\\.-]*$#i', $parts['query'], $regs)) { - return false; - } else { - return true; - } - } - return false; - } - - public static function ID3v2ShortFrameNameLookup($majorversion, $long_description) { - $long_description = str_replace(' ', '_', strtolower(trim($long_description))); - static $ID3v2ShortFrameNameLookup = array(); - if (empty($ID3v2ShortFrameNameLookup)) { - - // The following are unique to ID3v2.2 - $ID3v2ShortFrameNameLookup[2]['comment'] = 'COM'; - $ID3v2ShortFrameNameLookup[2]['album'] = 'TAL'; - $ID3v2ShortFrameNameLookup[2]['beats_per_minute'] = 'TBP'; - $ID3v2ShortFrameNameLookup[2]['composer'] = 'TCM'; - $ID3v2ShortFrameNameLookup[2]['genre'] = 'TCO'; - $ID3v2ShortFrameNameLookup[2]['itunescompilation'] = 'TCP'; - $ID3v2ShortFrameNameLookup[2]['copyright'] = 'TCR'; - $ID3v2ShortFrameNameLookup[2]['encoded_by'] = 'TEN'; - $ID3v2ShortFrameNameLookup[2]['language'] = 'TLA'; - $ID3v2ShortFrameNameLookup[2]['length'] = 'TLE'; - $ID3v2ShortFrameNameLookup[2]['original_artist'] = 'TOA'; - $ID3v2ShortFrameNameLookup[2]['original_filename'] = 'TOF'; - $ID3v2ShortFrameNameLookup[2]['original_lyricist'] = 'TOL'; - $ID3v2ShortFrameNameLookup[2]['original_album_title'] = 'TOT'; - $ID3v2ShortFrameNameLookup[2]['artist'] = 'TP1'; - $ID3v2ShortFrameNameLookup[2]['band'] = 'TP2'; - $ID3v2ShortFrameNameLookup[2]['conductor'] = 'TP3'; - $ID3v2ShortFrameNameLookup[2]['remixer'] = 'TP4'; - $ID3v2ShortFrameNameLookup[2]['publisher'] = 'TPB'; - $ID3v2ShortFrameNameLookup[2]['isrc'] = 'TRC'; - $ID3v2ShortFrameNameLookup[2]['tracknumber'] = 'TRK'; - $ID3v2ShortFrameNameLookup[2]['size'] = 'TSI'; - $ID3v2ShortFrameNameLookup[2]['encoder_settings'] = 'TSS'; - $ID3v2ShortFrameNameLookup[2]['description'] = 'TT1'; - $ID3v2ShortFrameNameLookup[2]['title'] = 'TT2'; - $ID3v2ShortFrameNameLookup[2]['subtitle'] = 'TT3'; - $ID3v2ShortFrameNameLookup[2]['lyricist'] = 'TXT'; - $ID3v2ShortFrameNameLookup[2]['user_text'] = 'TXX'; - $ID3v2ShortFrameNameLookup[2]['year'] = 'TYE'; - $ID3v2ShortFrameNameLookup[2]['unique_file_identifier'] = 'UFI'; - $ID3v2ShortFrameNameLookup[2]['unsynchronised_lyrics'] = 'ULT'; - $ID3v2ShortFrameNameLookup[2]['url_file'] = 'WAF'; - $ID3v2ShortFrameNameLookup[2]['url_artist'] = 'WAR'; - $ID3v2ShortFrameNameLookup[2]['url_source'] = 'WAS'; - $ID3v2ShortFrameNameLookup[2]['copyright_information'] = 'WCP'; - $ID3v2ShortFrameNameLookup[2]['url_publisher'] = 'WPB'; - $ID3v2ShortFrameNameLookup[2]['url_user'] = 'WXX'; - - // The following are common to ID3v2.3 and ID3v2.4 - $ID3v2ShortFrameNameLookup[3]['audio_encryption'] = 'AENC'; - $ID3v2ShortFrameNameLookup[3]['attached_picture'] = 'APIC'; - $ID3v2ShortFrameNameLookup[3]['comment'] = 'COMM'; - $ID3v2ShortFrameNameLookup[3]['commercial'] = 'COMR'; - $ID3v2ShortFrameNameLookup[3]['encryption_method_registration'] = 'ENCR'; - $ID3v2ShortFrameNameLookup[3]['event_timing_codes'] = 'ETCO'; - $ID3v2ShortFrameNameLookup[3]['general_encapsulated_object'] = 'GEOB'; - $ID3v2ShortFrameNameLookup[3]['group_identification_registration'] = 'GRID'; - $ID3v2ShortFrameNameLookup[3]['linked_information'] = 'LINK'; - $ID3v2ShortFrameNameLookup[3]['music_cd_identifier'] = 'MCDI'; - $ID3v2ShortFrameNameLookup[3]['mpeg_location_lookup_table'] = 'MLLT'; - $ID3v2ShortFrameNameLookup[3]['ownership'] = 'OWNE'; - $ID3v2ShortFrameNameLookup[3]['play_counter'] = 'PCNT'; - $ID3v2ShortFrameNameLookup[3]['popularimeter'] = 'POPM'; - $ID3v2ShortFrameNameLookup[3]['position_synchronisation'] = 'POSS'; - $ID3v2ShortFrameNameLookup[3]['private'] = 'PRIV'; - $ID3v2ShortFrameNameLookup[3]['recommended_buffer_size'] = 'RBUF'; - $ID3v2ShortFrameNameLookup[3]['reverb'] = 'RVRB'; - $ID3v2ShortFrameNameLookup[3]['synchronised_lyrics'] = 'SYLT'; - $ID3v2ShortFrameNameLookup[3]['synchronised_tempo_codes'] = 'SYTC'; - $ID3v2ShortFrameNameLookup[3]['album'] = 'TALB'; - $ID3v2ShortFrameNameLookup[3]['beats_per_minute'] = 'TBPM'; - $ID3v2ShortFrameNameLookup[3]['itunescompilation'] = 'TCMP'; - $ID3v2ShortFrameNameLookup[3]['composer'] = 'TCOM'; - $ID3v2ShortFrameNameLookup[3]['genre'] = 'TCON'; - $ID3v2ShortFrameNameLookup[3]['copyright'] = 'TCOP'; - $ID3v2ShortFrameNameLookup[3]['playlist_delay'] = 'TDLY'; - $ID3v2ShortFrameNameLookup[3]['encoded_by'] = 'TENC'; - $ID3v2ShortFrameNameLookup[3]['lyricist'] = 'TEXT'; - $ID3v2ShortFrameNameLookup[3]['file_type'] = 'TFLT'; - $ID3v2ShortFrameNameLookup[3]['content_group_description'] = 'TIT1'; - $ID3v2ShortFrameNameLookup[3]['title'] = 'TIT2'; - $ID3v2ShortFrameNameLookup[3]['subtitle'] = 'TIT3'; - $ID3v2ShortFrameNameLookup[3]['initial_key'] = 'TKEY'; - $ID3v2ShortFrameNameLookup[3]['language'] = 'TLAN'; - $ID3v2ShortFrameNameLookup[3]['length'] = 'TLEN'; - $ID3v2ShortFrameNameLookup[3]['media_type'] = 'TMED'; - $ID3v2ShortFrameNameLookup[3]['original_album_title'] = 'TOAL'; - $ID3v2ShortFrameNameLookup[3]['original_filename'] = 'TOFN'; - $ID3v2ShortFrameNameLookup[3]['original_lyricist'] = 'TOLY'; - $ID3v2ShortFrameNameLookup[3]['original_artist'] = 'TOPE'; - $ID3v2ShortFrameNameLookup[3]['file_owner'] = 'TOWN'; - $ID3v2ShortFrameNameLookup[3]['artist'] = 'TPE1'; - $ID3v2ShortFrameNameLookup[3]['band'] = 'TPE2'; - $ID3v2ShortFrameNameLookup[3]['conductor'] = 'TPE3'; - $ID3v2ShortFrameNameLookup[3]['remixer'] = 'TPE4'; - $ID3v2ShortFrameNameLookup[3]['part_of_a_set'] = 'TPOS'; - $ID3v2ShortFrameNameLookup[3]['publisher'] = 'TPUB'; - $ID3v2ShortFrameNameLookup[3]['tracknumber'] = 'TRCK'; - $ID3v2ShortFrameNameLookup[3]['internet_radio_station_name'] = 'TRSN'; - $ID3v2ShortFrameNameLookup[3]['internet_radio_station_owner'] = 'TRSO'; - $ID3v2ShortFrameNameLookup[3]['isrc'] = 'TSRC'; - $ID3v2ShortFrameNameLookup[3]['encoder_settings'] = 'TSSE'; - $ID3v2ShortFrameNameLookup[3]['user_text'] = 'TXXX'; - $ID3v2ShortFrameNameLookup[3]['unique_file_identifier'] = 'UFID'; - $ID3v2ShortFrameNameLookup[3]['terms_of_use'] = 'USER'; - $ID3v2ShortFrameNameLookup[3]['unsynchronised_lyrics'] = 'USLT'; - $ID3v2ShortFrameNameLookup[3]['commercial'] = 'WCOM'; - $ID3v2ShortFrameNameLookup[3]['copyright_information'] = 'WCOP'; - $ID3v2ShortFrameNameLookup[3]['url_file'] = 'WOAF'; - $ID3v2ShortFrameNameLookup[3]['url_artist'] = 'WOAR'; - $ID3v2ShortFrameNameLookup[3]['url_source'] = 'WOAS'; - $ID3v2ShortFrameNameLookup[3]['url_station'] = 'WORS'; - $ID3v2ShortFrameNameLookup[3]['payment'] = 'WPAY'; - $ID3v2ShortFrameNameLookup[3]['url_publisher'] = 'WPUB'; - $ID3v2ShortFrameNameLookup[3]['url_user'] = 'WXXX'; - - // The above are common to ID3v2.3 and ID3v2.4 - // so copy them to ID3v2.4 before adding specifics for 2.3 and 2.4 - $ID3v2ShortFrameNameLookup[4] = $ID3v2ShortFrameNameLookup[3]; - - // The following are unique to ID3v2.3 - $ID3v2ShortFrameNameLookup[3]['equalisation'] = 'EQUA'; - $ID3v2ShortFrameNameLookup[3]['involved_people_list'] = 'IPLS'; - $ID3v2ShortFrameNameLookup[3]['relative_volume_adjustment'] = 'RVAD'; - $ID3v2ShortFrameNameLookup[3]['date'] = 'TDAT'; - $ID3v2ShortFrameNameLookup[3]['time'] = 'TIME'; - $ID3v2ShortFrameNameLookup[3]['original_release_year'] = 'TORY'; - $ID3v2ShortFrameNameLookup[3]['recording_dates'] = 'TRDA'; - $ID3v2ShortFrameNameLookup[3]['size'] = 'TSIZ'; - $ID3v2ShortFrameNameLookup[3]['year'] = 'TYER'; - - - // The following are unique to ID3v2.4 - $ID3v2ShortFrameNameLookup[4]['audio_seek_point_index'] = 'ASPI'; - $ID3v2ShortFrameNameLookup[4]['equalisation'] = 'EQU2'; - $ID3v2ShortFrameNameLookup[4]['relative_volume_adjustment'] = 'RVA2'; - $ID3v2ShortFrameNameLookup[4]['seek'] = 'SEEK'; - $ID3v2ShortFrameNameLookup[4]['signature'] = 'SIGN'; - $ID3v2ShortFrameNameLookup[4]['encoding_time'] = 'TDEN'; - $ID3v2ShortFrameNameLookup[4]['original_release_time'] = 'TDOR'; - $ID3v2ShortFrameNameLookup[4]['recording_time'] = 'TDRC'; - $ID3v2ShortFrameNameLookup[4]['release_time'] = 'TDRL'; - $ID3v2ShortFrameNameLookup[4]['tagging_time'] = 'TDTG'; - $ID3v2ShortFrameNameLookup[4]['involved_people_list'] = 'TIPL'; - $ID3v2ShortFrameNameLookup[4]['musician_credits_list'] = 'TMCL'; - $ID3v2ShortFrameNameLookup[4]['mood'] = 'TMOO'; - $ID3v2ShortFrameNameLookup[4]['produced_notice'] = 'TPRO'; - $ID3v2ShortFrameNameLookup[4]['album_sort_order'] = 'TSOA'; - $ID3v2ShortFrameNameLookup[4]['performer_sort_order'] = 'TSOP'; - $ID3v2ShortFrameNameLookup[4]['title_sort_order'] = 'TSOT'; - $ID3v2ShortFrameNameLookup[4]['set_subtitle'] = 'TSST'; - } - return (isset($ID3v2ShortFrameNameLookup[$majorversion][strtolower($long_description)]) ? $ID3v2ShortFrameNameLookup[$majorversion][strtolower($long_description)] : ''); - - } - -} - diff --git a/src/Classes/Vendor/getid3/write.lyrics3.php b/src/Classes/Vendor/getid3/write.lyrics3.php deleted file mode 100755 index 1f85ebd0a..000000000 --- a/src/Classes/Vendor/getid3/write.lyrics3.php +++ /dev/null @@ -1,71 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// write.lyrics3.php // -// module for writing Lyrics3 tags // -// dependencies: module.tag.lyrics3.php // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_write_lyrics3 -{ - public $filename; - public $tag_data; - //public $lyrics3_version = 2; // 1 or 2 - public $warnings = array(); // any non-critical errors will be stored here - public $errors = array(); // any critical errors will be stored here - - public function getid3_write_lyrics3() { - return true; - } - - public function WriteLyrics3() { - $this->errors[] = 'WriteLyrics3() not yet functional - cannot write Lyrics3'; - return false; - } - public function DeleteLyrics3() { - // Initialize getID3 engine - $getID3 = new getID3; - $ThisFileInfo = $getID3->analyze($this->filename); - if (isset($ThisFileInfo['lyrics3']['tag_offset_start']) && isset($ThisFileInfo['lyrics3']['tag_offset_end'])) { - if (is_readable($this->filename) && is_writable($this->filename) && is_file($this->filename) && ($fp = fopen($this->filename, 'a+b'))) { - - flock($fp, LOCK_EX); - $oldignoreuserabort = ignore_user_abort(true); - - fseek($fp, $ThisFileInfo['lyrics3']['tag_offset_end'], SEEK_SET); - $DataAfterLyrics3 = ''; - if ($ThisFileInfo['filesize'] > $ThisFileInfo['lyrics3']['tag_offset_end']) { - $DataAfterLyrics3 = fread($fp, $ThisFileInfo['filesize'] - $ThisFileInfo['lyrics3']['tag_offset_end']); - } - - ftruncate($fp, $ThisFileInfo['lyrics3']['tag_offset_start']); - - if (!empty($DataAfterLyrics3)) { - fseek($fp, $ThisFileInfo['lyrics3']['tag_offset_start'], SEEK_SET); - fwrite($fp, $DataAfterLyrics3, strlen($DataAfterLyrics3)); - } - - flock($fp, LOCK_UN); - fclose($fp); - ignore_user_abort($oldignoreuserabort); - - return true; - - } else { - $this->errors[] = 'Cannot fopen('.$this->filename.', "a+b")'; - return false; - } - } - // no Lyrics3 present - return true; - } - -} diff --git a/src/Classes/Vendor/getid3/write.metaflac.php b/src/Classes/Vendor/getid3/write.metaflac.php deleted file mode 100755 index f20ea6048..000000000 --- a/src/Classes/Vendor/getid3/write.metaflac.php +++ /dev/null @@ -1,161 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// write.metaflac.php // -// module for writing metaflac tags // -// dependencies: /helperapps/metaflac.exe // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_write_metaflac -{ - - public $filename; - public $tag_data; - public $warnings = array(); // any non-critical errors will be stored here - public $errors = array(); // any critical errors will be stored here - - public function getid3_write_metaflac() { - return true; - } - - public function WriteMetaFLAC() { - - if (preg_match('#(1|ON)#i', ini_get('safe_mode'))) { - $this->errors[] = 'PHP running in Safe Mode (backtick operator not available) - cannot call metaflac, tags not written'; - return false; - } - - // Create file with new comments - $tempcommentsfilename = tempnam(GETID3_TEMP_DIR, 'getID3'); - if (is_writable($tempcommentsfilename) && is_file($tempcommentsfilename) && ($fpcomments = fopen($tempcommentsfilename, 'wb'))) { - foreach ($this->tag_data as $key => $value) { - foreach ($value as $commentdata) { - fwrite($fpcomments, $this->CleanmetaflacName($key).'='.$commentdata."\n"); - } - } - fclose($fpcomments); - - } else { - $this->errors[] = 'failed to open temporary tags file, tags not written - fopen("'.$tempcommentsfilename.'", "wb")'; - return false; - } - - $oldignoreuserabort = ignore_user_abort(true); - if (GETID3_OS_ISWINDOWS) { - - if (file_exists(GETID3_HELPERAPPSDIR.'metaflac.exe')) { - //$commandline = '"'.GETID3_HELPERAPPSDIR.'metaflac.exe" --no-utf8-convert --remove-all-tags --import-tags-from="'.$tempcommentsfilename.'" "'.str_replace('/', '\\', $this->filename).'"'; - // metaflac works fine if you copy-paste the above commandline into a command prompt, - // but refuses to work with `backtick` if there are "doublequotes" present around BOTH - // the metaflac pathname and the target filename. For whatever reason...?? - // The solution is simply ensure that the metaflac pathname has no spaces, - // and therefore does not need to be quoted - - // On top of that, if error messages are not always captured properly under Windows - // To at least see if there was a problem, compare file modification timestamps before and after writing - clearstatcache(); - $timestampbeforewriting = filemtime($this->filename); - - $commandline = GETID3_HELPERAPPSDIR.'metaflac.exe --no-utf8-convert --remove-all-tags --import-tags-from='.escapeshellarg($tempcommentsfilename).' '.escapeshellarg($this->filename).' 2>&1'; - $metaflacError = `$commandline`; - - if (empty($metaflacError)) { - clearstatcache(); - if ($timestampbeforewriting == filemtime($this->filename)) { - $metaflacError = 'File modification timestamp has not changed - it looks like the tags were not written'; - } - } - } else { - $metaflacError = 'metaflac.exe not found in '.GETID3_HELPERAPPSDIR; - } - - } else { - - // It's simpler on *nix - $commandline = 'metaflac --no-utf8-convert --remove-all-tags --import-tags-from='.escapeshellarg($tempcommentsfilename).' '.escapeshellarg($this->filename).' 2>&1'; - $metaflacError = `$commandline`; - - } - - // Remove temporary comments file - unlink($tempcommentsfilename); - ignore_user_abort($oldignoreuserabort); - - if (!empty($metaflacError)) { - - $this->errors[] = 'System call to metaflac failed with this message returned: '."\n\n".$metaflacError; - return false; - - } - - return true; - } - - - public function DeleteMetaFLAC() { - - if (preg_match('#(1|ON)#i', ini_get('safe_mode'))) { - $this->errors[] = 'PHP running in Safe Mode (backtick operator not available) - cannot call metaflac, tags not deleted'; - return false; - } - - $oldignoreuserabort = ignore_user_abort(true); - if (GETID3_OS_ISWINDOWS) { - - if (file_exists(GETID3_HELPERAPPSDIR.'metaflac.exe')) { - // To at least see if there was a problem, compare file modification timestamps before and after writing - clearstatcache(); - $timestampbeforewriting = filemtime($this->filename); - - $commandline = GETID3_HELPERAPPSDIR.'metaflac.exe --remove-all-tags "'.$this->filename.'" 2>&1'; - $metaflacError = `$commandline`; - - if (empty($metaflacError)) { - clearstatcache(); - if ($timestampbeforewriting == filemtime($this->filename)) { - $metaflacError = 'File modification timestamp has not changed - it looks like the tags were not deleted'; - } - } - } else { - $metaflacError = 'metaflac.exe not found in '.GETID3_HELPERAPPSDIR; - } - - } else { - - // It's simpler on *nix - $commandline = 'metaflac --remove-all-tags "'.$this->filename.'" 2>&1'; - $metaflacError = `$commandline`; - - } - - ignore_user_abort($oldignoreuserabort); - - if (!empty($metaflacError)) { - $this->errors[] = 'System call to metaflac failed with this message returned: '."\n\n".$metaflacError; - return false; - } - return true; - } - - - public function CleanmetaflacName($originalcommentname) { - // A case-insensitive field name that may consist of ASCII 0x20 through 0x7D, 0x3D ('=') excluded. - // ASCII 0x41 through 0x5A inclusive (A-Z) is to be considered equivalent to ASCII 0x61 through - // 0x7A inclusive (a-z). - - // replace invalid chars with a space, return uppercase text - // Thanks Chris Bolt for improving this function - // note: *reg_replace() replaces nulls with empty string (not space) - return strtoupper(preg_replace('#[^ -<>-}]#', ' ', str_replace("\x00", ' ', $originalcommentname))); - - } - -} diff --git a/src/Classes/Vendor/getid3/write.php b/src/Classes/Vendor/getid3/write.php deleted file mode 100755 index 3a7f1974d..000000000 --- a/src/Classes/Vendor/getid3/write.php +++ /dev/null @@ -1,613 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -/// // -// write.php // -// module for writing tags (APEv2, ID3v1, ID3v2) // -// dependencies: getid3.lib.php // -// write.apetag.php (optional) // -// write.id3v1.php (optional) // -// write.id3v2.php (optional) // -// write.vorbiscomment.php (optional) // -// write.metaflac.php (optional) // -// write.lyrics3.php (optional) // -// /// -///////////////////////////////////////////////////////////////// - -if (!defined('GETID3_INCLUDEPATH')) { - throw new Exception('getid3.php MUST be included before calling getid3_writetags'); -} -if (!include_once(GETID3_INCLUDEPATH.'getid3.lib.php')) { - throw new Exception('write.php depends on getid3.lib.php, which is missing.'); -} - - -// NOTES: -// -// You should pass data here with standard field names as follows: -// * TITLE -// * ARTIST -// * ALBUM -// * TRACKNUMBER -// * COMMENT -// * GENRE -// * YEAR -// * ATTACHED_PICTURE (ID3v2 only) -// -// http://www.personal.uni-jena.de/~pfk/mpp/sv8/apekey.html -// The APEv2 Tag Items Keys definition says "TRACK" is correct but foobar2000 uses "TRACKNUMBER" instead -// Pass data here as "TRACKNUMBER" for compatability with all formats - - -class getid3_writetags -{ - // public - public $filename; // absolute filename of file to write tags to - public $tagformats = array(); // array of tag formats to write ('id3v1', 'id3v2.2', 'id2v2.3', 'id3v2.4', 'ape', 'vorbiscomment', 'metaflac', 'real') - public $tag_data = array(array()); // 2-dimensional array of tag data (ex: $data['ARTIST'][0] = 'Elvis') - public $tag_encoding = 'ISO-8859-1'; // text encoding used for tag data ('ISO-8859-1', 'UTF-8', 'UTF-16', 'UTF-16LE', 'UTF-16BE', ) - public $overwrite_tags = true; // if true will erase existing tag data and write only passed data; if false will merge passed data with existing tag data - public $remove_other_tags = false; // if true will erase remove all existing tags and only write those passed in $tagformats; if false will ignore any tags not mentioned in $tagformats - - public $id3v2_tag_language = 'eng'; // ISO-639-2 3-character language code needed for some ID3v2 frames (http://www.id3.org/iso639-2.html) - public $id3v2_paddedlength = 4096; // minimum length of ID3v2 tags (will be padded to this length if tag data is shorter) - - public $warnings = array(); // any non-critical errors will be stored here - public $errors = array(); // any critical errors will be stored here - - // private - private $ThisFileInfo; // analysis of file before writing - - public function getid3_writetags() { - return true; - } - - - public function WriteTags() { - - if (empty($this->filename)) { - $this->errors[] = 'filename is undefined in getid3_writetags'; - return false; - } elseif (!file_exists($this->filename)) { - $this->errors[] = 'filename set to non-existant file "'.$this->filename.'" in getid3_writetags'; - return false; - } - - if (!is_array($this->tagformats)) { - $this->errors[] = 'tagformats must be an array in getid3_writetags'; - return false; - } - - $TagFormatsToRemove = array(); - if (filesize($this->filename) == 0) { - - // empty file special case - allow any tag format, don't check existing format - // could be useful if you want to generate tag data for a non-existant file - $this->ThisFileInfo = array('fileformat'=>''); - $AllowedTagFormats = array('id3v1', 'id3v2.2', 'id3v2.3', 'id3v2.4', 'ape', 'lyrics3'); - - } else { - - $getID3 = new getID3; - $getID3->encoding = $this->tag_encoding; - $this->ThisFileInfo = $getID3->analyze($this->filename); - - // check for what file types are allowed on this fileformat - switch (isset($this->ThisFileInfo['fileformat']) ? $this->ThisFileInfo['fileformat'] : '') { - case 'mp3': - case 'mp2': - case 'mp1': - case 'riff': // maybe not officially, but people do it anyway - $AllowedTagFormats = array('id3v1', 'id3v2.2', 'id3v2.3', 'id3v2.4', 'ape', 'lyrics3'); - break; - - case 'mpc': - $AllowedTagFormats = array('ape'); - break; - - case 'flac': - $AllowedTagFormats = array('metaflac'); - break; - - case 'real': - $AllowedTagFormats = array('real'); - break; - - case 'ogg': - switch (isset($this->ThisFileInfo['audio']['dataformat']) ? $this->ThisFileInfo['audio']['dataformat'] : '') { - case 'flac': - //$AllowedTagFormats = array('metaflac'); - $this->errors[] = 'metaflac is not (yet) compatible with OggFLAC files'; - return false; - break; - case 'vorbis': - $AllowedTagFormats = array('vorbiscomment'); - break; - default: - $this->errors[] = 'metaflac is not (yet) compatible with Ogg files other than OggVorbis'; - return false; - break; - } - break; - - default: - $AllowedTagFormats = array(); - break; - } - foreach ($this->tagformats as $requested_tag_format) { - if (!in_array($requested_tag_format, $AllowedTagFormats)) { - $errormessage = 'Tag format "'.$requested_tag_format.'" is not allowed on "'.(isset($this->ThisFileInfo['fileformat']) ? $this->ThisFileInfo['fileformat'] : ''); - $errormessage .= (isset($this->ThisFileInfo['audio']['dataformat']) ? '.'.$this->ThisFileInfo['audio']['dataformat'] : ''); - $errormessage .= '" files'; - $this->errors[] = $errormessage; - return false; - } - } - - // List of other tag formats, removed if requested - if ($this->remove_other_tags) { - foreach ($AllowedTagFormats as $AllowedTagFormat) { - switch ($AllowedTagFormat) { - case 'id3v2.2': - case 'id3v2.3': - case 'id3v2.4': - if (!in_array('id3v2', $TagFormatsToRemove) && !in_array('id3v2.2', $this->tagformats) && !in_array('id3v2.3', $this->tagformats) && !in_array('id3v2.4', $this->tagformats)) { - $TagFormatsToRemove[] = 'id3v2'; - } - break; - - default: - if (!in_array($AllowedTagFormat, $this->tagformats)) { - $TagFormatsToRemove[] = $AllowedTagFormat; - } - break; - } - } - } - } - - $WritingFilesToInclude = array_merge($this->tagformats, $TagFormatsToRemove); - - // Check for required include files and include them - foreach ($WritingFilesToInclude as $tagformat) { - switch ($tagformat) { - case 'ape': - $GETID3_ERRORARRAY = &$this->errors; - if (!getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'write.apetag.php', __FILE__, false)) { - return false; - } - break; - - case 'id3v1': - case 'lyrics3': - case 'vorbiscomment': - case 'metaflac': - case 'real': - $GETID3_ERRORARRAY = &$this->errors; - if (!getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'write.'.$tagformat.'.php', __FILE__, false)) { - return false; - } - break; - - case 'id3v2.2': - case 'id3v2.3': - case 'id3v2.4': - case 'id3v2': - $GETID3_ERRORARRAY = &$this->errors; - if (!getid3_lib::IncludeDependency(GETID3_INCLUDEPATH.'write.id3v2.php', __FILE__, false)) { - return false; - } - break; - - default: - $this->errors[] = 'unknown tag format "'.$tagformat.'" in $tagformats in WriteTags()'; - return false; - break; - } - - } - - // Validation of supplied data - if (!is_array($this->tag_data)) { - $this->errors[] = '$this->tag_data is not an array in WriteTags()'; - return false; - } - // convert supplied data array keys to upper case, if they're not already - foreach ($this->tag_data as $tag_key => $tag_array) { - if (strtoupper($tag_key) !== $tag_key) { - $this->tag_data[strtoupper($tag_key)] = $this->tag_data[$tag_key]; - unset($this->tag_data[$tag_key]); - } - } - // convert source data array keys to upper case, if they're not already - if (!empty($this->ThisFileInfo['tags'])) { - foreach ($this->ThisFileInfo['tags'] as $tag_format => $tag_data_array) { - foreach ($tag_data_array as $tag_key => $tag_array) { - if (strtoupper($tag_key) !== $tag_key) { - $this->ThisFileInfo['tags'][$tag_format][strtoupper($tag_key)] = $this->ThisFileInfo['tags'][$tag_format][$tag_key]; - unset($this->ThisFileInfo['tags'][$tag_format][$tag_key]); - } - } - } - } - - // Convert "TRACK" to "TRACKNUMBER" (if needed) for compatability with all formats - if (isset($this->tag_data['TRACK']) && !isset($this->tag_data['TRACKNUMBER'])) { - $this->tag_data['TRACKNUMBER'] = $this->tag_data['TRACK']; - unset($this->tag_data['TRACK']); - } - - // Remove all other tag formats, if requested - if ($this->remove_other_tags) { - $this->DeleteTags($TagFormatsToRemove); - } - - // Write data for each tag format - foreach ($this->tagformats as $tagformat) { - $success = false; // overridden if tag writing is successful - switch ($tagformat) { - case 'ape': - $ape_writer = new getid3_write_apetag; - if (($ape_writer->tag_data = $this->FormatDataForAPE()) !== false) { - $ape_writer->filename = $this->filename; - if (($success = $ape_writer->WriteAPEtag()) === false) { - $this->errors[] = 'WriteAPEtag() failed with message(s):
    • '.str_replace("\n", '
    • ', htmlentities(trim(implode("\n", $ape_writer->errors)))).'
    '; - } - } else { - $this->errors[] = 'FormatDataForAPE() failed'; - } - break; - - case 'id3v1': - $id3v1_writer = new getid3_write_id3v1; - if (($id3v1_writer->tag_data = $this->FormatDataForID3v1()) !== false) { - $id3v1_writer->filename = $this->filename; - if (($success = $id3v1_writer->WriteID3v1()) === false) { - $this->errors[] = 'WriteID3v1() failed with message(s):
    • '.str_replace("\n", '
    • ', htmlentities(trim(implode("\n", $id3v1_writer->errors)))).'
    '; - } - } else { - $this->errors[] = 'FormatDataForID3v1() failed'; - } - break; - - case 'id3v2.2': - case 'id3v2.3': - case 'id3v2.4': - $id3v2_writer = new getid3_write_id3v2; - $id3v2_writer->majorversion = intval(substr($tagformat, -1)); - $id3v2_writer->paddedlength = $this->id3v2_paddedlength; - if (($id3v2_writer->tag_data = $this->FormatDataForID3v2($id3v2_writer->majorversion)) !== false) { - $id3v2_writer->filename = $this->filename; - if (($success = $id3v2_writer->WriteID3v2()) === false) { - $this->errors[] = 'WriteID3v2() failed with message(s):
    • '.str_replace("\n", '
    • ', htmlentities(trim(implode("\n", $id3v2_writer->errors)))).'
    '; - } - } else { - $this->errors[] = 'FormatDataForID3v2() failed'; - } - break; - - case 'vorbiscomment': - $vorbiscomment_writer = new getid3_write_vorbiscomment; - if (($vorbiscomment_writer->tag_data = $this->FormatDataForVorbisComment()) !== false) { - $vorbiscomment_writer->filename = $this->filename; - if (($success = $vorbiscomment_writer->WriteVorbisComment()) === false) { - $this->errors[] = 'WriteVorbisComment() failed with message(s):
    • '.str_replace("\n", '
    • ', htmlentities(trim(implode("\n", $vorbiscomment_writer->errors)))).'
    '; - } - } else { - $this->errors[] = 'FormatDataForVorbisComment() failed'; - } - break; - - case 'metaflac': - $metaflac_writer = new getid3_write_metaflac; - if (($metaflac_writer->tag_data = $this->FormatDataForMetaFLAC()) !== false) { - $metaflac_writer->filename = $this->filename; - if (($success = $metaflac_writer->WriteMetaFLAC()) === false) { - $this->errors[] = 'WriteMetaFLAC() failed with message(s):
    • '.str_replace("\n", '
    • ', htmlentities(trim(implode("\n", $metaflac_writer->errors)))).'
    '; - } - } else { - $this->errors[] = 'FormatDataForMetaFLAC() failed'; - } - break; - - case 'real': - $real_writer = new getid3_write_real; - if (($real_writer->tag_data = $this->FormatDataForReal()) !== false) { - $real_writer->filename = $this->filename; - if (($success = $real_writer->WriteReal()) === false) { - $this->errors[] = 'WriteReal() failed with message(s):
    • '.str_replace("\n", '
    • ', htmlentities(trim(implode("\n", $real_writer->errors)))).'
    '; - } - } else { - $this->errors[] = 'FormatDataForReal() failed'; - } - break; - - default: - $this->errors[] = 'Invalid tag format to write: "'.$tagformat.'"'; - return false; - break; - } - if (!$success) { - return false; - } - } - return true; - - } - - - public function DeleteTags($TagFormatsToDelete) { - foreach ($TagFormatsToDelete as $DeleteTagFormat) { - $success = false; // overridden if tag deletion is successful - switch ($DeleteTagFormat) { - case 'id3v1': - $id3v1_writer = new getid3_write_id3v1; - $id3v1_writer->filename = $this->filename; - if (($success = $id3v1_writer->RemoveID3v1()) === false) { - $this->errors[] = 'RemoveID3v1() failed with message(s):
    • '.trim(implode('
    • ', $id3v1_writer->errors)).'
    '; - } - break; - - case 'id3v2': - $id3v2_writer = new getid3_write_id3v2; - $id3v2_writer->filename = $this->filename; - if (($success = $id3v2_writer->RemoveID3v2()) === false) { - $this->errors[] = 'RemoveID3v2() failed with message(s):
    • '.trim(implode('
    • ', $id3v2_writer->errors)).'
    '; - } - break; - - case 'ape': - $ape_writer = new getid3_write_apetag; - $ape_writer->filename = $this->filename; - if (($success = $ape_writer->DeleteAPEtag()) === false) { - $this->errors[] = 'DeleteAPEtag() failed with message(s):
    • '.trim(implode('
    • ', $ape_writer->errors)).'
    '; - } - break; - - case 'vorbiscomment': - $vorbiscomment_writer = new getid3_write_vorbiscomment; - $vorbiscomment_writer->filename = $this->filename; - if (($success = $vorbiscomment_writer->DeleteVorbisComment()) === false) { - $this->errors[] = 'DeleteVorbisComment() failed with message(s):
    • '.trim(implode('
    • ', $vorbiscomment_writer->errors)).'
    '; - } - break; - - case 'metaflac': - $metaflac_writer = new getid3_write_metaflac; - $metaflac_writer->filename = $this->filename; - if (($success = $metaflac_writer->DeleteMetaFLAC()) === false) { - $this->errors[] = 'DeleteMetaFLAC() failed with message(s):
    • '.trim(implode('
    • ', $metaflac_writer->errors)).'
    '; - } - break; - - case 'lyrics3': - $lyrics3_writer = new getid3_write_lyrics3; - $lyrics3_writer->filename = $this->filename; - if (($success = $lyrics3_writer->DeleteLyrics3()) === false) { - $this->errors[] = 'DeleteLyrics3() failed with message(s):
    • '.trim(implode('
    • ', $lyrics3_writer->errors)).'
    '; - } - break; - - case 'real': - $real_writer = new getid3_write_real; - $real_writer->filename = $this->filename; - if (($success = $real_writer->RemoveReal()) === false) { - $this->errors[] = 'RemoveReal() failed with message(s):
    • '.trim(implode('
    • ', $real_writer->errors)).'
    '; - } - break; - - default: - $this->errors[] = 'Invalid tag format to delete: "'.$tagformat.'"'; - return false; - break; - } - if (!$success) { - return false; - } - } - return true; - } - - - public function MergeExistingTagData($TagFormat, &$tag_data) { - // Merge supplied data with existing data, if requested - if ($this->overwrite_tags) { - // do nothing - ignore previous data - } else { -throw new Exception('$this->overwrite_tags=false is known to be buggy in this version of getID3. Will be fixed in the near future, check www.getid3.org for a newer version.'); - if (!isset($this->ThisFileInfo['tags'][$TagFormat])) { - return false; - } - $tag_data = array_merge_recursive($tag_data, $this->ThisFileInfo['tags'][$TagFormat]); - } - return true; - } - - public function FormatDataForAPE() { - $ape_tag_data = array(); - foreach ($this->tag_data as $tag_key => $valuearray) { - switch ($tag_key) { - case 'ATTACHED_PICTURE': - // ATTACHED_PICTURE is ID3v2 only - ignore - $this->warnings[] = '$data['.$tag_key.'] is assumed to be ID3v2 APIC data - NOT written to APE tag'; - break; - - default: - foreach ($valuearray as $key => $value) { - if (is_string($value) || is_numeric($value)) { - $ape_tag_data[$tag_key][$key] = getid3_lib::iconv_fallback($this->tag_encoding, 'UTF-8', $value); - } else { - $this->warnings[] = '$data['.$tag_key.']['.$key.'] is not a string value - all of $data['.$tag_key.'] NOT written to APE tag'; - unset($ape_tag_data[$tag_key]); - break; - } - } - break; - } - } - $this->MergeExistingTagData('ape', $ape_tag_data); - return $ape_tag_data; - } - - - public function FormatDataForID3v1() { - $tag_data_id3v1['genreid'] = 255; - if (!empty($this->tag_data['GENRE'])) { - foreach ($this->tag_data['GENRE'] as $key => $value) { - if (getid3_id3v1::LookupGenreID($value) !== false) { - $tag_data_id3v1['genreid'] = getid3_id3v1::LookupGenreID($value); - break; - } - } - } - $tag_data_id3v1['title'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['TITLE'] ) ? $this->tag_data['TITLE'] : array()))); - $tag_data_id3v1['artist'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['ARTIST'] ) ? $this->tag_data['ARTIST'] : array()))); - $tag_data_id3v1['album'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['ALBUM'] ) ? $this->tag_data['ALBUM'] : array()))); - $tag_data_id3v1['year'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['YEAR'] ) ? $this->tag_data['YEAR'] : array()))); - $tag_data_id3v1['comment'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['COMMENT'] ) ? $this->tag_data['COMMENT'] : array()))); - $tag_data_id3v1['track'] = intval(getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['TRACKNUMBER']) ? $this->tag_data['TRACKNUMBER'] : array())))); - if ($tag_data_id3v1['track'] <= 0) { - $tag_data_id3v1['track'] = ''; - } - - $this->MergeExistingTagData('id3v1', $tag_data_id3v1); - return $tag_data_id3v1; - } - - public function FormatDataForID3v2($id3v2_majorversion) { - $tag_data_id3v2 = array(); - - $ID3v2_text_encoding_lookup[2] = array('ISO-8859-1'=>0, 'UTF-16'=>1); - $ID3v2_text_encoding_lookup[3] = array('ISO-8859-1'=>0, 'UTF-16'=>1); - $ID3v2_text_encoding_lookup[4] = array('ISO-8859-1'=>0, 'UTF-16'=>1, 'UTF-16BE'=>2, 'UTF-8'=>3); - foreach ($this->tag_data as $tag_key => $valuearray) { - $ID3v2_framename = getid3_write_id3v2::ID3v2ShortFrameNameLookup($id3v2_majorversion, $tag_key); - switch ($ID3v2_framename) { - case 'APIC': - foreach ($valuearray as $key => $apic_data_array) { - if (isset($apic_data_array['data']) && - isset($apic_data_array['picturetypeid']) && - isset($apic_data_array['description']) && - isset($apic_data_array['mime'])) { - $tag_data_id3v2['APIC'][] = $apic_data_array; - } else { - $this->errors[] = 'ID3v2 APIC data is not properly structured'; - return false; - } - } - break; - - case '': - $this->errors[] = 'ID3v2: Skipping "'.$tag_key.'" because cannot match it to a known ID3v2 frame type'; - // some other data type, don't know how to handle it, ignore it - break; - - default: - // most other (text) frames can be copied over as-is - foreach ($valuearray as $key => $value) { - if (isset($ID3v2_text_encoding_lookup[$id3v2_majorversion][$this->tag_encoding])) { - // source encoding is valid in ID3v2 - use it with no conversion - $tag_data_id3v2[$ID3v2_framename][$key]['encodingid'] = $ID3v2_text_encoding_lookup[$id3v2_majorversion][$this->tag_encoding]; - $tag_data_id3v2[$ID3v2_framename][$key]['data'] = $value; - } else { - // source encoding is NOT valid in ID3v2 - convert it to an ID3v2-valid encoding first - if ($id3v2_majorversion < 4) { - // convert data from other encoding to UTF-16 (with BOM) - // note: some software, notably Windows Media Player and iTunes are broken and treat files tagged with UTF-16BE (with BOM) as corrupt - // therefore we force data to UTF-16LE and manually prepend the BOM - $ID3v2_tag_data_converted = false; - if (!$ID3v2_tag_data_converted && ($this->tag_encoding == 'ISO-8859-1')) { - // great, leave data as-is for minimum compatability problems - $tag_data_id3v2[$ID3v2_framename][$key]['encodingid'] = 0; - $tag_data_id3v2[$ID3v2_framename][$key]['data'] = $value; - $ID3v2_tag_data_converted = true; - } - if (!$ID3v2_tag_data_converted && ($this->tag_encoding == 'UTF-8')) { - do { - // if UTF-8 string does not include any characters above chr(127) then it is identical to ISO-8859-1 - for ($i = 0; $i < strlen($value); $i++) { - if (ord($value{$i}) > 127) { - break 2; - } - } - $tag_data_id3v2[$ID3v2_framename][$key]['encodingid'] = 0; - $tag_data_id3v2[$ID3v2_framename][$key]['data'] = $value; - $ID3v2_tag_data_converted = true; - } while (false); - } - if (!$ID3v2_tag_data_converted) { - $tag_data_id3v2[$ID3v2_framename][$key]['encodingid'] = 1; - //$tag_data_id3v2[$ID3v2_framename][$key]['data'] = getid3_lib::iconv_fallback($this->tag_encoding, 'UTF-16', $value); // output is UTF-16LE+BOM or UTF-16BE+BOM depending on system architecture - $tag_data_id3v2[$ID3v2_framename][$key]['data'] = "\xFF\xFE".getid3_lib::iconv_fallback($this->tag_encoding, 'UTF-16LE', $value); // force LittleEndian order version of UTF-16 - $ID3v2_tag_data_converted = true; - } - - } else { - // convert data from other encoding to UTF-8 - $tag_data_id3v2[$ID3v2_framename][$key]['encodingid'] = 3; - $tag_data_id3v2[$ID3v2_framename][$key]['data'] = getid3_lib::iconv_fallback($this->tag_encoding, 'UTF-8', $value); - } - } - - // These values are not needed for all frame types, but if they're not used no matter - $tag_data_id3v2[$ID3v2_framename][$key]['description'] = ''; - $tag_data_id3v2[$ID3v2_framename][$key]['language'] = $this->id3v2_tag_language; - } - break; - } - } - $this->MergeExistingTagData('id3v2', $tag_data_id3v2); - return $tag_data_id3v2; - } - - public function FormatDataForVorbisComment() { - $tag_data_vorbiscomment = $this->tag_data; - - // check for multi-line comment values - split out to multiple comments if neccesary - // and convert data to UTF-8 strings - foreach ($tag_data_vorbiscomment as $tag_key => $valuearray) { - foreach ($valuearray as $key => $value) { - str_replace("\r", "\n", $value); - if (strstr($value, "\n")) { - unset($tag_data_vorbiscomment[$tag_key][$key]); - $multilineexploded = explode("\n", $value); - foreach ($multilineexploded as $newcomment) { - if (strlen(trim($newcomment)) > 0) { - $tag_data_vorbiscomment[$tag_key][] = getid3_lib::iconv_fallback($this->tag_encoding, 'UTF-8', $newcomment); - } - } - } elseif (is_string($value) || is_numeric($value)) { - $tag_data_vorbiscomment[$tag_key][$key] = getid3_lib::iconv_fallback($this->tag_encoding, 'UTF-8', $value); - } else { - $this->warnings[] = '$data['.$tag_key.']['.$key.'] is not a string value - all of $data['.$tag_key.'] NOT written to VorbisComment tag'; - unset($tag_data_vorbiscomment[$tag_key]); - break; - } - } - } - $this->MergeExistingTagData('vorbiscomment', $tag_data_vorbiscomment); - return $tag_data_vorbiscomment; - } - - public function FormatDataForMetaFLAC() { - // FLAC & OggFLAC use VorbisComments same as OggVorbis - // but require metaflac to do the writing rather than vorbiscomment - return $this->FormatDataForVorbisComment(); - } - - public function FormatDataForReal() { - $tag_data_real['title'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['TITLE'] ) ? $this->tag_data['TITLE'] : array()))); - $tag_data_real['artist'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['ARTIST'] ) ? $this->tag_data['ARTIST'] : array()))); - $tag_data_real['copyright'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['COPYRIGHT']) ? $this->tag_data['COPYRIGHT'] : array()))); - $tag_data_real['comment'] = getid3_lib::iconv_fallback($this->tag_encoding, 'ISO-8859-1', implode(' ', (isset($this->tag_data['COMMENT'] ) ? $this->tag_data['COMMENT'] : array()))); - - $this->MergeExistingTagData('real', $tag_data_real); - return $tag_data_real; - } - -} diff --git a/src/Classes/Vendor/getid3/write.real.php b/src/Classes/Vendor/getid3/write.real.php deleted file mode 100755 index 02b916528..000000000 --- a/src/Classes/Vendor/getid3/write.real.php +++ /dev/null @@ -1,273 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// write.real.php // -// module for writing RealAudio/RealVideo tags // -// dependencies: module.tag.real.php // -// /// -///////////////////////////////////////////////////////////////// - -class getid3_write_real -{ - public $filename; - public $tag_data = array(); - public $fread_buffer_size = 32768; // read buffer size in bytes - public $warnings = array(); // any non-critical errors will be stored here - public $errors = array(); // any critical errors will be stored here - public $paddedlength = 512; // minimum length of CONT tag in bytes - - public function getid3_write_real() { - return true; - } - - public function WriteReal() { - // File MUST be writeable - CHMOD(646) at least - if (is_writeable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'r+b'))) { - - // Initialize getID3 engine - $getID3 = new getID3; - $OldThisFileInfo = $getID3->analyze($this->filename); - if (empty($OldThisFileInfo['real']['chunks']) && !empty($OldThisFileInfo['real']['old_ra_header'])) { - $this->errors[] = 'Cannot write Real tags on old-style file format'; - fclose($fp_source); - return false; - } - - if (empty($OldThisFileInfo['real']['chunks'])) { - $this->errors[] = 'Cannot write Real tags because cannot find DATA chunk in file'; - fclose($fp_source); - return false; - } - foreach ($OldThisFileInfo['real']['chunks'] as $chunknumber => $chunkarray) { - $oldChunkInfo[$chunkarray['name']] = $chunkarray; - } - if (!empty($oldChunkInfo['CONT']['length'])) { - $this->paddedlength = max($oldChunkInfo['CONT']['length'], $this->paddedlength); - } - - $new_CONT_tag_data = $this->GenerateCONTchunk(); - $new_PROP_tag_data = $this->GeneratePROPchunk($OldThisFileInfo['real']['chunks'], $new_CONT_tag_data); - $new__RMF_tag_data = $this->GenerateRMFchunk($OldThisFileInfo['real']['chunks']); - - if (isset($oldChunkInfo['.RMF']['length']) && ($oldChunkInfo['.RMF']['length'] == strlen($new__RMF_tag_data))) { - fseek($fp_source, $oldChunkInfo['.RMF']['offset'], SEEK_SET); - fwrite($fp_source, $new__RMF_tag_data); - } else { - $this->errors[] = 'new .RMF tag ('.strlen($new__RMF_tag_data).' bytes) different length than old .RMF tag ('.$oldChunkInfo['.RMF']['length'].' bytes)'; - fclose($fp_source); - return false; - } - - if (isset($oldChunkInfo['PROP']['length']) && ($oldChunkInfo['PROP']['length'] == strlen($new_PROP_tag_data))) { - fseek($fp_source, $oldChunkInfo['PROP']['offset'], SEEK_SET); - fwrite($fp_source, $new_PROP_tag_data); - } else { - $this->errors[] = 'new PROP tag ('.strlen($new_PROP_tag_data).' bytes) different length than old PROP tag ('.$oldChunkInfo['PROP']['length'].' bytes)'; - fclose($fp_source); - return false; - } - - if (isset($oldChunkInfo['CONT']['length']) && ($oldChunkInfo['CONT']['length'] == strlen($new_CONT_tag_data))) { - - // new data length is same as old data length - just overwrite - fseek($fp_source, $oldChunkInfo['CONT']['offset'], SEEK_SET); - fwrite($fp_source, $new_CONT_tag_data); - fclose($fp_source); - return true; - - } else { - - if (empty($oldChunkInfo['CONT'])) { - // no existing CONT chunk - $BeforeOffset = $oldChunkInfo['DATA']['offset']; - $AfterOffset = $oldChunkInfo['DATA']['offset']; - } else { - // new data is longer than old data - $BeforeOffset = $oldChunkInfo['CONT']['offset']; - $AfterOffset = $oldChunkInfo['CONT']['offset'] + $oldChunkInfo['CONT']['length']; - } - if ($tempfilename = tempnam(GETID3_TEMP_DIR, 'getID3')) { - if (is_writable($tempfilename) && is_file($tempfilename) && ($fp_temp = fopen($tempfilename, 'wb'))) { - - rewind($fp_source); - fwrite($fp_temp, fread($fp_source, $BeforeOffset)); - fwrite($fp_temp, $new_CONT_tag_data); - fseek($fp_source, $AfterOffset, SEEK_SET); - while ($buffer = fread($fp_source, $this->fread_buffer_size)) { - fwrite($fp_temp, $buffer, strlen($buffer)); - } - fclose($fp_temp); - - if (copy($tempfilename, $this->filename)) { - unlink($tempfilename); - fclose($fp_source); - return true; - } - unlink($tempfilename); - $this->errors[] = 'FAILED: copy('.$tempfilename.', '.$this->filename.')'; - - } else { - $this->errors[] = 'Could not fopen("'.$tempfilename.'", "wb")'; - } - } - fclose($fp_source); - return false; - - } - - } - $this->errors[] = 'Could not fopen("'.$this->filename.'", "r+b")'; - return false; - } - - public function GenerateRMFchunk(&$chunks) { - $oldCONTexists = false; - foreach ($chunks as $key => $chunk) { - $chunkNameKeys[$chunk['name']] = $key; - if ($chunk['name'] == 'CONT') { - $oldCONTexists = true; - } - } - $newHeadersCount = $chunks[$chunkNameKeys['.RMF']]['headers_count'] + ($oldCONTexists ? 0 : 1); - - $RMFchunk = "\x00\x00"; // object version - $RMFchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['.RMF']]['file_version'], 4); - $RMFchunk .= getid3_lib::BigEndian2String($newHeadersCount, 4); - - $RMFchunk = '.RMF'.getid3_lib::BigEndian2String(strlen($RMFchunk) + 8, 4).$RMFchunk; // .RMF chunk identifier + chunk length - return $RMFchunk; - } - - public function GeneratePROPchunk(&$chunks, &$new_CONT_tag_data) { - $old_CONT_length = 0; - $old_DATA_offset = 0; - $old_INDX_offset = 0; - foreach ($chunks as $key => $chunk) { - $chunkNameKeys[$chunk['name']] = $key; - if ($chunk['name'] == 'CONT') { - $old_CONT_length = $chunk['length']; - } elseif ($chunk['name'] == 'DATA') { - if (!$old_DATA_offset) { - $old_DATA_offset = $chunk['offset']; - } - } elseif ($chunk['name'] == 'INDX') { - if (!$old_INDX_offset) { - $old_INDX_offset = $chunk['offset']; - } - } - } - $CONTdelta = strlen($new_CONT_tag_data) - $old_CONT_length; - - $PROPchunk = "\x00\x00"; // object version - $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['max_bit_rate'], 4); - $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['avg_bit_rate'], 4); - $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['max_packet_size'], 4); - $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['avg_packet_size'], 4); - $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['num_packets'], 4); - $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['duration'], 4); - $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['preroll'], 4); - $PROPchunk .= getid3_lib::BigEndian2String(max(0, $old_INDX_offset + $CONTdelta), 4); - $PROPchunk .= getid3_lib::BigEndian2String(max(0, $old_DATA_offset + $CONTdelta), 4); - $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['num_streams'], 2); - $PROPchunk .= getid3_lib::BigEndian2String($chunks[$chunkNameKeys['PROP']]['flags_raw'], 2); - - $PROPchunk = 'PROP'.getid3_lib::BigEndian2String(strlen($PROPchunk) + 8, 4).$PROPchunk; // PROP chunk identifier + chunk length - return $PROPchunk; - } - - public function GenerateCONTchunk() { - foreach ($this->tag_data as $key => $value) { - // limit each value to 0xFFFF bytes - $this->tag_data[$key] = substr($value, 0, 65535); - } - - $CONTchunk = "\x00\x00"; // object version - - $CONTchunk .= getid3_lib::BigEndian2String((!empty($this->tag_data['title']) ? strlen($this->tag_data['title']) : 0), 2); - $CONTchunk .= (!empty($this->tag_data['title']) ? strlen($this->tag_data['title']) : ''); - - $CONTchunk .= getid3_lib::BigEndian2String((!empty($this->tag_data['artist']) ? strlen($this->tag_data['artist']) : 0), 2); - $CONTchunk .= (!empty($this->tag_data['artist']) ? strlen($this->tag_data['artist']) : ''); - - $CONTchunk .= getid3_lib::BigEndian2String((!empty($this->tag_data['copyright']) ? strlen($this->tag_data['copyright']) : 0), 2); - $CONTchunk .= (!empty($this->tag_data['copyright']) ? strlen($this->tag_data['copyright']) : ''); - - $CONTchunk .= getid3_lib::BigEndian2String((!empty($this->tag_data['comment']) ? strlen($this->tag_data['comment']) : 0), 2); - $CONTchunk .= (!empty($this->tag_data['comment']) ? strlen($this->tag_data['comment']) : ''); - - if ($this->paddedlength > (strlen($CONTchunk) + 8)) { - $CONTchunk .= str_repeat("\x00", $this->paddedlength - strlen($CONTchunk) - 8); - } - - $CONTchunk = 'CONT'.getid3_lib::BigEndian2String(strlen($CONTchunk) + 8, 4).$CONTchunk; // CONT chunk identifier + chunk length - - return $CONTchunk; - } - - public function RemoveReal() { - // File MUST be writeable - CHMOD(646) at least - if (is_writeable($this->filename) && is_file($this->filename) && ($fp_source = fopen($this->filename, 'r+b'))) { - - // Initialize getID3 engine - $getID3 = new getID3; - $OldThisFileInfo = $getID3->analyze($this->filename); - if (empty($OldThisFileInfo['real']['chunks']) && !empty($OldThisFileInfo['real']['old_ra_header'])) { - $this->errors[] = 'Cannot remove Real tags from old-style file format'; - fclose($fp_source); - return false; - } - - if (empty($OldThisFileInfo['real']['chunks'])) { - $this->errors[] = 'Cannot remove Real tags because cannot find DATA chunk in file'; - fclose($fp_source); - return false; - } - foreach ($OldThisFileInfo['real']['chunks'] as $chunknumber => $chunkarray) { - $oldChunkInfo[$chunkarray['name']] = $chunkarray; - } - - if (empty($oldChunkInfo['CONT'])) { - // no existing CONT chunk - fclose($fp_source); - return true; - } - - $BeforeOffset = $oldChunkInfo['CONT']['offset']; - $AfterOffset = $oldChunkInfo['CONT']['offset'] + $oldChunkInfo['CONT']['length']; - if ($tempfilename = tempnam(GETID3_TEMP_DIR, 'getID3')) { - if (is_writable($tempfilename) && is_file($tempfilename) && ($fp_temp = fopen($tempfilename, 'wb'))) { - - rewind($fp_source); - fwrite($fp_temp, fread($fp_source, $BeforeOffset)); - fseek($fp_source, $AfterOffset, SEEK_SET); - while ($buffer = fread($fp_source, $this->fread_buffer_size)) { - fwrite($fp_temp, $buffer, strlen($buffer)); - } - fclose($fp_temp); - - if (copy($tempfilename, $this->filename)) { - unlink($tempfilename); - fclose($fp_source); - return true; - } - unlink($tempfilename); - $this->errors[] = 'FAILED: copy('.$tempfilename.', '.$this->filename.')'; - - } else { - $this->errors[] = 'Could not fopen("'.$tempfilename.'", "wb")'; - } - } - fclose($fp_source); - return false; - } - $this->errors[] = 'Could not fopen("'.$this->filename.'", "r+b")'; - return false; - } - -} diff --git a/src/Classes/Vendor/getid3/write.vorbiscomment.php b/src/Classes/Vendor/getid3/write.vorbiscomment.php deleted file mode 100755 index 7fc722be8..000000000 --- a/src/Classes/Vendor/getid3/write.vorbiscomment.php +++ /dev/null @@ -1,119 +0,0 @@ - // -// available at http://getid3.sourceforge.net // -// or http://www.getid3.org // -///////////////////////////////////////////////////////////////// -// See readme.txt for more details // -///////////////////////////////////////////////////////////////// -// // -// write.vorbiscomment.php // -// module for writing VorbisComment tags // -// dependencies: /helperapps/vorbiscomment.exe // -// /// -///////////////////////////////////////////////////////////////// - - -class getid3_write_vorbiscomment -{ - - public $filename; - public $tag_data; - public $warnings = array(); // any non-critical errors will be stored here - public $errors = array(); // any critical errors will be stored here - - public function getid3_write_vorbiscomment() { - return true; - } - - public function WriteVorbisComment() { - - if (preg_match('#(1|ON)#i', ini_get('safe_mode'))) { - $this->errors[] = 'PHP running in Safe Mode (backtick operator not available) - cannot call vorbiscomment, tags not written'; - return false; - } - - // Create file with new comments - $tempcommentsfilename = tempnam(GETID3_TEMP_DIR, 'getID3'); - if (is_writable($tempcommentsfilename) && is_file($tempcommentsfilename) && ($fpcomments = fopen($tempcommentsfilename, 'wb'))) { - - foreach ($this->tag_data as $key => $value) { - foreach ($value as $commentdata) { - fwrite($fpcomments, $this->CleanVorbisCommentName($key).'='.$commentdata."\n"); - } - } - fclose($fpcomments); - - } else { - $this->errors[] = 'failed to open temporary tags file "'.$tempcommentsfilename.'", tags not written'; - return false; - } - - $oldignoreuserabort = ignore_user_abort(true); - if (GETID3_OS_ISWINDOWS) { - - if (file_exists(GETID3_HELPERAPPSDIR.'vorbiscomment.exe')) { - //$commandline = '"'.GETID3_HELPERAPPSDIR.'vorbiscomment.exe" -w --raw -c "'.$tempcommentsfilename.'" "'.str_replace('/', '\\', $this->filename).'"'; - // vorbiscomment works fine if you copy-paste the above commandline into a command prompt, - // but refuses to work with `backtick` if there are "doublequotes" present around BOTH - // the metaflac pathname and the target filename. For whatever reason...?? - // The solution is simply ensure that the metaflac pathname has no spaces, - // and therefore does not need to be quoted - - // On top of that, if error messages are not always captured properly under Windows - // To at least see if there was a problem, compare file modification timestamps before and after writing - clearstatcache(); - $timestampbeforewriting = filemtime($this->filename); - - $commandline = GETID3_HELPERAPPSDIR.'vorbiscomment.exe -w --raw -c "'.$tempcommentsfilename.'" "'.$this->filename.'" 2>&1'; - $VorbiscommentError = `$commandline`; - - if (empty($VorbiscommentError)) { - clearstatcache(); - if ($timestampbeforewriting == filemtime($this->filename)) { - $VorbiscommentError = 'File modification timestamp has not changed - it looks like the tags were not written'; - } - } - } else { - $VorbiscommentError = 'vorbiscomment.exe not found in '.GETID3_HELPERAPPSDIR; - } - - } else { - - $commandline = 'vorbiscomment -w --raw -c "'.$tempcommentsfilename.'" "'.$this->filename.'" 2>&1'; - $VorbiscommentError = `$commandline`; - - } - - // Remove temporary comments file - unlink($tempcommentsfilename); - ignore_user_abort($oldignoreuserabort); - - if (!empty($VorbiscommentError)) { - - $this->errors[] = 'system call to vorbiscomment failed with message: '."\n\n".$VorbiscommentError; - return false; - - } - - return true; - } - - public function DeleteVorbisComment() { - $this->tag_data = array(array()); - return $this->WriteVorbisComment(); - } - - public function CleanVorbisCommentName($originalcommentname) { - // A case-insensitive field name that may consist of ASCII 0x20 through 0x7D, 0x3D ('=') excluded. - // ASCII 0x41 through 0x5A inclusive (A-Z) is to be considered equivalent to ASCII 0x61 through - // 0x7A inclusive (a-z). - - // replace invalid chars with a space, return uppercase text - // Thanks Chris Bolt for improving this function - // note: *reg_replace() replaces nulls with empty string (not space) - return strtoupper(preg_replace('#[^ -<>-}]#', ' ', str_replace("\x00", ' ', $originalcommentname))); - - } - -} diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier.auto.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier.auto.php deleted file mode 100644 index 1960c399f..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier.auto.php +++ /dev/null @@ -1,11 +0,0 @@ -purify($html, $config); -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier.includes.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier.includes.php deleted file mode 100644 index 18cb00130..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier.includes.php +++ /dev/null @@ -1,222 +0,0 @@ - $attributes) { - $allowed_elements[$element] = true; - foreach ($attributes as $attribute => $x) { - $allowed_attributes["$element.$attribute"] = true; - } - } - $config->set('HTML.AllowedElements', $allowed_elements); - $config->set('HTML.AllowedAttributes', $allowed_attributes); - $allowed_schemes = array(); - if ($allowed_protocols !== null) { - $config->set('URI.AllowedSchemes', $allowed_protocols); - } - $purifier = new HTMLPurifier($config); - return $purifier->purify($string); -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier.path.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier.path.php deleted file mode 100644 index 39b1b6531..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier.path.php +++ /dev/null @@ -1,11 +0,0 @@ -config = HTMLPurifier_Config::create($config); - - $this->strategy = new HTMLPurifier_Strategy_Core(); - - } - - /** - * Adds a filter to process the output. First come first serve - * @param $filter HTMLPurifier_Filter object - */ - public function addFilter($filter) { - trigger_error('HTMLPurifier->addFilter() is deprecated, use configuration directives in the Filter namespace or Filter.Custom', E_USER_WARNING); - $this->filters[] = $filter; - } - - /** - * Filters an HTML snippet/document to be XSS-free and standards-compliant. - * - * @param $html String of HTML to purify - * @param $config HTMLPurifier_Config object for this operation, if omitted, - * defaults to the config object specified during this - * object's construction. The parameter can also be any type - * that HTMLPurifier_Config::create() supports. - * @return Purified HTML - */ - public function purify($html, $config = null) { - - // :TODO: make the config merge in, instead of replace - $config = $config ? HTMLPurifier_Config::create($config) : $this->config; - - // implementation is partially environment dependant, partially - // configuration dependant - $lexer = HTMLPurifier_Lexer::create($config); - - $context = new HTMLPurifier_Context(); - - // setup HTML generator - $this->generator = new HTMLPurifier_Generator($config, $context); - $context->register('Generator', $this->generator); - - // set up global context variables - if ($config->get('Core.CollectErrors')) { - // may get moved out if other facilities use it - $language_factory = HTMLPurifier_LanguageFactory::instance(); - $language = $language_factory->create($config, $context); - $context->register('Locale', $language); - - $error_collector = new HTMLPurifier_ErrorCollector($context); - $context->register('ErrorCollector', $error_collector); - } - - // setup id_accumulator context, necessary due to the fact that - // AttrValidator can be called from many places - $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context); - $context->register('IDAccumulator', $id_accumulator); - - $html = HTMLPurifier_Encoder::convertToUTF8($html, $config, $context); - - // setup filters - $filter_flags = $config->getBatch('Filter'); - $custom_filters = $filter_flags['Custom']; - unset($filter_flags['Custom']); - $filters = array(); - foreach ($filter_flags as $filter => $flag) { - if (!$flag) continue; - if (strpos($filter, '.') !== false) continue; - $class = "HTMLPurifier_Filter_$filter"; - $filters[] = new $class; - } - foreach ($custom_filters as $filter) { - // maybe "HTMLPurifier_Filter_$filter", but be consistent with AutoFormat - $filters[] = $filter; - } - $filters = array_merge($filters, $this->filters); - // maybe prepare(), but later - - for ($i = 0, $filter_size = count($filters); $i < $filter_size; $i++) { - $html = $filters[$i]->preFilter($html, $config, $context); - } - - // purified HTML - $html = - $this->generator->generateFromTokens( - // list of tokens - $this->strategy->execute( - // list of un-purified tokens - $lexer->tokenizeHTML( - // un-purified HTML - $html, $config, $context - ), - $config, $context - ) - ); - - for ($i = $filter_size - 1; $i >= 0; $i--) { - $html = $filters[$i]->postFilter($html, $config, $context); - } - - $html = HTMLPurifier_Encoder::convertFromUTF8($html, $config, $context); - $this->context =& $context; - return $html; - } - - /** - * Filters an array of HTML snippets - * @param $config Optional HTMLPurifier_Config object for this operation. - * See HTMLPurifier::purify() for more details. - * @return Array of purified HTML - */ - public function purifyArray($array_of_html, $config = null) { - $context_array = array(); - foreach ($array_of_html as $key => $html) { - $array_of_html[$key] = $this->purify($html, $config); - $context_array[$key] = $this->context; - } - $this->context = $context_array; - return $array_of_html; - } - - /** - * Singleton for enforcing just one HTML Purifier in your system - * @param $prototype Optional prototype HTMLPurifier instance to - * overload singleton with, or HTMLPurifier_Config - * instance to configure the generated version with. - */ - public static function instance($prototype = null) { - if (!self::$instance || $prototype) { - if ($prototype instanceof HTMLPurifier) { - self::$instance = $prototype; - } elseif ($prototype) { - self::$instance = new HTMLPurifier($prototype); - } else { - self::$instance = new HTMLPurifier(); - } - } - return self::$instance; - } - - /** - * @note Backwards compatibility, see instance() - */ - public static function getInstance($prototype = null) { - return HTMLPurifier::instance($prototype); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier.safe-includes.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier.safe-includes.php deleted file mode 100644 index e23a81a71..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier.safe-includes.php +++ /dev/null @@ -1,216 +0,0 @@ -attr_collections as $coll_i => $coll) { - if (!isset($this->info[$coll_i])) { - $this->info[$coll_i] = array(); - } - foreach ($coll as $attr_i => $attr) { - if ($attr_i === 0 && isset($this->info[$coll_i][$attr_i])) { - // merge in includes - $this->info[$coll_i][$attr_i] = array_merge( - $this->info[$coll_i][$attr_i], $attr); - continue; - } - $this->info[$coll_i][$attr_i] = $attr; - } - } - } - // perform internal expansions and inclusions - foreach ($this->info as $name => $attr) { - // merge attribute collections that include others - $this->performInclusions($this->info[$name]); - // replace string identifiers with actual attribute objects - $this->expandIdentifiers($this->info[$name], $attr_types); - } - } - - /** - * Takes a reference to an attribute associative array and performs - * all inclusions specified by the zero index. - * @param &$attr Reference to attribute array - */ - public function performInclusions(&$attr) { - if (!isset($attr[0])) return; - $merge = $attr[0]; - $seen = array(); // recursion guard - // loop through all the inclusions - for ($i = 0; isset($merge[$i]); $i++) { - if (isset($seen[$merge[$i]])) continue; - $seen[$merge[$i]] = true; - // foreach attribute of the inclusion, copy it over - if (!isset($this->info[$merge[$i]])) continue; - foreach ($this->info[$merge[$i]] as $key => $value) { - if (isset($attr[$key])) continue; // also catches more inclusions - $attr[$key] = $value; - } - if (isset($this->info[$merge[$i]][0])) { - // recursion - $merge = array_merge($merge, $this->info[$merge[$i]][0]); - } - } - unset($attr[0]); - } - - /** - * Expands all string identifiers in an attribute array by replacing - * them with the appropriate values inside HTMLPurifier_AttrTypes - * @param &$attr Reference to attribute array - * @param $attr_types HTMLPurifier_AttrTypes instance - */ - public function expandIdentifiers(&$attr, $attr_types) { - - // because foreach will process new elements we add, make sure we - // skip duplicates - $processed = array(); - - foreach ($attr as $def_i => $def) { - // skip inclusions - if ($def_i === 0) continue; - - if (isset($processed[$def_i])) continue; - - // determine whether or not attribute is required - if ($required = (strpos($def_i, '*') !== false)) { - // rename the definition - unset($attr[$def_i]); - $def_i = trim($def_i, '*'); - $attr[$def_i] = $def; - } - - $processed[$def_i] = true; - - // if we've already got a literal object, move on - if (is_object($def)) { - // preserve previous required - $attr[$def_i]->required = ($required || $attr[$def_i]->required); - continue; - } - - if ($def === false) { - unset($attr[$def_i]); - continue; - } - - if ($t = $attr_types->get($def)) { - $attr[$def_i] = $t; - $attr[$def_i]->required = $required; - } else { - unset($attr[$def_i]); - } - } - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef.php deleted file mode 100644 index b2e4f36c5..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef.php +++ /dev/null @@ -1,123 +0,0 @@ - by removing - * leading and trailing whitespace, ignoring line feeds, and replacing - * carriage returns and tabs with spaces. While most useful for HTML - * attributes specified as CDATA, it can also be applied to most CSS - * values. - * - * @note This method is not entirely standards compliant, as trim() removes - * more types of whitespace than specified in the spec. In practice, - * this is rarely a problem, as those extra characters usually have - * already been removed by HTMLPurifier_Encoder. - * - * @warning This processing is inconsistent with XML's whitespace handling - * as specified by section 3.3.3 and referenced XHTML 1.0 section - * 4.7. However, note that we are NOT necessarily - * parsing XML, thus, this behavior may still be correct. We - * assume that newlines have been normalized. - */ - public function parseCDATA($string) { - $string = trim($string); - $string = str_replace(array("\n", "\t", "\r"), ' ', $string); - return $string; - } - - /** - * Factory method for creating this class from a string. - * @param $string String construction info - * @return Created AttrDef object corresponding to $string - */ - public function make($string) { - // default implementation, return a flyweight of this object. - // If $string has an effect on the returned object (i.e. you - // need to overload this method), it is best - // to clone or instantiate new copies. (Instantiation is safer.) - return $this; - } - - /** - * Removes spaces from rgb(0, 0, 0) so that shorthand CSS properties work - * properly. THIS IS A HACK! - */ - protected function mungeRgb($string) { - return preg_replace('/rgb\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)\)/', 'rgb(\1,\2,\3)', $string); - } - - /** - * Parses a possibly escaped CSS string and returns the "pure" - * version of it. - */ - protected function expandCSSEscape($string) { - // flexibly parse it - $ret = ''; - for ($i = 0, $c = strlen($string); $i < $c; $i++) { - if ($string[$i] === '\\') { - $i++; - if ($i >= $c) { - $ret .= '\\'; - break; - } - if (ctype_xdigit($string[$i])) { - $code = $string[$i]; - for ($a = 1, $i++; $i < $c && $a < 6; $i++, $a++) { - if (!ctype_xdigit($string[$i])) break; - $code .= $string[$i]; - } - // We have to be extremely careful when adding - // new characters, to make sure we're not breaking - // the encoding. - $char = HTMLPurifier_Encoder::unichr(hexdec($code)); - if (HTMLPurifier_Encoder::cleanUTF8($char) === '') continue; - $ret .= $char; - if ($i < $c && trim($string[$i]) !== '') $i--; - continue; - } - if ($string[$i] === "\n") continue; - } - $ret .= $string[$i]; - } - return $ret; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS.php deleted file mode 100644 index 953e70675..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS.php +++ /dev/null @@ -1,87 +0,0 @@ -parseCDATA($css); - - $definition = $config->getCSSDefinition(); - - // we're going to break the spec and explode by semicolons. - // This is because semicolon rarely appears in escaped form - // Doing this is generally flaky but fast - // IT MIGHT APPEAR IN URIs, see HTMLPurifier_AttrDef_CSSURI - // for details - - $declarations = explode(';', $css); - $propvalues = array(); - - /** - * Name of the current CSS property being validated. - */ - $property = false; - $context->register('CurrentCSSProperty', $property); - - foreach ($declarations as $declaration) { - if (!$declaration) continue; - if (!strpos($declaration, ':')) continue; - list($property, $value) = explode(':', $declaration, 2); - $property = trim($property); - $value = trim($value); - $ok = false; - do { - if (isset($definition->info[$property])) { - $ok = true; - break; - } - if (ctype_lower($property)) break; - $property = strtolower($property); - if (isset($definition->info[$property])) { - $ok = true; - break; - } - } while(0); - if (!$ok) continue; - // inefficient call, since the validator will do this again - if (strtolower(trim($value)) !== 'inherit') { - // inherit works for everything (but only on the base property) - $result = $definition->info[$property]->validate( - $value, $config, $context ); - } else { - $result = 'inherit'; - } - if ($result === false) continue; - $propvalues[$property] = $result; - } - - $context->destroy('CurrentCSSProperty'); - - // procedure does not write the new CSS simultaneously, so it's - // slightly inefficient, but it's the only way of getting rid of - // duplicates. Perhaps config to optimize it, but not now. - - $new_declarations = ''; - foreach ($propvalues as $prop => $value) { - $new_declarations .= "$prop:$value;"; - } - - return $new_declarations ? $new_declarations : false; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/AlphaValue.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/AlphaValue.php deleted file mode 100644 index 292c040d4..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/AlphaValue.php +++ /dev/null @@ -1,21 +0,0 @@ - 1.0) $result = '1'; - return $result; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Background.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Background.php deleted file mode 100644 index e5b7438c2..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Background.php +++ /dev/null @@ -1,87 +0,0 @@ -getCSSDefinition(); - $this->info['background-color'] = $def->info['background-color']; - $this->info['background-image'] = $def->info['background-image']; - $this->info['background-repeat'] = $def->info['background-repeat']; - $this->info['background-attachment'] = $def->info['background-attachment']; - $this->info['background-position'] = $def->info['background-position']; - } - - public function validate($string, $config, $context) { - - // regular pre-processing - $string = $this->parseCDATA($string); - if ($string === '') return false; - - // munge rgb() decl if necessary - $string = $this->mungeRgb($string); - - // assumes URI doesn't have spaces in it - $bits = explode(' ', $string); // bits to process - - $caught = array(); - $caught['color'] = false; - $caught['image'] = false; - $caught['repeat'] = false; - $caught['attachment'] = false; - $caught['position'] = false; - - $i = 0; // number of catches - $none = false; - - foreach ($bits as $bit) { - if ($bit === '') continue; - foreach ($caught as $key => $status) { - if ($key != 'position') { - if ($status !== false) continue; - $r = $this->info['background-' . $key]->validate($bit, $config, $context); - } else { - $r = $bit; - } - if ($r === false) continue; - if ($key == 'position') { - if ($caught[$key] === false) $caught[$key] = ''; - $caught[$key] .= $r . ' '; - } else { - $caught[$key] = $r; - } - $i++; - break; - } - } - - if (!$i) return false; - if ($caught['position'] !== false) { - $caught['position'] = $this->info['background-position']-> - validate($caught['position'], $config, $context); - } - - $ret = array(); - foreach ($caught as $value) { - if ($value === false) continue; - $ret[] = $value; - } - - if (empty($ret)) return false; - return implode(' ', $ret); - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php deleted file mode 100644 index fae82eaec..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/BackgroundPosition.php +++ /dev/null @@ -1,133 +0,0 @@ - | | left | center | right - ] - [ - | | top | center | bottom - ]? - ] | - [ // this signifies that the vertical and horizontal adjectives - // can be arbitrarily ordered, however, there can only be two, - // one of each, or none at all - [ - left | center | right - ] || - [ - top | center | bottom - ] - ] - top, left = 0% - center, (none) = 50% - bottom, right = 100% -*/ - -/* QuirksMode says: - keyword + length/percentage must be ordered correctly, as per W3C - - Internet Explorer and Opera, however, support arbitrary ordering. We - should fix it up. - - Minor issue though, not strictly necessary. -*/ - -// control freaks may appreciate the ability to convert these to -// percentages or something, but it's not necessary - -/** - * Validates the value of background-position. - */ -class HTMLPurifier_AttrDef_CSS_BackgroundPosition extends HTMLPurifier_AttrDef -{ - - protected $length; - protected $percentage; - - public function __construct() { - $this->length = new HTMLPurifier_AttrDef_CSS_Length(); - $this->percentage = new HTMLPurifier_AttrDef_CSS_Percentage(); - } - - public function validate($string, $config, $context) { - $string = $this->parseCDATA($string); - $bits = explode(' ', $string); - - $keywords = array(); - $keywords['h'] = false; // left, right - $keywords['v'] = false; // top, bottom - $keywords['ch'] = false; // center (first word) - $keywords['cv'] = false; // center (second word) - $measures = array(); - - $i = 0; - - $lookup = array( - 'top' => 'v', - 'bottom' => 'v', - 'left' => 'h', - 'right' => 'h', - 'center' => 'c' - ); - - foreach ($bits as $bit) { - if ($bit === '') continue; - - // test for keyword - $lbit = ctype_lower($bit) ? $bit : strtolower($bit); - if (isset($lookup[$lbit])) { - $status = $lookup[$lbit]; - if ($status == 'c') { - if ($i == 0) { - $status = 'ch'; - } else { - $status = 'cv'; - } - } - $keywords[$status] = $lbit; - $i++; - } - - // test for length - $r = $this->length->validate($bit, $config, $context); - if ($r !== false) { - $measures[] = $r; - $i++; - } - - // test for percentage - $r = $this->percentage->validate($bit, $config, $context); - if ($r !== false) { - $measures[] = $r; - $i++; - } - - } - - if (!$i) return false; // no valid values were caught - - $ret = array(); - - // first keyword - if ($keywords['h']) $ret[] = $keywords['h']; - elseif ($keywords['ch']) { - $ret[] = $keywords['ch']; - $keywords['cv'] = false; // prevent re-use: center = center center - } - elseif (count($measures)) $ret[] = array_shift($measures); - - if ($keywords['v']) $ret[] = $keywords['v']; - elseif ($keywords['cv']) $ret[] = $keywords['cv']; - elseif (count($measures)) $ret[] = array_shift($measures); - - if (empty($ret)) return false; - return implode(' ', $ret); - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Border.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Border.php deleted file mode 100644 index 42a1d1b4a..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Border.php +++ /dev/null @@ -1,43 +0,0 @@ -getCSSDefinition(); - $this->info['border-width'] = $def->info['border-width']; - $this->info['border-style'] = $def->info['border-style']; - $this->info['border-top-color'] = $def->info['border-top-color']; - } - - public function validate($string, $config, $context) { - $string = $this->parseCDATA($string); - $string = $this->mungeRgb($string); - $bits = explode(' ', $string); - $done = array(); // segments we've finished - $ret = ''; // return value - foreach ($bits as $bit) { - foreach ($this->info as $propname => $validator) { - if (isset($done[$propname])) continue; - $r = $validator->validate($bit, $config, $context); - if ($r !== false) { - $ret .= $r . ' '; - $done[$propname] = true; - break; - } - } - } - return rtrim($ret); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Color.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Color.php deleted file mode 100644 index 07f95a671..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Color.php +++ /dev/null @@ -1,78 +0,0 @@ -get('Core.ColorKeywords'); - - $color = trim($color); - if ($color === '') return false; - - $lower = strtolower($color); - if (isset($colors[$lower])) return $colors[$lower]; - - if (strpos($color, 'rgb(') !== false) { - // rgb literal handling - $length = strlen($color); - if (strpos($color, ')') !== $length - 1) return false; - $triad = substr($color, 4, $length - 4 - 1); - $parts = explode(',', $triad); - if (count($parts) !== 3) return false; - $type = false; // to ensure that they're all the same type - $new_parts = array(); - foreach ($parts as $part) { - $part = trim($part); - if ($part === '') return false; - $length = strlen($part); - if ($part[$length - 1] === '%') { - // handle percents - if (!$type) { - $type = 'percentage'; - } elseif ($type !== 'percentage') { - return false; - } - $num = (float) substr($part, 0, $length - 1); - if ($num < 0) $num = 0; - if ($num > 100) $num = 100; - $new_parts[] = "$num%"; - } else { - // handle integers - if (!$type) { - $type = 'integer'; - } elseif ($type !== 'integer') { - return false; - } - $num = (int) $part; - if ($num < 0) $num = 0; - if ($num > 255) $num = 255; - $new_parts[] = (string) $num; - } - } - $new_triad = implode(',', $new_parts); - $color = "rgb($new_triad)"; - } else { - // hexadecimal handling - if ($color[0] === '#') { - $hex = substr($color, 1); - } else { - $hex = $color; - $color = '#' . $color; - } - $length = strlen($hex); - if ($length !== 3 && $length !== 6) return false; - if (!ctype_xdigit($hex)) return false; - } - - return $color; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Composite.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Composite.php deleted file mode 100644 index de1289cba..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Composite.php +++ /dev/null @@ -1,38 +0,0 @@ -defs = $defs; - } - - public function validate($string, $config, $context) { - foreach ($this->defs as $i => $def) { - $result = $this->defs[$i]->validate($string, $config, $context); - if ($result !== false) return $result; - } - return false; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php deleted file mode 100644 index 6599c5b2d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/DenyElementDecorator.php +++ /dev/null @@ -1,28 +0,0 @@ -def = $def; - $this->element = $element; - } - /** - * Checks if CurrentToken is set and equal to $this->element - */ - public function validate($string, $config, $context) { - $token = $context->get('CurrentToken', true); - if ($token && $token->name == $this->element) return false; - return $this->def->validate($string, $config, $context); - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Filter.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Filter.php deleted file mode 100644 index 147894b86..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Filter.php +++ /dev/null @@ -1,54 +0,0 @@ -intValidator = new HTMLPurifier_AttrDef_Integer(); - } - - public function validate($value, $config, $context) { - $value = $this->parseCDATA($value); - if ($value === 'none') return $value; - // if we looped this we could support multiple filters - $function_length = strcspn($value, '('); - $function = trim(substr($value, 0, $function_length)); - if ($function !== 'alpha' && - $function !== 'Alpha' && - $function !== 'progid:DXImageTransform.Microsoft.Alpha' - ) return false; - $cursor = $function_length + 1; - $parameters_length = strcspn($value, ')', $cursor); - $parameters = substr($value, $cursor, $parameters_length); - $params = explode(',', $parameters); - $ret_params = array(); - $lookup = array(); - foreach ($params as $param) { - list($key, $value) = explode('=', $param); - $key = trim($key); - $value = trim($value); - if (isset($lookup[$key])) continue; - if ($key !== 'opacity') continue; - $value = $this->intValidator->validate($value, $config, $context); - if ($value === false) continue; - $int = (int) $value; - if ($int > 100) $value = '100'; - if ($int < 0) $value = '0'; - $ret_params[] = "$key=$value"; - $lookup[$key] = true; - } - $ret_parameters = implode(',', $ret_params); - $ret_function = "$function($ret_parameters)"; - return $ret_function; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Font.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Font.php deleted file mode 100644 index 699ee0b70..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Font.php +++ /dev/null @@ -1,149 +0,0 @@ -getCSSDefinition(); - $this->info['font-style'] = $def->info['font-style']; - $this->info['font-variant'] = $def->info['font-variant']; - $this->info['font-weight'] = $def->info['font-weight']; - $this->info['font-size'] = $def->info['font-size']; - $this->info['line-height'] = $def->info['line-height']; - $this->info['font-family'] = $def->info['font-family']; - } - - public function validate($string, $config, $context) { - - static $system_fonts = array( - 'caption' => true, - 'icon' => true, - 'menu' => true, - 'message-box' => true, - 'small-caption' => true, - 'status-bar' => true - ); - - // regular pre-processing - $string = $this->parseCDATA($string); - if ($string === '') return false; - - // check if it's one of the keywords - $lowercase_string = strtolower($string); - if (isset($system_fonts[$lowercase_string])) { - return $lowercase_string; - } - - $bits = explode(' ', $string); // bits to process - $stage = 0; // this indicates what we're looking for - $caught = array(); // which stage 0 properties have we caught? - $stage_1 = array('font-style', 'font-variant', 'font-weight'); - $final = ''; // output - - for ($i = 0, $size = count($bits); $i < $size; $i++) { - if ($bits[$i] === '') continue; - switch ($stage) { - - // attempting to catch font-style, font-variant or font-weight - case 0: - foreach ($stage_1 as $validator_name) { - if (isset($caught[$validator_name])) continue; - $r = $this->info[$validator_name]->validate( - $bits[$i], $config, $context); - if ($r !== false) { - $final .= $r . ' '; - $caught[$validator_name] = true; - break; - } - } - // all three caught, continue on - if (count($caught) >= 3) $stage = 1; - if ($r !== false) break; - - // attempting to catch font-size and perhaps line-height - case 1: - $found_slash = false; - if (strpos($bits[$i], '/') !== false) { - list($font_size, $line_height) = - explode('/', $bits[$i]); - if ($line_height === '') { - // ooh, there's a space after the slash! - $line_height = false; - $found_slash = true; - } - } else { - $font_size = $bits[$i]; - $line_height = false; - } - $r = $this->info['font-size']->validate( - $font_size, $config, $context); - if ($r !== false) { - $final .= $r; - // attempt to catch line-height - if ($line_height === false) { - // we need to scroll forward - for ($j = $i + 1; $j < $size; $j++) { - if ($bits[$j] === '') continue; - if ($bits[$j] === '/') { - if ($found_slash) { - return false; - } else { - $found_slash = true; - continue; - } - } - $line_height = $bits[$j]; - break; - } - } else { - // slash already found - $found_slash = true; - $j = $i; - } - if ($found_slash) { - $i = $j; - $r = $this->info['line-height']->validate( - $line_height, $config, $context); - if ($r !== false) { - $final .= '/' . $r; - } - } - $final .= ' '; - $stage = 2; - break; - } - return false; - - // attempting to catch font-family - case 2: - $font_family = - implode(' ', array_slice($bits, $i, $size - $i)); - $r = $this->info['font-family']->validate( - $font_family, $config, $context); - if ($r !== false) { - $final .= $r . ' '; - // processing completed successfully - return rtrim($final); - } - return false; - } - } - return false; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/FontFamily.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/FontFamily.php deleted file mode 100644 index 98dcf820d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/FontFamily.php +++ /dev/null @@ -1,197 +0,0 @@ -mask = '_- '; - for ($c = 'a'; $c <= 'z'; $c++) $this->mask .= $c; - for ($c = 'A'; $c <= 'Z'; $c++) $this->mask .= $c; - for ($c = '0'; $c <= '9'; $c++) $this->mask .= $c; // cast-y, but should be fine - // special bytes used by UTF-8 - for ($i = 0x80; $i <= 0xFF; $i++) { - // We don't bother excluding invalid bytes in this range, - // because the our restriction of well-formed UTF-8 will - // prevent these from ever occurring. - $this->mask .= chr($i); - } - - /* - PHP's internal strcspn implementation is - O(length of string * length of mask), making it inefficient - for large masks. However, it's still faster than - preg_match 8) - for (p = s1;;) { - spanp = s2; - do { - if (*spanp == c || p == s1_end) { - return p - s1; - } - } while (spanp++ < (s2_end - 1)); - c = *++p; - } - */ - // possible optimization: invert the mask. - } - - public function validate($string, $config, $context) { - static $generic_names = array( - 'serif' => true, - 'sans-serif' => true, - 'monospace' => true, - 'fantasy' => true, - 'cursive' => true - ); - $allowed_fonts = $config->get('CSS.AllowedFonts'); - - // assume that no font names contain commas in them - $fonts = explode(',', $string); - $final = ''; - foreach($fonts as $font) { - $font = trim($font); - if ($font === '') continue; - // match a generic name - if (isset($generic_names[$font])) { - if ($allowed_fonts === null || isset($allowed_fonts[$font])) { - $final .= $font . ', '; - } - continue; - } - // match a quoted name - if ($font[0] === '"' || $font[0] === "'") { - $length = strlen($font); - if ($length <= 2) continue; - $quote = $font[0]; - if ($font[$length - 1] !== $quote) continue; - $font = substr($font, 1, $length - 2); - } - - $font = $this->expandCSSEscape($font); - - // $font is a pure representation of the font name - - if ($allowed_fonts !== null && !isset($allowed_fonts[$font])) { - continue; - } - - if (ctype_alnum($font) && $font !== '') { - // very simple font, allow it in unharmed - $final .= $font . ', '; - continue; - } - - // bugger out on whitespace. form feed (0C) really - // shouldn't show up regardless - $font = str_replace(array("\n", "\t", "\r", "\x0C"), ' ', $font); - - // Here, there are various classes of characters which need - // to be treated differently: - // - Alphanumeric characters are essentially safe. We - // handled these above. - // - Spaces require quoting, though most parsers will do - // the right thing if there aren't any characters that - // can be misinterpreted - // - Dashes rarely occur, but they fairly unproblematic - // for parsing/rendering purposes. - // The above characters cover the majority of Western font - // names. - // - Arbitrary Unicode characters not in ASCII. Because - // most parsers give little thought to Unicode, treatment - // of these codepoints is basically uniform, even for - // punctuation-like codepoints. These characters can - // show up in non-Western pages and are supported by most - // major browsers, for example: "ï¼­ï¼³ 明æœ" is a - // legitimate font-name - // . See - // the CSS3 spec for more examples: - // - // You can see live samples of these on the Internet: - // - // However, most of these fonts have ASCII equivalents: - // for example, 'MS Mincho', and it's considered - // professional to use ASCII font names instead of - // Unicode font names. Thanks Takeshi Terada for - // providing this information. - // The following characters, to my knowledge, have not been - // used to name font names. - // - Single quote. While theoretically you might find a - // font name that has a single quote in its name (serving - // as an apostrophe, e.g. Dave's Scribble), I haven't - // been able to find any actual examples of this. - // Internet Explorer's cssText translation (which I - // believe is invoked by innerHTML) normalizes any - // quoting to single quotes, and fails to escape single - // quotes. (Note that this is not IE's behavior for all - // CSS properties, just some sort of special casing for - // font-family). So a single quote *cannot* be used - // safely in the font-family context if there will be an - // innerHTML/cssText translation. Note that Firefox 3.x - // does this too. - // - Double quote. In IE, these get normalized to - // single-quotes, no matter what the encoding. (Fun - // fact, in IE8, the 'content' CSS property gained - // support, where they special cased to preserve encoded - // double quotes, but still translate unadorned double - // quotes into single quotes.) So, because their - // fixpoint behavior is identical to single quotes, they - // cannot be allowed either. Firefox 3.x displays - // single-quote style behavior. - // - Backslashes are reduced by one (so \\ -> \) every - // iteration, so they cannot be used safely. This shows - // up in IE7, IE8 and FF3 - // - Semicolons, commas and backticks are handled properly. - // - The rest of the ASCII punctuation is handled properly. - // We haven't checked what browsers do to unadorned - // versions, but this is not important as long as the - // browser doesn't /remove/ surrounding quotes (as IE does - // for HTML). - // - // With these results in hand, we conclude that there are - // various levels of safety: - // - Paranoid: alphanumeric, spaces and dashes(?) - // - International: Paranoid + non-ASCII Unicode - // - Edgy: Everything except quotes, backslashes - // - NoJS: Standards compliance, e.g. sod IE. Note that - // with some judicious character escaping (since certain - // types of escaping doesn't work) this is theoretically - // OK as long as innerHTML/cssText is not called. - // We believe that international is a reasonable default - // (that we will implement now), and once we do more - // extensive research, we may feel comfortable with dropping - // it down to edgy. - - // Edgy: alphanumeric, spaces, dashes, underscores and Unicode. Use of - // str(c)spn assumes that the string was already well formed - // Unicode (which of course it is). - if (strspn($font, $this->mask) !== strlen($font)) { - continue; - } - - // Historical: - // In the absence of innerHTML/cssText, these ugly - // transforms don't pose a security risk (as \\ and \" - // might--these escapes are not supported by most browsers). - // We could try to be clever and use single-quote wrapping - // when there is a double quote present, but I have choosen - // not to implement that. (NOTE: you can reduce the amount - // of escapes by one depending on what quoting style you use) - // $font = str_replace('\\', '\\5C ', $font); - // $font = str_replace('"', '\\22 ', $font); - // $font = str_replace("'", '\\27 ', $font); - - // font possibly with spaces, requires quoting - $final .= "'$font', "; - } - $final = rtrim($final, ', '); - if ($final === '') return false; - return $final; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Ident.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Ident.php deleted file mode 100644 index 779794a0b..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Ident.php +++ /dev/null @@ -1,24 +0,0 @@ -def = $def; - $this->allow = $allow; - } - /** - * Intercepts and removes !important if necessary - */ - public function validate($string, $config, $context) { - // test for ! and important tokens - $string = trim($string); - $is_important = false; - // :TODO: optimization: test directly for !important and ! important - if (strlen($string) >= 9 && substr($string, -9) === 'important') { - $temp = rtrim(substr($string, 0, -9)); - // use a temp, because we might want to restore important - if (strlen($temp) >= 1 && substr($temp, -1) === '!') { - $string = rtrim(substr($temp, 0, -1)); - $is_important = true; - } - } - $string = $this->def->validate($string, $config, $context); - if ($this->allow && $is_important) $string .= ' !important'; - return $string; - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Length.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Length.php deleted file mode 100644 index a07ec5813..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Length.php +++ /dev/null @@ -1,47 +0,0 @@ -min = $min !== null ? HTMLPurifier_Length::make($min) : null; - $this->max = $max !== null ? HTMLPurifier_Length::make($max) : null; - } - - public function validate($string, $config, $context) { - $string = $this->parseCDATA($string); - - // Optimizations - if ($string === '') return false; - if ($string === '0') return '0'; - if (strlen($string) === 1) return false; - - $length = HTMLPurifier_Length::make($string); - if (!$length->isValid()) return false; - - if ($this->min) { - $c = $length->compareTo($this->min); - if ($c === false) return false; - if ($c < 0) return false; - } - if ($this->max) { - $c = $length->compareTo($this->max); - if ($c === false) return false; - if ($c > 0) return false; - } - - return $length->toString(); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/ListStyle.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/ListStyle.php deleted file mode 100644 index 4406868c0..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/ListStyle.php +++ /dev/null @@ -1,78 +0,0 @@ -getCSSDefinition(); - $this->info['list-style-type'] = $def->info['list-style-type']; - $this->info['list-style-position'] = $def->info['list-style-position']; - $this->info['list-style-image'] = $def->info['list-style-image']; - } - - public function validate($string, $config, $context) { - - // regular pre-processing - $string = $this->parseCDATA($string); - if ($string === '') return false; - - // assumes URI doesn't have spaces in it - $bits = explode(' ', strtolower($string)); // bits to process - - $caught = array(); - $caught['type'] = false; - $caught['position'] = false; - $caught['image'] = false; - - $i = 0; // number of catches - $none = false; - - foreach ($bits as $bit) { - if ($i >= 3) return; // optimization bit - if ($bit === '') continue; - foreach ($caught as $key => $status) { - if ($status !== false) continue; - $r = $this->info['list-style-' . $key]->validate($bit, $config, $context); - if ($r === false) continue; - if ($r === 'none') { - if ($none) continue; - else $none = true; - if ($key == 'image') continue; - } - $caught[$key] = $r; - $i++; - break; - } - } - - if (!$i) return false; - - $ret = array(); - - // construct type - if ($caught['type']) $ret[] = $caught['type']; - - // construct image - if ($caught['image']) $ret[] = $caught['image']; - - // construct position - if ($caught['position']) $ret[] = $caught['position']; - - if (empty($ret)) return false; - return implode(' ', $ret); - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Multiple.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Multiple.php deleted file mode 100644 index 4d62a40d7..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Multiple.php +++ /dev/null @@ -1,58 +0,0 @@ -single = $single; - $this->max = $max; - } - - public function validate($string, $config, $context) { - $string = $this->parseCDATA($string); - if ($string === '') return false; - $parts = explode(' ', $string); // parseCDATA replaced \r, \t and \n - $length = count($parts); - $final = ''; - for ($i = 0, $num = 0; $i < $length && $num < $this->max; $i++) { - if (ctype_space($parts[$i])) continue; - $result = $this->single->validate($parts[$i], $config, $context); - if ($result !== false) { - $final .= $result . ' '; - $num++; - } - } - if ($final === '') return false; - return rtrim($final); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Number.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Number.php deleted file mode 100644 index 3f99e12ec..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Number.php +++ /dev/null @@ -1,69 +0,0 @@ -non_negative = $non_negative; - } - - /** - * @warning Some contexts do not pass $config, $context. These - * variables should not be used without checking HTMLPurifier_Length - */ - public function validate($number, $config, $context) { - - $number = $this->parseCDATA($number); - - if ($number === '') return false; - if ($number === '0') return '0'; - - $sign = ''; - switch ($number[0]) { - case '-': - if ($this->non_negative) return false; - $sign = '-'; - case '+': - $number = substr($number, 1); - } - - if (ctype_digit($number)) { - $number = ltrim($number, '0'); - return $number ? $sign . $number : '0'; - } - - // Period is the only non-numeric character allowed - if (strpos($number, '.') === false) return false; - - list($left, $right) = explode('.', $number, 2); - - if ($left === '' && $right === '') return false; - if ($left !== '' && !ctype_digit($left)) return false; - - $left = ltrim($left, '0'); - $right = rtrim($right, '0'); - - if ($right === '') { - return $left ? $sign . $left : '0'; - } elseif (!ctype_digit($right)) { - return false; - } - - return $sign . $left . '.' . $right; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Percentage.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Percentage.php deleted file mode 100644 index c34b8fc3c..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/Percentage.php +++ /dev/null @@ -1,40 +0,0 @@ -number_def = new HTMLPurifier_AttrDef_CSS_Number($non_negative); - } - - public function validate($string, $config, $context) { - - $string = $this->parseCDATA($string); - - if ($string === '') return false; - $length = strlen($string); - if ($length === 1) return false; - if ($string[$length - 1] !== '%') return false; - - $number = substr($string, 0, $length - 1); - $number = $this->number_def->validate($number, $config, $context); - - if ($number === false) return false; - return "$number%"; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/TextDecoration.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/TextDecoration.php deleted file mode 100644 index 772c922d8..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/TextDecoration.php +++ /dev/null @@ -1,38 +0,0 @@ - true, - 'overline' => true, - 'underline' => true, - ); - - $string = strtolower($this->parseCDATA($string)); - - if ($string === 'none') return $string; - - $parts = explode(' ', $string); - $final = ''; - foreach ($parts as $part) { - if (isset($allowed_values[$part])) { - $final .= $part . ' '; - } - } - $final = rtrim($final); - if ($final === '') return false; - return $final; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/URI.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/URI.php deleted file mode 100644 index c2f767e57..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/CSS/URI.php +++ /dev/null @@ -1,61 +0,0 @@ -parseCDATA($uri_string); - if (strpos($uri_string, 'url(') !== 0) return false; - $uri_string = substr($uri_string, 4); - $new_length = strlen($uri_string) - 1; - if ($uri_string[$new_length] != ')') return false; - $uri = trim(substr($uri_string, 0, $new_length)); - - if (!empty($uri) && ($uri[0] == "'" || $uri[0] == '"')) { - $quote = $uri[0]; - $new_length = strlen($uri) - 1; - if ($uri[$new_length] !== $quote) return false; - $uri = substr($uri, 1, $new_length - 1); - } - - $uri = $this->expandCSSEscape($uri); - - $result = parent::validate($uri, $config, $context); - - if ($result === false) return false; - - // extra sanity check; should have been done by URI - $result = str_replace(array('"', "\\", "\n", "\x0c", "\r"), "", $result); - - // suspicious characters are ()'; we're going to percent encode - // them for safety. - $result = str_replace(array('(', ')', "'"), array('%28', '%29', '%27'), $result); - - // there's an extra bug where ampersands lose their escaping on - // an innerHTML cycle, so a very unlucky query parameter could - // then change the meaning of the URL. Unfortunately, there's - // not much we can do about that... - - return "url(\"$result\")"; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Clone.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Clone.php deleted file mode 100644 index ce68dbd54..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Clone.php +++ /dev/null @@ -1,28 +0,0 @@ -clone = $clone; - } - - public function validate($v, $config, $context) { - return $this->clone->validate($v, $config, $context); - } - - public function make($string) { - return clone $this->clone; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Enum.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Enum.php deleted file mode 100644 index 5d603ebcc..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Enum.php +++ /dev/null @@ -1,65 +0,0 @@ -valid_values = array_flip($valid_values); - $this->case_sensitive = $case_sensitive; - } - - public function validate($string, $config, $context) { - $string = trim($string); - if (!$this->case_sensitive) { - // we may want to do full case-insensitive libraries - $string = ctype_lower($string) ? $string : strtolower($string); - } - $result = isset($this->valid_values[$string]); - - return $result ? $string : false; - } - - /** - * @param $string In form of comma-delimited list of case-insensitive - * valid values. Example: "foo,bar,baz". Prepend "s:" to make - * case sensitive - */ - public function make($string) { - if (strlen($string) > 2 && $string[0] == 's' && $string[1] == ':') { - $string = substr($string, 2); - $sensitive = true; - } else { - $sensitive = false; - } - $values = explode(',', $string); - return new HTMLPurifier_AttrDef_Enum($values, $sensitive); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/Bool.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/Bool.php deleted file mode 100644 index e06987eb8..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/Bool.php +++ /dev/null @@ -1,28 +0,0 @@ -name = $name;} - - public function validate($string, $config, $context) { - if (empty($string)) return false; - return $this->name; - } - - /** - * @param $string Name of attribute - */ - public function make($string) { - return new HTMLPurifier_AttrDef_HTML_Bool($string); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/Class.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/Class.php deleted file mode 100644 index 370068d97..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/Class.php +++ /dev/null @@ -1,34 +0,0 @@ -getDefinition('HTML')->doctype->name; - if ($name == "XHTML 1.1" || $name == "XHTML 2.0") { - return parent::split($string, $config, $context); - } else { - return preg_split('/\s+/', $string); - } - } - protected function filter($tokens, $config, $context) { - $allowed = $config->get('Attr.AllowedClasses'); - $forbidden = $config->get('Attr.ForbiddenClasses'); - $ret = array(); - foreach ($tokens as $token) { - if ( - ($allowed === null || isset($allowed[$token])) && - !isset($forbidden[$token]) && - // We need this O(n) check because of PHP's array - // implementation that casts -0 to 0. - !in_array($token, $ret, true) - ) { - $ret[] = $token; - } - } - return $ret; - } -} diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/Color.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/Color.php deleted file mode 100644 index e02abb075..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/Color.php +++ /dev/null @@ -1,33 +0,0 @@ -get('Core.ColorKeywords'); - - $string = trim($string); - - if (empty($string)) return false; - $lower = strtolower($string); - if (isset($colors[$lower])) return $colors[$lower]; - if ($string[0] === '#') $hex = substr($string, 1); - else $hex = $string; - - $length = strlen($hex); - if ($length !== 3 && $length !== 6) return false; - if (!ctype_xdigit($hex)) return false; - if ($length === 3) $hex = $hex[0].$hex[0].$hex[1].$hex[1].$hex[2].$hex[2]; - - return "#$hex"; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/FrameTarget.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/FrameTarget.php deleted file mode 100644 index ae6ea7c01..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/FrameTarget.php +++ /dev/null @@ -1,21 +0,0 @@ -valid_values === false) $this->valid_values = $config->get('Attr.AllowedFrameTargets'); - return parent::validate($string, $config, $context); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/ID.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/ID.php deleted file mode 100644 index 0015fa1eb..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/ID.php +++ /dev/null @@ -1,80 +0,0 @@ -selector = $selector; - } - - public function validate($id, $config, $context) { - - if (!$this->selector && !$config->get('Attr.EnableID')) return false; - - $id = trim($id); // trim it first - - if ($id === '') return false; - - $prefix = $config->get('Attr.IDPrefix'); - if ($prefix !== '') { - $prefix .= $config->get('Attr.IDPrefixLocal'); - // prevent re-appending the prefix - if (strpos($id, $prefix) !== 0) $id = $prefix . $id; - } elseif ($config->get('Attr.IDPrefixLocal') !== '') { - trigger_error('%Attr.IDPrefixLocal cannot be used unless '. - '%Attr.IDPrefix is set', E_USER_WARNING); - } - - if (!$this->selector) { - $id_accumulator =& $context->get('IDAccumulator'); - if (isset($id_accumulator->ids[$id])) return false; - } - - // we purposely avoid using regex, hopefully this is faster - - if (ctype_alpha($id)) { - $result = true; - } else { - if (!ctype_alpha(@$id[0])) return false; - $trim = trim( // primitive style of regexps, I suppose - $id, - 'A..Za..z0..9:-._' - ); - $result = ($trim === ''); - } - - $regexp = $config->get('Attr.IDBlacklistRegexp'); - if ($regexp && preg_match($regexp, $id)) { - return false; - } - - if (!$this->selector && $result) $id_accumulator->add($id); - - // if no change was made to the ID, return the result - // else, return the new id if stripping whitespace made it - // valid, or return false. - return $result ? $id : false; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/Length.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/Length.php deleted file mode 100644 index a242f9c23..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/Length.php +++ /dev/null @@ -1,41 +0,0 @@ - 100) return '100%'; - - return ((string) $points) . '%'; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/LinkTypes.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/LinkTypes.php deleted file mode 100644 index 76d25ed08..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/LinkTypes.php +++ /dev/null @@ -1,53 +0,0 @@ - 'AllowedRel', - 'rev' => 'AllowedRev' - ); - if (!isset($configLookup[$name])) { - trigger_error('Unrecognized attribute name for link '. - 'relationship.', E_USER_ERROR); - return; - } - $this->name = $configLookup[$name]; - } - - public function validate($string, $config, $context) { - - $allowed = $config->get('Attr.' . $this->name); - if (empty($allowed)) return false; - - $string = $this->parseCDATA($string); - $parts = explode(' ', $string); - - // lookup to prevent duplicates - $ret_lookup = array(); - foreach ($parts as $part) { - $part = strtolower(trim($part)); - if (!isset($allowed[$part])) continue; - $ret_lookup[$part] = true; - } - - if (empty($ret_lookup)) return false; - $string = implode(' ', array_keys($ret_lookup)); - - return $string; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/MultiLength.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/MultiLength.php deleted file mode 100644 index c72fc76e4..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/MultiLength.php +++ /dev/null @@ -1,41 +0,0 @@ -split($string, $config, $context); - $tokens = $this->filter($tokens, $config, $context); - if (empty($tokens)) return false; - return implode(' ', $tokens); - - } - - /** - * Splits a space separated list of tokens into its constituent parts. - */ - protected function split($string, $config, $context) { - // OPTIMIZABLE! - // do the preg_match, capture all subpatterns for reformulation - - // we don't support U+00A1 and up codepoints or - // escaping because I don't know how to do that with regexps - // and plus it would complicate optimization efforts (you never - // see that anyway). - $pattern = '/(?:(?<=\s)|\A)'. // look behind for space or string start - '((?:--|-?[A-Za-z_])[A-Za-z_\-0-9]*)'. - '(?:(?=\s)|\z)/'; // look ahead for space or string end - preg_match_all($pattern, $string, $matches); - return $matches[1]; - } - - /** - * Template method for removing certain tokens based on arbitrary criteria. - * @note If we wanted to be really functional, we'd do an array_filter - * with a callback. But... we're not. - */ - protected function filter($tokens, $config, $context) { - return $tokens; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/Pixels.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/Pixels.php deleted file mode 100644 index 4cb2c1b85..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/HTML/Pixels.php +++ /dev/null @@ -1,48 +0,0 @@ -max = $max; - } - - public function validate($string, $config, $context) { - - $string = trim($string); - if ($string === '0') return $string; - if ($string === '') return false; - $length = strlen($string); - if (substr($string, $length - 2) == 'px') { - $string = substr($string, 0, $length - 2); - } - if (!is_numeric($string)) return false; - $int = (int) $string; - - if ($int < 0) return '0'; - - // upper-bound value, extremely high values can - // crash operating systems, see - // WARNING, above link WILL crash you if you're using Windows - - if ($this->max !== null && $int > $this->max) return (string) $this->max; - - return (string) $int; - - } - - public function make($string) { - if ($string === '') $max = null; - else $max = (int) $string; - $class = get_class($this); - return new $class($max); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Integer.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Integer.php deleted file mode 100644 index d59738d2a..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Integer.php +++ /dev/null @@ -1,73 +0,0 @@ -negative = $negative; - $this->zero = $zero; - $this->positive = $positive; - } - - public function validate($integer, $config, $context) { - - $integer = $this->parseCDATA($integer); - if ($integer === '') return false; - - // we could possibly simply typecast it to integer, but there are - // certain fringe cases that must not return an integer. - - // clip leading sign - if ( $this->negative && $integer[0] === '-' ) { - $digits = substr($integer, 1); - if ($digits === '0') $integer = '0'; // rm minus sign for zero - } elseif( $this->positive && $integer[0] === '+' ) { - $digits = $integer = substr($integer, 1); // rm unnecessary plus - } else { - $digits = $integer; - } - - // test if it's numeric - if (!ctype_digit($digits)) return false; - - // perform scope tests - if (!$this->zero && $integer == 0) return false; - if (!$this->positive && $integer > 0) return false; - if (!$this->negative && $integer < 0) return false; - - return $integer; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Lang.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Lang.php deleted file mode 100644 index 10e6da56d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Lang.php +++ /dev/null @@ -1,73 +0,0 @@ - 8 || !ctype_alnum($subtags[1])) { - return $new_string; - } - if (!ctype_lower($subtags[1])) $subtags[1] = strtolower($subtags[1]); - - $new_string .= '-' . $subtags[1]; - if ($num_subtags == 2) return $new_string; - - // process all other subtags, index 2 and up - for ($i = 2; $i < $num_subtags; $i++) { - $length = strlen($subtags[$i]); - if ($length == 0 || $length > 8 || !ctype_alnum($subtags[$i])) { - return $new_string; - } - if (!ctype_lower($subtags[$i])) { - $subtags[$i] = strtolower($subtags[$i]); - } - $new_string .= '-' . $subtags[$i]; - } - - return $new_string; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Switch.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Switch.php deleted file mode 100644 index c9e3ed193..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Switch.php +++ /dev/null @@ -1,34 +0,0 @@ -tag = $tag; - $this->withTag = $with_tag; - $this->withoutTag = $without_tag; - } - - public function validate($string, $config, $context) { - $token = $context->get('CurrentToken', true); - if (!$token || $token->name !== $this->tag) { - return $this->withoutTag->validate($string, $config, $context); - } else { - return $this->withTag->validate($string, $config, $context); - } - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Text.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Text.php deleted file mode 100644 index c6216cc53..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/Text.php +++ /dev/null @@ -1,15 +0,0 @@ -parseCDATA($string); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/URI.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/URI.php deleted file mode 100644 index c2b684671..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/URI.php +++ /dev/null @@ -1,77 +0,0 @@ -parser = new HTMLPurifier_URIParser(); - $this->embedsResource = (bool) $embeds_resource; - } - - public function make($string) { - $embeds = ($string === 'embedded'); - return new HTMLPurifier_AttrDef_URI($embeds); - } - - public function validate($uri, $config, $context) { - - if ($config->get('URI.Disable')) return false; - - $uri = $this->parseCDATA($uri); - - // parse the URI - $uri = $this->parser->parse($uri); - if ($uri === false) return false; - - // add embedded flag to context for validators - $context->register('EmbeddedURI', $this->embedsResource); - - $ok = false; - do { - - // generic validation - $result = $uri->validate($config, $context); - if (!$result) break; - - // chained filtering - $uri_def = $config->getDefinition('URI'); - $result = $uri_def->filter($uri, $config, $context); - if (!$result) break; - - // scheme-specific validation - $scheme_obj = $uri->getSchemeObj($config, $context); - if (!$scheme_obj) break; - if ($this->embedsResource && !$scheme_obj->browsable) break; - $result = $scheme_obj->validate($uri, $config, $context); - if (!$result) break; - - // Post chained filtering - $result = $uri_def->postFilter($uri, $config, $context); - if (!$result) break; - - // survived gauntlet - $ok = true; - - } while (false); - - $context->destroy('EmbeddedURI'); - if (!$ok) return false; - - // back to string - return $uri->toString(); - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/URI/Email.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/URI/Email.php deleted file mode 100644 index bfee9d166..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/URI/Email.php +++ /dev/null @@ -1,17 +0,0 @@ -" - // that needs more percent encoding to be done - if ($string == '') return false; - $string = trim($string); - $result = preg_match('/^[A-Z0-9._%-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i', $string); - return $result ? $string : false; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/URI/Host.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/URI/Host.php deleted file mode 100644 index 125decb2d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/URI/Host.php +++ /dev/null @@ -1,101 +0,0 @@ -ipv4 = new HTMLPurifier_AttrDef_URI_IPv4(); - $this->ipv6 = new HTMLPurifier_AttrDef_URI_IPv6(); - } - - public function validate($string, $config, $context) { - $length = strlen($string); - // empty hostname is OK; it's usually semantically equivalent: - // the default host as defined by a URI scheme is used: - // - // If the URI scheme defines a default for host, then that - // default applies when the host subcomponent is undefined - // or when the registered name is empty (zero length). - if ($string === '') return ''; - if ($length > 1 && $string[0] === '[' && $string[$length-1] === ']') { - //IPv6 - $ip = substr($string, 1, $length - 2); - $valid = $this->ipv6->validate($ip, $config, $context); - if ($valid === false) return false; - return '['. $valid . ']'; - } - - // need to do checks on unusual encodings too - $ipv4 = $this->ipv4->validate($string, $config, $context); - if ($ipv4 !== false) return $ipv4; - - // A regular domain name. - - // This doesn't match I18N domain names, but we don't have proper IRI support, - // so force users to insert Punycode. - - // The productions describing this are: - $a = '[a-z]'; // alpha - $an = '[a-z0-9]'; // alphanum - $and = '[a-z0-9-]'; // alphanum | "-" - // domainlabel = alphanum | alphanum *( alphanum | "-" ) alphanum - $domainlabel = "$an($and*$an)?"; - // toplabel = alpha | alpha *( alphanum | "-" ) alphanum - $toplabel = "$a($and*$an)?"; - // hostname = *( domainlabel "." ) toplabel [ "." ] - if (preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string)) { - return $string; - } - - // If we have Net_IDNA2 support, we can support IRIs by - // punycoding them. (This is the most portable thing to do, - // since otherwise we have to assume browsers support - - if ($config->get('Core.EnableIDNA')) { - $idna = new Net_IDNA2(array('encoding' => 'utf8', 'overlong' => false, 'strict' => true)); - // we need to encode each period separately - $parts = explode('.', $string); - try { - $new_parts = array(); - foreach ($parts as $part) { - $encodable = false; - for ($i = 0, $c = strlen($part); $i < $c; $i++) { - if (ord($part[$i]) > 0x7a) { - $encodable = true; - break; - } - } - if (!$encodable) { - $new_parts[] = $part; - } else { - $new_parts[] = $idna->encode($part); - } - } - $string = implode('.', $new_parts); - if (preg_match("/^($domainlabel\.)*$toplabel\.?$/i", $string)) { - return $string; - } - } catch (Exception $e) { - // XXX error reporting - } - } - - return false; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/URI/IPv4.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/URI/IPv4.php deleted file mode 100644 index ec4cf591b..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/URI/IPv4.php +++ /dev/null @@ -1,39 +0,0 @@ -ip4) $this->_loadRegex(); - - if (preg_match('#^' . $this->ip4 . '$#s', $aIP)) - { - return $aIP; - } - - return false; - - } - - /** - * Lazy load function to prevent regex from being stuffed in - * cache. - */ - protected function _loadRegex() { - $oct = '(?:25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])'; // 0-255 - $this->ip4 = "(?:{$oct}\\.{$oct}\\.{$oct}\\.{$oct})"; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/URI/IPv6.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/URI/IPv6.php deleted file mode 100644 index 9454e9be5..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrDef/URI/IPv6.php +++ /dev/null @@ -1,99 +0,0 @@ -ip4) $this->_loadRegex(); - - $original = $aIP; - - $hex = '[0-9a-fA-F]'; - $blk = '(?:' . $hex . '{1,4})'; - $pre = '(?:/(?:12[0-8]|1[0-1][0-9]|[1-9][0-9]|[0-9]))'; // /0 - /128 - - // prefix check - if (strpos($aIP, '/') !== false) - { - if (preg_match('#' . $pre . '$#s', $aIP, $find)) - { - $aIP = substr($aIP, 0, 0-strlen($find[0])); - unset($find); - } - else - { - return false; - } - } - - // IPv4-compatiblity check - if (preg_match('#(?<=:'.')' . $this->ip4 . '$#s', $aIP, $find)) - { - $aIP = substr($aIP, 0, 0-strlen($find[0])); - $ip = explode('.', $find[0]); - $ip = array_map('dechex', $ip); - $aIP .= $ip[0] . $ip[1] . ':' . $ip[2] . $ip[3]; - unset($find, $ip); - } - - // compression check - $aIP = explode('::', $aIP); - $c = count($aIP); - if ($c > 2) - { - return false; - } - elseif ($c == 2) - { - list($first, $second) = $aIP; - $first = explode(':', $first); - $second = explode(':', $second); - - if (count($first) + count($second) > 8) - { - return false; - } - - while(count($first) < 8) - { - array_push($first, '0'); - } - - array_splice($first, 8 - count($second), 8, $second); - $aIP = $first; - unset($first,$second); - } - else - { - $aIP = explode(':', $aIP[0]); - } - $c = count($aIP); - - if ($c != 8) - { - return false; - } - - // All the pieces should be 16-bit hex strings. Are they? - foreach ($aIP as $piece) - { - if (!preg_match('#^[0-9a-fA-F]{4}$#s', sprintf('%04s', $piece))) - { - return false; - } - } - - return $original; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform.php deleted file mode 100644 index e61d3e01b..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform.php +++ /dev/null @@ -1,56 +0,0 @@ -confiscateAttr($attr, 'background'); - // some validation should happen here - - $this->prependCSS($attr, "background-image:url($background);"); - - return $attr; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/BdoDir.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/BdoDir.php deleted file mode 100644 index 4d1a05665..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/BdoDir.php +++ /dev/null @@ -1,19 +0,0 @@ -get('Attr.DefaultTextDir'); - return $attr; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/BgColor.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/BgColor.php deleted file mode 100644 index ad3916bb9..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/BgColor.php +++ /dev/null @@ -1,23 +0,0 @@ -confiscateAttr($attr, 'bgcolor'); - // some validation should happen here - - $this->prependCSS($attr, "background-color:$bgcolor;"); - - return $attr; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/BoolToCSS.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/BoolToCSS.php deleted file mode 100644 index 51159b671..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/BoolToCSS.php +++ /dev/null @@ -1,36 +0,0 @@ -attr = $attr; - $this->css = $css; - } - - public function transform($attr, $config, $context) { - if (!isset($attr[$this->attr])) return $attr; - unset($attr[$this->attr]); - $this->prependCSS($attr, $this->css); - return $attr; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Border.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Border.php deleted file mode 100644 index 476b0b079..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Border.php +++ /dev/null @@ -1,18 +0,0 @@ -confiscateAttr($attr, 'border'); - // some validation should happen here - $this->prependCSS($attr, "border:{$border_width}px solid;"); - return $attr; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/EnumToCSS.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/EnumToCSS.php deleted file mode 100644 index 2a5b4514a..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/EnumToCSS.php +++ /dev/null @@ -1,58 +0,0 @@ -attr = $attr; - $this->enumToCSS = $enum_to_css; - $this->caseSensitive = (bool) $case_sensitive; - } - - public function transform($attr, $config, $context) { - - if (!isset($attr[$this->attr])) return $attr; - - $value = trim($attr[$this->attr]); - unset($attr[$this->attr]); - - if (!$this->caseSensitive) $value = strtolower($value); - - if (!isset($this->enumToCSS[$value])) { - return $attr; - } - - $this->prependCSS($attr, $this->enumToCSS[$value]); - - return $attr; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/ImgRequired.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/ImgRequired.php deleted file mode 100644 index 7f0e4b7a5..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/ImgRequired.php +++ /dev/null @@ -1,43 +0,0 @@ -get('Core.RemoveInvalidImg')) return $attr; - $attr['src'] = $config->get('Attr.DefaultInvalidImage'); - $src = false; - } - - if (!isset($attr['alt'])) { - if ($src) { - $alt = $config->get('Attr.DefaultImageAlt'); - if ($alt === null) { - // truncate if the alt is too long - $attr['alt'] = substr(basename($attr['src']),0,40); - } else { - $attr['alt'] = $alt; - } - } else { - $attr['alt'] = $config->get('Attr.DefaultInvalidImageAlt'); - } - } - - return $attr; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/ImgSpace.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/ImgSpace.php deleted file mode 100644 index fd84c10c3..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/ImgSpace.php +++ /dev/null @@ -1,44 +0,0 @@ - array('left', 'right'), - 'vspace' => array('top', 'bottom') - ); - - public function __construct($attr) { - $this->attr = $attr; - if (!isset($this->css[$attr])) { - trigger_error(htmlspecialchars($attr) . ' is not valid space attribute'); - } - } - - public function transform($attr, $config, $context) { - - if (!isset($attr[$this->attr])) return $attr; - - $width = $this->confiscateAttr($attr, $this->attr); - // some validation could happen here - - if (!isset($this->css[$this->attr])) return $attr; - - $style = ''; - foreach ($this->css[$this->attr] as $suffix) { - $property = "margin-$suffix"; - $style .= "$property:{$width}px;"; - } - - $this->prependCSS($attr, $style); - - return $attr; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Input.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Input.php deleted file mode 100644 index 16829552d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Input.php +++ /dev/null @@ -1,40 +0,0 @@ -pixels = new HTMLPurifier_AttrDef_HTML_Pixels(); - } - - public function transform($attr, $config, $context) { - if (!isset($attr['type'])) $t = 'text'; - else $t = strtolower($attr['type']); - if (isset($attr['checked']) && $t !== 'radio' && $t !== 'checkbox') { - unset($attr['checked']); - } - if (isset($attr['maxlength']) && $t !== 'text' && $t !== 'password') { - unset($attr['maxlength']); - } - if (isset($attr['size']) && $t !== 'text' && $t !== 'password') { - $result = $this->pixels->validate($attr['size'], $config, $context); - if ($result === false) unset($attr['size']); - else $attr['size'] = $result; - } - if (isset($attr['src']) && $t !== 'image') { - unset($attr['src']); - } - if (!isset($attr['value']) && ($t === 'radio' || $t === 'checkbox')) { - $attr['value'] = ''; - } - return $attr; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Lang.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Lang.php deleted file mode 100644 index 5869e7f82..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Lang.php +++ /dev/null @@ -1,28 +0,0 @@ -name = $name; - $this->cssName = $css_name ? $css_name : $name; - } - - public function transform($attr, $config, $context) { - if (!isset($attr[$this->name])) return $attr; - $length = $this->confiscateAttr($attr, $this->name); - if(ctype_digit($length)) $length .= 'px'; - $this->prependCSS($attr, $this->cssName . ":$length;"); - return $attr; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Name.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Name.php deleted file mode 100644 index 15315bc73..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Name.php +++ /dev/null @@ -1,21 +0,0 @@ -get('HTML.Attr.Name.UseCDATA')) return $attr; - if (!isset($attr['name'])) return $attr; - $id = $this->confiscateAttr($attr, 'name'); - if ( isset($attr['id'])) return $attr; - $attr['id'] = $id; - return $attr; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/NameSync.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/NameSync.php deleted file mode 100644 index a95638c14..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/NameSync.php +++ /dev/null @@ -1,27 +0,0 @@ -idDef = new HTMLPurifier_AttrDef_HTML_ID(); - } - - public function transform($attr, $config, $context) { - if (!isset($attr['name'])) return $attr; - $name = $attr['name']; - if (isset($attr['id']) && $attr['id'] === $name) return $attr; - $result = $this->idDef->validate($name, $config, $context); - if ($result === false) unset($attr['name']); - else $attr['name'] = $result; - return $attr; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Nofollow.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Nofollow.php deleted file mode 100644 index e699c79a8..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Nofollow.php +++ /dev/null @@ -1,45 +0,0 @@ -parser = new HTMLPurifier_URIParser(); - } - - public function transform($attr, $config, $context) { - - if (!isset($attr['href'])) { - return $attr; - } - - // XXX Kind of inefficient - $url = $this->parser->parse($attr['href']); - $scheme = $url->getSchemeObj($config, $context); - - if ($scheme->browsable && !$url->isLocal($config, $context)) { - if (isset($attr['rel'])) { - $rels = explode(' ', $attr['rel']); - if (!in_array('nofollow', $rels)) { - $rels[] = 'nofollow'; - } - $attr['rel'] = implode(' ', $rels); - } else { - $attr['rel'] = 'nofollow'; - } - } - - return $attr; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/SafeEmbed.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/SafeEmbed.php deleted file mode 100644 index 4da449981..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/SafeEmbed.php +++ /dev/null @@ -1,15 +0,0 @@ -uri = new HTMLPurifier_AttrDef_URI(true); // embedded - $this->wmode = new HTMLPurifier_AttrDef_Enum(array('window', 'opaque', 'transparent')); - } - - public function transform($attr, $config, $context) { - // If we add support for other objects, we'll need to alter the - // transforms. - switch ($attr['name']) { - // application/x-shockwave-flash - // Keep this synchronized with Injector/SafeObject.php - case 'allowScriptAccess': - $attr['value'] = 'never'; - break; - case 'allowNetworking': - $attr['value'] = 'internal'; - break; - case 'allowFullScreen': - if ($config->get('HTML.FlashAllowFullScreen')) { - $attr['value'] = ($attr['value'] == 'true') ? 'true' : 'false'; - } else { - $attr['value'] = 'false'; - } - break; - case 'wmode': - $attr['value'] = $this->wmode->validate($attr['value'], $config, $context); - break; - case 'movie': - case 'src': - $attr['name'] = "movie"; - $attr['value'] = $this->uri->validate($attr['value'], $config, $context); - break; - case 'flashvars': - // we're going to allow arbitrary inputs to the SWF, on - // the reasoning that it could only hack the SWF, not us. - break; - // add other cases to support other param name/value pairs - default: - $attr['name'] = $attr['value'] = null; - } - return $attr; - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/ScriptRequired.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/ScriptRequired.php deleted file mode 100644 index 4499050a2..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/ScriptRequired.php +++ /dev/null @@ -1,16 +0,0 @@ - - */ -class HTMLPurifier_AttrTransform_ScriptRequired extends HTMLPurifier_AttrTransform -{ - public function transform($attr, $config, $context) { - if (!isset($attr['type'])) { - $attr['type'] = 'text/javascript'; - } - return $attr; - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/TargetBlank.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/TargetBlank.php deleted file mode 100644 index deba8b40f..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/TargetBlank.php +++ /dev/null @@ -1,38 +0,0 @@ -parser = new HTMLPurifier_URIParser(); - } - - public function transform($attr, $config, $context) { - - if (!isset($attr['href'])) { - return $attr; - } - - // XXX Kind of inefficient - $url = $this->parser->parse($attr['href']); - $scheme = $url->getSchemeObj($config, $context); - - if ($scheme->browsable && !$url->isBenign($config, $context)) { - $attr['target'] = '_blank'; - } - - return $attr; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Textarea.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Textarea.php deleted file mode 100644 index 81ac3488b..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTransform/Textarea.php +++ /dev/null @@ -1,18 +0,0 @@ - - */ -class HTMLPurifier_AttrTransform_Textarea extends HTMLPurifier_AttrTransform -{ - - public function transform($attr, $config, $context) { - // Calculated from Firefox - if (!isset($attr['cols'])) $attr['cols'] = '22'; - if (!isset($attr['rows'])) $attr['rows'] = '3'; - return $attr; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTypes.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTypes.php deleted file mode 100644 index 6f985ff93..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrTypes.php +++ /dev/null @@ -1,91 +0,0 @@ -info['Enum'] = new HTMLPurifier_AttrDef_Enum(); - $this->info['Bool'] = new HTMLPurifier_AttrDef_HTML_Bool(); - - $this->info['CDATA'] = new HTMLPurifier_AttrDef_Text(); - $this->info['ID'] = new HTMLPurifier_AttrDef_HTML_ID(); - $this->info['Length'] = new HTMLPurifier_AttrDef_HTML_Length(); - $this->info['MultiLength'] = new HTMLPurifier_AttrDef_HTML_MultiLength(); - $this->info['NMTOKENS'] = new HTMLPurifier_AttrDef_HTML_Nmtokens(); - $this->info['Pixels'] = new HTMLPurifier_AttrDef_HTML_Pixels(); - $this->info['Text'] = new HTMLPurifier_AttrDef_Text(); - $this->info['URI'] = new HTMLPurifier_AttrDef_URI(); - $this->info['LanguageCode'] = new HTMLPurifier_AttrDef_Lang(); - $this->info['Color'] = new HTMLPurifier_AttrDef_HTML_Color(); - $this->info['IAlign'] = self::makeEnum('top,middle,bottom,left,right'); - $this->info['LAlign'] = self::makeEnum('top,bottom,left,right'); - $this->info['FrameTarget'] = new HTMLPurifier_AttrDef_HTML_FrameTarget(); - - // unimplemented aliases - $this->info['ContentType'] = new HTMLPurifier_AttrDef_Text(); - $this->info['ContentTypes'] = new HTMLPurifier_AttrDef_Text(); - $this->info['Charsets'] = new HTMLPurifier_AttrDef_Text(); - $this->info['Character'] = new HTMLPurifier_AttrDef_Text(); - - // "proprietary" types - $this->info['Class'] = new HTMLPurifier_AttrDef_HTML_Class(); - - // number is really a positive integer (one or more digits) - // FIXME: ^^ not always, see start and value of list items - $this->info['Number'] = new HTMLPurifier_AttrDef_Integer(false, false, true); - } - - private static function makeEnum($in) { - return new HTMLPurifier_AttrDef_Clone(new HTMLPurifier_AttrDef_Enum(explode(',', $in))); - } - - /** - * Retrieves a type - * @param $type String type name - * @return Object AttrDef for type - */ - public function get($type) { - - // determine if there is any extra info tacked on - if (strpos($type, '#') !== false) list($type, $string) = explode('#', $type, 2); - else $string = ''; - - if (!isset($this->info[$type])) { - trigger_error('Cannot retrieve undefined attribute type ' . $type, E_USER_ERROR); - return; - } - - return $this->info[$type]->make($string); - - } - - /** - * Sets a new implementation for a type - * @param $type String type name - * @param $impl Object AttrDef for type - */ - public function set($type, $impl) { - $this->info[$type] = $impl; - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrValidator.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrValidator.php deleted file mode 100644 index 829a0f8f2..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/AttrValidator.php +++ /dev/null @@ -1,162 +0,0 @@ -getHTMLDefinition(); - $e =& $context->get('ErrorCollector', true); - - // initialize IDAccumulator if necessary - $ok =& $context->get('IDAccumulator', true); - if (!$ok) { - $id_accumulator = HTMLPurifier_IDAccumulator::build($config, $context); - $context->register('IDAccumulator', $id_accumulator); - } - - // initialize CurrentToken if necessary - $current_token =& $context->get('CurrentToken', true); - if (!$current_token) $context->register('CurrentToken', $token); - - if ( - !$token instanceof HTMLPurifier_Token_Start && - !$token instanceof HTMLPurifier_Token_Empty - ) return $token; - - // create alias to global definition array, see also $defs - // DEFINITION CALL - $d_defs = $definition->info_global_attr; - - // don't update token until the very end, to ensure an atomic update - $attr = $token->attr; - - // do global transformations (pre) - // nothing currently utilizes this - foreach ($definition->info_attr_transform_pre as $transform) { - $attr = $transform->transform($o = $attr, $config, $context); - if ($e) { - if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); - } - } - - // do local transformations only applicable to this element (pre) - // ex.

    to

    - foreach ($definition->info[$token->name]->attr_transform_pre as $transform) { - $attr = $transform->transform($o = $attr, $config, $context); - if ($e) { - if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); - } - } - - // create alias to this element's attribute definition array, see - // also $d_defs (global attribute definition array) - // DEFINITION CALL - $defs = $definition->info[$token->name]->attr; - - $attr_key = false; - $context->register('CurrentAttr', $attr_key); - - // iterate through all the attribute keypairs - // Watch out for name collisions: $key has previously been used - foreach ($attr as $attr_key => $value) { - - // call the definition - if ( isset($defs[$attr_key]) ) { - // there is a local definition defined - if ($defs[$attr_key] === false) { - // We've explicitly been told not to allow this element. - // This is usually when there's a global definition - // that must be overridden. - // Theoretically speaking, we could have a - // AttrDef_DenyAll, but this is faster! - $result = false; - } else { - // validate according to the element's definition - $result = $defs[$attr_key]->validate( - $value, $config, $context - ); - } - } elseif ( isset($d_defs[$attr_key]) ) { - // there is a global definition defined, validate according - // to the global definition - $result = $d_defs[$attr_key]->validate( - $value, $config, $context - ); - } else { - // system never heard of the attribute? DELETE! - $result = false; - } - - // put the results into effect - if ($result === false || $result === null) { - // this is a generic error message that should replaced - // with more specific ones when possible - if ($e) $e->send(E_ERROR, 'AttrValidator: Attribute removed'); - - // remove the attribute - unset($attr[$attr_key]); - } elseif (is_string($result)) { - // generally, if a substitution is happening, there - // was some sort of implicit correction going on. We'll - // delegate it to the attribute classes to say exactly what. - - // simple substitution - $attr[$attr_key] = $result; - } else { - // nothing happens - } - - // we'd also want slightly more complicated substitution - // involving an array as the return value, - // although we're not sure how colliding attributes would - // resolve (certain ones would be completely overriden, - // others would prepend themselves). - } - - $context->destroy('CurrentAttr'); - - // post transforms - - // global (error reporting untested) - foreach ($definition->info_attr_transform_post as $transform) { - $attr = $transform->transform($o = $attr, $config, $context); - if ($e) { - if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); - } - } - - // local (error reporting untested) - foreach ($definition->info[$token->name]->attr_transform_post as $transform) { - $attr = $transform->transform($o = $attr, $config, $context); - if ($e) { - if ($attr != $o) $e->send(E_NOTICE, 'AttrValidator: Attributes transformed', $o, $attr); - } - } - - $token->attr = $attr; - - // destroy CurrentToken if we made it ourselves - if (!$current_token) $context->destroy('CurrentToken'); - - } - - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Bootstrap.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Bootstrap.php deleted file mode 100644 index ae5033203..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Bootstrap.php +++ /dev/null @@ -1,109 +0,0 @@ - -if (!defined('PHP_EOL')) { - switch (strtoupper(substr(PHP_OS, 0, 3))) { - case 'WIN': - define('PHP_EOL', "\r\n"); - break; - case 'DAR': - define('PHP_EOL', "\r"); - break; - default: - define('PHP_EOL', "\n"); - } -} - -/** - * Bootstrap class that contains meta-functionality for HTML Purifier such as - * the autoload function. - * - * @note - * This class may be used without any other files from HTML Purifier. - */ -class HTMLPurifier_Bootstrap -{ - - /** - * Autoload function for HTML Purifier - * @param $class Class to load - */ - public static function autoload($class) { - $file = HTMLPurifier_Bootstrap::getPath($class); - if (!$file) return false; - // Technically speaking, it should be ok and more efficient to - // just do 'require', but Antonio Parraga reports that with - // Zend extensions such as Zend debugger and APC, this invariant - // may be broken. Since we have efficient alternatives, pay - // the cost here and avoid the bug. - require_once HTMLPURIFIER_PREFIX . '/' . $file; - return true; - } - - /** - * Returns the path for a specific class. - */ - public static function getPath($class) { - if (strncmp('HTMLPurifier', $class, 12) !== 0) return false; - // Custom implementations - if (strncmp('HTMLPurifier_Language_', $class, 22) === 0) { - $code = str_replace('_', '-', substr($class, 22)); - $file = 'HTMLPurifier/Language/classes/' . $code . '.php'; - } else { - $file = str_replace('_', '/', $class) . '.php'; - } - if (!file_exists(HTMLPURIFIER_PREFIX . '/' . $file)) return false; - return $file; - } - - /** - * "Pre-registers" our autoloader on the SPL stack. - */ - public static function registerAutoload() { - $autoload = array('HTMLPurifier_Bootstrap', 'autoload'); - if ( ($funcs = spl_autoload_functions()) === false ) { - spl_autoload_register($autoload); - } elseif (function_exists('spl_autoload_unregister')) { - if (version_compare(PHP_VERSION, '5.3.0', '>=')) { - // prepend flag exists, no need for shenanigans - spl_autoload_register($autoload, true, true); - } else { - $buggy = version_compare(PHP_VERSION, '5.2.11', '<'); - $compat = version_compare(PHP_VERSION, '5.1.2', '<=') && - version_compare(PHP_VERSION, '5.1.0', '>='); - foreach ($funcs as $func) { - if ($buggy && is_array($func)) { - // :TRICKY: There are some compatibility issues and some - // places where we need to error out - $reflector = new ReflectionMethod($func[0], $func[1]); - if (!$reflector->isStatic()) { - throw new Exception(' - HTML Purifier autoloader registrar is not compatible - with non-static object methods due to PHP Bug #44144; - Please do not use HTMLPurifier.autoload.php (or any - file that includes this file); instead, place the code: - spl_autoload_register(array(\'HTMLPurifier_Bootstrap\', \'autoload\')) - after your own autoloaders. - '); - } - // Suprisingly, spl_autoload_register supports the - // Class::staticMethod callback format, although call_user_func doesn't - if ($compat) $func = implode('::', $func); - } - spl_autoload_unregister($func); - } - spl_autoload_register($autoload); - foreach ($funcs as $func) spl_autoload_register($func); - } - } - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/CSSDefinition.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/CSSDefinition.php deleted file mode 100644 index 8c4c3127b..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/CSSDefinition.php +++ /dev/null @@ -1,328 +0,0 @@ -info['text-align'] = new HTMLPurifier_AttrDef_Enum( - array('left', 'right', 'center', 'justify'), false); - - $border_style = - $this->info['border-bottom-style'] = - $this->info['border-right-style'] = - $this->info['border-left-style'] = - $this->info['border-top-style'] = new HTMLPurifier_AttrDef_Enum( - array('none', 'hidden', 'dotted', 'dashed', 'solid', 'double', - 'groove', 'ridge', 'inset', 'outset'), false); - - $this->info['border-style'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_style); - - $this->info['clear'] = new HTMLPurifier_AttrDef_Enum( - array('none', 'left', 'right', 'both'), false); - $this->info['float'] = new HTMLPurifier_AttrDef_Enum( - array('none', 'left', 'right'), false); - $this->info['font-style'] = new HTMLPurifier_AttrDef_Enum( - array('normal', 'italic', 'oblique'), false); - $this->info['font-variant'] = new HTMLPurifier_AttrDef_Enum( - array('normal', 'small-caps'), false); - - $uri_or_none = new HTMLPurifier_AttrDef_CSS_Composite( - array( - new HTMLPurifier_AttrDef_Enum(array('none')), - new HTMLPurifier_AttrDef_CSS_URI() - ) - ); - - $this->info['list-style-position'] = new HTMLPurifier_AttrDef_Enum( - array('inside', 'outside'), false); - $this->info['list-style-type'] = new HTMLPurifier_AttrDef_Enum( - array('disc', 'circle', 'square', 'decimal', 'lower-roman', - 'upper-roman', 'lower-alpha', 'upper-alpha', 'none'), false); - $this->info['list-style-image'] = $uri_or_none; - - $this->info['list-style'] = new HTMLPurifier_AttrDef_CSS_ListStyle($config); - - $this->info['text-transform'] = new HTMLPurifier_AttrDef_Enum( - array('capitalize', 'uppercase', 'lowercase', 'none'), false); - $this->info['color'] = new HTMLPurifier_AttrDef_CSS_Color(); - - $this->info['background-image'] = $uri_or_none; - $this->info['background-repeat'] = new HTMLPurifier_AttrDef_Enum( - array('repeat', 'repeat-x', 'repeat-y', 'no-repeat') - ); - $this->info['background-attachment'] = new HTMLPurifier_AttrDef_Enum( - array('scroll', 'fixed') - ); - $this->info['background-position'] = new HTMLPurifier_AttrDef_CSS_BackgroundPosition(); - - $border_color = - $this->info['border-top-color'] = - $this->info['border-bottom-color'] = - $this->info['border-left-color'] = - $this->info['border-right-color'] = - $this->info['background-color'] = new HTMLPurifier_AttrDef_CSS_Composite(array( - new HTMLPurifier_AttrDef_Enum(array('transparent')), - new HTMLPurifier_AttrDef_CSS_Color() - )); - - $this->info['background'] = new HTMLPurifier_AttrDef_CSS_Background($config); - - $this->info['border-color'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_color); - - $border_width = - $this->info['border-top-width'] = - $this->info['border-bottom-width'] = - $this->info['border-left-width'] = - $this->info['border-right-width'] = new HTMLPurifier_AttrDef_CSS_Composite(array( - new HTMLPurifier_AttrDef_Enum(array('thin', 'medium', 'thick')), - new HTMLPurifier_AttrDef_CSS_Length('0') //disallow negative - )); - - $this->info['border-width'] = new HTMLPurifier_AttrDef_CSS_Multiple($border_width); - - $this->info['letter-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite(array( - new HTMLPurifier_AttrDef_Enum(array('normal')), - new HTMLPurifier_AttrDef_CSS_Length() - )); - - $this->info['word-spacing'] = new HTMLPurifier_AttrDef_CSS_Composite(array( - new HTMLPurifier_AttrDef_Enum(array('normal')), - new HTMLPurifier_AttrDef_CSS_Length() - )); - - $this->info['font-size'] = new HTMLPurifier_AttrDef_CSS_Composite(array( - new HTMLPurifier_AttrDef_Enum(array('xx-small', 'x-small', - 'small', 'medium', 'large', 'x-large', 'xx-large', - 'larger', 'smaller')), - new HTMLPurifier_AttrDef_CSS_Percentage(), - new HTMLPurifier_AttrDef_CSS_Length() - )); - - $this->info['line-height'] = new HTMLPurifier_AttrDef_CSS_Composite(array( - new HTMLPurifier_AttrDef_Enum(array('normal')), - new HTMLPurifier_AttrDef_CSS_Number(true), // no negatives - new HTMLPurifier_AttrDef_CSS_Length('0'), - new HTMLPurifier_AttrDef_CSS_Percentage(true) - )); - - $margin = - $this->info['margin-top'] = - $this->info['margin-bottom'] = - $this->info['margin-left'] = - $this->info['margin-right'] = new HTMLPurifier_AttrDef_CSS_Composite(array( - new HTMLPurifier_AttrDef_CSS_Length(), - new HTMLPurifier_AttrDef_CSS_Percentage(), - new HTMLPurifier_AttrDef_Enum(array('auto')) - )); - - $this->info['margin'] = new HTMLPurifier_AttrDef_CSS_Multiple($margin); - - // non-negative - $padding = - $this->info['padding-top'] = - $this->info['padding-bottom'] = - $this->info['padding-left'] = - $this->info['padding-right'] = new HTMLPurifier_AttrDef_CSS_Composite(array( - new HTMLPurifier_AttrDef_CSS_Length('0'), - new HTMLPurifier_AttrDef_CSS_Percentage(true) - )); - - $this->info['padding'] = new HTMLPurifier_AttrDef_CSS_Multiple($padding); - - $this->info['text-indent'] = new HTMLPurifier_AttrDef_CSS_Composite(array( - new HTMLPurifier_AttrDef_CSS_Length(), - new HTMLPurifier_AttrDef_CSS_Percentage() - )); - - $trusted_wh = new HTMLPurifier_AttrDef_CSS_Composite(array( - new HTMLPurifier_AttrDef_CSS_Length('0'), - new HTMLPurifier_AttrDef_CSS_Percentage(true), - new HTMLPurifier_AttrDef_Enum(array('auto')) - )); - $max = $config->get('CSS.MaxImgLength'); - - $this->info['width'] = - $this->info['height'] = - $max === null ? - $trusted_wh : - new HTMLPurifier_AttrDef_Switch('img', - // For img tags: - new HTMLPurifier_AttrDef_CSS_Composite(array( - new HTMLPurifier_AttrDef_CSS_Length('0', $max), - new HTMLPurifier_AttrDef_Enum(array('auto')) - )), - // For everyone else: - $trusted_wh - ); - - $this->info['text-decoration'] = new HTMLPurifier_AttrDef_CSS_TextDecoration(); - - $this->info['font-family'] = new HTMLPurifier_AttrDef_CSS_FontFamily(); - - // this could use specialized code - $this->info['font-weight'] = new HTMLPurifier_AttrDef_Enum( - array('normal', 'bold', 'bolder', 'lighter', '100', '200', '300', - '400', '500', '600', '700', '800', '900'), false); - - // MUST be called after other font properties, as it references - // a CSSDefinition object - $this->info['font'] = new HTMLPurifier_AttrDef_CSS_Font($config); - - // same here - $this->info['border'] = - $this->info['border-bottom'] = - $this->info['border-top'] = - $this->info['border-left'] = - $this->info['border-right'] = new HTMLPurifier_AttrDef_CSS_Border($config); - - $this->info['border-collapse'] = new HTMLPurifier_AttrDef_Enum(array( - 'collapse', 'separate')); - - $this->info['caption-side'] = new HTMLPurifier_AttrDef_Enum(array( - 'top', 'bottom')); - - $this->info['table-layout'] = new HTMLPurifier_AttrDef_Enum(array( - 'auto', 'fixed')); - - $this->info['vertical-align'] = new HTMLPurifier_AttrDef_CSS_Composite(array( - new HTMLPurifier_AttrDef_Enum(array('baseline', 'sub', 'super', - 'top', 'text-top', 'middle', 'bottom', 'text-bottom')), - new HTMLPurifier_AttrDef_CSS_Length(), - new HTMLPurifier_AttrDef_CSS_Percentage() - )); - - $this->info['border-spacing'] = new HTMLPurifier_AttrDef_CSS_Multiple(new HTMLPurifier_AttrDef_CSS_Length(), 2); - - // These CSS properties don't work on many browsers, but we live - // in THE FUTURE! - $this->info['white-space'] = new HTMLPurifier_AttrDef_Enum(array('nowrap', 'normal', 'pre', 'pre-wrap', 'pre-line')); - - if ($config->get('CSS.Proprietary')) { - $this->doSetupProprietary($config); - } - - if ($config->get('CSS.AllowTricky')) { - $this->doSetupTricky($config); - } - - if ($config->get('CSS.Trusted')) { - $this->doSetupTrusted($config); - } - - $allow_important = $config->get('CSS.AllowImportant'); - // wrap all attr-defs with decorator that handles !important - foreach ($this->info as $k => $v) { - $this->info[$k] = new HTMLPurifier_AttrDef_CSS_ImportantDecorator($v, $allow_important); - } - - $this->setupConfigStuff($config); - } - - protected function doSetupProprietary($config) { - // Internet Explorer only scrollbar colors - $this->info['scrollbar-arrow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); - $this->info['scrollbar-base-color'] = new HTMLPurifier_AttrDef_CSS_Color(); - $this->info['scrollbar-darkshadow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); - $this->info['scrollbar-face-color'] = new HTMLPurifier_AttrDef_CSS_Color(); - $this->info['scrollbar-highlight-color'] = new HTMLPurifier_AttrDef_CSS_Color(); - $this->info['scrollbar-shadow-color'] = new HTMLPurifier_AttrDef_CSS_Color(); - - // technically not proprietary, but CSS3, and no one supports it - $this->info['opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); - $this->info['-moz-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); - $this->info['-khtml-opacity'] = new HTMLPurifier_AttrDef_CSS_AlphaValue(); - - // only opacity, for now - $this->info['filter'] = new HTMLPurifier_AttrDef_CSS_Filter(); - - // more CSS3 - $this->info['page-break-after'] = - $this->info['page-break-before'] = new HTMLPurifier_AttrDef_Enum(array('auto','always','avoid','left','right')); - $this->info['page-break-inside'] = new HTMLPurifier_AttrDef_Enum(array('auto','avoid')); - - } - - protected function doSetupTricky($config) { - $this->info['display'] = new HTMLPurifier_AttrDef_Enum(array( - 'inline', 'block', 'list-item', 'run-in', 'compact', - 'marker', 'table', 'inline-block', 'inline-table', 'table-row-group', - 'table-header-group', 'table-footer-group', 'table-row', - 'table-column-group', 'table-column', 'table-cell', 'table-caption', 'none' - )); - $this->info['visibility'] = new HTMLPurifier_AttrDef_Enum(array( - 'visible', 'hidden', 'collapse' - )); - $this->info['overflow'] = new HTMLPurifier_AttrDef_Enum(array('visible', 'hidden', 'auto', 'scroll')); - } - - protected function doSetupTrusted($config) { - $this->info['position'] = new HTMLPurifier_AttrDef_Enum(array( - 'static', 'relative', 'absolute', 'fixed' - )); - $this->info['top'] = - $this->info['left'] = - $this->info['right'] = - $this->info['bottom'] = new HTMLPurifier_AttrDef_CSS_Composite(array( - new HTMLPurifier_AttrDef_CSS_Length(), - new HTMLPurifier_AttrDef_CSS_Percentage(), - new HTMLPurifier_AttrDef_Enum(array('auto')), - )); - $this->info['z-index'] = new HTMLPurifier_AttrDef_CSS_Composite(array( - new HTMLPurifier_AttrDef_Integer(), - new HTMLPurifier_AttrDef_Enum(array('auto')), - )); - } - - /** - * Performs extra config-based processing. Based off of - * HTMLPurifier_HTMLDefinition. - * @todo Refactor duplicate elements into common class (probably using - * composition, not inheritance). - */ - protected function setupConfigStuff($config) { - - // setup allowed elements - $support = "(for information on implementing this, see the ". - "support forums) "; - $allowed_properties = $config->get('CSS.AllowedProperties'); - if ($allowed_properties !== null) { - foreach ($this->info as $name => $d) { - if(!isset($allowed_properties[$name])) unset($this->info[$name]); - unset($allowed_properties[$name]); - } - // emit errors - foreach ($allowed_properties as $name => $d) { - // :TODO: Is this htmlspecialchars() call really necessary? - $name = htmlspecialchars($name); - trigger_error("Style attribute '$name' is not supported $support", E_USER_WARNING); - } - } - - $forbidden_properties = $config->get('CSS.ForbiddenProperties'); - if ($forbidden_properties !== null) { - foreach ($this->info as $name => $d) { - if (isset($forbidden_properties[$name])) { - unset($this->info[$name]); - } - } - } - - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef.php deleted file mode 100644 index c5d5216da..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef.php +++ /dev/null @@ -1,48 +0,0 @@ -elements; - } - - /** - * Validates nodes according to definition and returns modification. - * - * @param $tokens_of_children Array of HTMLPurifier_Token - * @param $config HTMLPurifier_Config object - * @param $context HTMLPurifier_Context object - * @return bool true to leave nodes as is - * @return bool false to remove parent node - * @return array of replacement child tokens - */ - abstract public function validateChildren($tokens_of_children, $config, $context); -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Chameleon.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Chameleon.php deleted file mode 100644 index 15c364ee3..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Chameleon.php +++ /dev/null @@ -1,48 +0,0 @@ -inline = new HTMLPurifier_ChildDef_Optional($inline); - $this->block = new HTMLPurifier_ChildDef_Optional($block); - $this->elements = $this->block->elements; - } - - public function validateChildren($tokens_of_children, $config, $context) { - if ($context->get('IsInline') === false) { - return $this->block->validateChildren( - $tokens_of_children, $config, $context); - } else { - return $this->inline->validateChildren( - $tokens_of_children, $config, $context); - } - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Custom.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Custom.php deleted file mode 100644 index b68047b4b..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Custom.php +++ /dev/null @@ -1,90 +0,0 @@ -dtd_regex = $dtd_regex; - $this->_compileRegex(); - } - /** - * Compiles the PCRE regex from a DTD regex ($dtd_regex to $_pcre_regex) - */ - protected function _compileRegex() { - $raw = str_replace(' ', '', $this->dtd_regex); - if ($raw{0} != '(') { - $raw = "($raw)"; - } - $el = '[#a-zA-Z0-9_.-]+'; - $reg = $raw; - - // COMPLICATED! AND MIGHT BE BUGGY! I HAVE NO CLUE WHAT I'M - // DOING! Seriously: if there's problems, please report them. - - // collect all elements into the $elements array - preg_match_all("/$el/", $reg, $matches); - foreach ($matches[0] as $match) { - $this->elements[$match] = true; - } - - // setup all elements as parentheticals with leading commas - $reg = preg_replace("/$el/", '(,\\0)', $reg); - - // remove commas when they were not solicited - $reg = preg_replace("/([^,(|]\(+),/", '\\1', $reg); - - // remove all non-paranthetical commas: they are handled by first regex - $reg = preg_replace("/,\(/", '(', $reg); - - $this->_pcre_regex = $reg; - } - public function validateChildren($tokens_of_children, $config, $context) { - $list_of_children = ''; - $nesting = 0; // depth into the nest - foreach ($tokens_of_children as $token) { - if (!empty($token->is_whitespace)) continue; - - $is_child = ($nesting == 0); // direct - - if ($token instanceof HTMLPurifier_Token_Start) { - $nesting++; - } elseif ($token instanceof HTMLPurifier_Token_End) { - $nesting--; - } - - if ($is_child) { - $list_of_children .= $token->name . ','; - } - } - // add leading comma to deal with stray comma declarations - $list_of_children = ',' . rtrim($list_of_children, ','); - $okay = - preg_match( - '/^,?'.$this->_pcre_regex.'$/', - $list_of_children - ); - - return (bool) $okay; - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Empty.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Empty.php deleted file mode 100644 index 13171f665..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Empty.php +++ /dev/null @@ -1,20 +0,0 @@ - true, 'ul' => true, 'ol' => true); - public function validateChildren($tokens_of_children, $config, $context) { - // Flag for subclasses - $this->whitespace = false; - - // if there are no tokens, delete parent node - if (empty($tokens_of_children)) return false; - - // the new set of children - $result = array(); - - // current depth into the nest - $nesting = 0; - - // a little sanity check to make sure it's not ALL whitespace - $all_whitespace = true; - - $seen_li = false; - $need_close_li = false; - - foreach ($tokens_of_children as $token) { - if (!empty($token->is_whitespace)) { - $result[] = $token; - continue; - } - $all_whitespace = false; // phew, we're not talking about whitespace - - if ($nesting == 1 && $need_close_li) { - $result[] = new HTMLPurifier_Token_End('li'); - $nesting--; - $need_close_li = false; - } - - $is_child = ($nesting == 0); - - if ($token instanceof HTMLPurifier_Token_Start) { - $nesting++; - } elseif ($token instanceof HTMLPurifier_Token_End) { - $nesting--; - } - - if ($is_child) { - if ($token->name === 'li') { - // good - $seen_li = true; - } elseif ($token->name === 'ul' || $token->name === 'ol') { - // we want to tuck this into the previous li - $need_close_li = true; - $nesting++; - if (!$seen_li) { - // create a new li element - $result[] = new HTMLPurifier_Token_Start('li'); - } else { - // backtrack until found - while(true) { - $t = array_pop($result); - if ($t instanceof HTMLPurifier_Token_End) { - // XXX actually, these invariants could very plausibly be violated - // if we are doing silly things with modifying the set of allowed elements. - // FORTUNATELY, it doesn't make a difference, since the allowed - // elements are hard-coded here! - if ($t->name !== 'li') { - trigger_error("Only li present invariant violated in List ChildDef", E_USER_ERROR); - return false; - } - break; - } elseif ($t instanceof HTMLPurifier_Token_Empty) { // bleagh - if ($t->name !== 'li') { - trigger_error("Only li present invariant violated in List ChildDef", E_USER_ERROR); - return false; - } - // XXX this should have a helper for it... - $result[] = new HTMLPurifier_Token_Start('li', $t->attr, $t->line, $t->col, $t->armor); - break; - } else { - if (!$t->is_whitespace) { - trigger_error("Only whitespace present invariant violated in List ChildDef", E_USER_ERROR); - return false; - } - } - } - } - } else { - // start wrapping (this doesn't precisely mimic - // browser behavior, but what browsers do is kind of - // hard to mimic in a standards compliant way - // XXX Actually, this has no impact in practice, - // because this gets handled earlier. Arguably, - // we should rip out all of that processing - $result[] = new HTMLPurifier_Token_Start('li'); - $nesting++; - $seen_li = true; - $need_close_li = true; - } - } - $result[] = $token; - } - if ($need_close_li) { - $result[] = new HTMLPurifier_Token_End('li'); - } - if (empty($result)) return false; - if ($all_whitespace) { - return false; - } - if ($tokens_of_children == $result) return true; - return $result; - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Optional.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Optional.php deleted file mode 100644 index 32bcb9898..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Optional.php +++ /dev/null @@ -1,26 +0,0 @@ -whitespace) return $tokens_of_children; - else return array(); - } - return $result; - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Required.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Required.php deleted file mode 100644 index 4889f249b..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Required.php +++ /dev/null @@ -1,117 +0,0 @@ - $x) { - $elements[$i] = true; - if (empty($i)) unset($elements[$i]); // remove blank - } - } - $this->elements = $elements; - } - public $allow_empty = false; - public $type = 'required'; - public function validateChildren($tokens_of_children, $config, $context) { - // Flag for subclasses - $this->whitespace = false; - - // if there are no tokens, delete parent node - if (empty($tokens_of_children)) return false; - - // the new set of children - $result = array(); - - // current depth into the nest - $nesting = 0; - - // whether or not we're deleting a node - $is_deleting = false; - - // whether or not parsed character data is allowed - // this controls whether or not we silently drop a tag - // or generate escaped HTML from it - $pcdata_allowed = isset($this->elements['#PCDATA']); - - // a little sanity check to make sure it's not ALL whitespace - $all_whitespace = true; - - // some configuration - $escape_invalid_children = $config->get('Core.EscapeInvalidChildren'); - - // generator - $gen = new HTMLPurifier_Generator($config, $context); - - foreach ($tokens_of_children as $token) { - if (!empty($token->is_whitespace)) { - $result[] = $token; - continue; - } - $all_whitespace = false; // phew, we're not talking about whitespace - - $is_child = ($nesting == 0); - - if ($token instanceof HTMLPurifier_Token_Start) { - $nesting++; - } elseif ($token instanceof HTMLPurifier_Token_End) { - $nesting--; - } - - if ($is_child) { - $is_deleting = false; - if (!isset($this->elements[$token->name])) { - $is_deleting = true; - if ($pcdata_allowed && $token instanceof HTMLPurifier_Token_Text) { - $result[] = $token; - } elseif ($pcdata_allowed && $escape_invalid_children) { - $result[] = new HTMLPurifier_Token_Text( - $gen->generateFromToken($token) - ); - } - continue; - } - } - if (!$is_deleting || ($pcdata_allowed && $token instanceof HTMLPurifier_Token_Text)) { - $result[] = $token; - } elseif ($pcdata_allowed && $escape_invalid_children) { - $result[] = - new HTMLPurifier_Token_Text( - $gen->generateFromToken($token) - ); - } else { - // drop silently - } - } - if (empty($result)) return false; - if ($all_whitespace) { - $this->whitespace = true; - return false; - } - if ($tokens_of_children == $result) return true; - return $result; - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/StrictBlockquote.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/StrictBlockquote.php deleted file mode 100644 index dfae8a6e5..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/StrictBlockquote.php +++ /dev/null @@ -1,88 +0,0 @@ -init($config); - return $this->fake_elements; - } - - public function validateChildren($tokens_of_children, $config, $context) { - - $this->init($config); - - // trick the parent class into thinking it allows more - $this->elements = $this->fake_elements; - $result = parent::validateChildren($tokens_of_children, $config, $context); - $this->elements = $this->real_elements; - - if ($result === false) return array(); - if ($result === true) $result = $tokens_of_children; - - $def = $config->getHTMLDefinition(); - $block_wrap_start = new HTMLPurifier_Token_Start($def->info_block_wrapper); - $block_wrap_end = new HTMLPurifier_Token_End( $def->info_block_wrapper); - $is_inline = false; - $depth = 0; - $ret = array(); - - // assuming that there are no comment tokens - foreach ($result as $i => $token) { - $token = $result[$i]; - // ifs are nested for readability - if (!$is_inline) { - if (!$depth) { - if ( - ($token instanceof HTMLPurifier_Token_Text && !$token->is_whitespace) || - (!$token instanceof HTMLPurifier_Token_Text && !isset($this->elements[$token->name])) - ) { - $is_inline = true; - $ret[] = $block_wrap_start; - } - } - } else { - if (!$depth) { - // starting tokens have been inline text / empty - if ($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) { - if (isset($this->elements[$token->name])) { - // ended - $ret[] = $block_wrap_end; - $is_inline = false; - } - } - } - } - $ret[] = $token; - if ($token instanceof HTMLPurifier_Token_Start) $depth++; - if ($token instanceof HTMLPurifier_Token_End) $depth--; - } - if ($is_inline) $ret[] = $block_wrap_end; - return $ret; - } - - private function init($config) { - if (!$this->init) { - $def = $config->getHTMLDefinition(); - // allow all inline elements - $this->real_elements = $this->elements; - $this->fake_elements = $def->info_content_sets['Flow']; - $this->fake_elements['#PCDATA'] = true; - $this->init = true; - } - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Table.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Table.php deleted file mode 100644 index 9a93421a1..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ChildDef/Table.php +++ /dev/null @@ -1,227 +0,0 @@ - true, 'tbody' => true, 'thead' => true, - 'tfoot' => true, 'caption' => true, 'colgroup' => true, 'col' => true); - public function __construct() {} - public function validateChildren($tokens_of_children, $config, $context) { - if (empty($tokens_of_children)) return false; - - // this ensures that the loop gets run one last time before closing - // up. It's a little bit of a hack, but it works! Just make sure you - // get rid of the token later. - $tokens_of_children[] = false; - - // only one of these elements is allowed in a table - $caption = false; - $thead = false; - $tfoot = false; - - // as many of these as you want - $cols = array(); - $content = array(); - - $nesting = 0; // current depth so we can determine nodes - $is_collecting = false; // are we globbing together tokens to package - // into one of the collectors? - $collection = array(); // collected nodes - $tag_index = 0; // the first node might be whitespace, - // so this tells us where the start tag is - $tbody_mode = false; // if true, then we need to wrap any stray - // s with a . - - foreach ($tokens_of_children as $token) { - $is_child = ($nesting == 0); - - if ($token === false) { - // terminating sequence started - } elseif ($token instanceof HTMLPurifier_Token_Start) { - $nesting++; - } elseif ($token instanceof HTMLPurifier_Token_End) { - $nesting--; - } - - // handle node collection - if ($is_collecting) { - if ($is_child) { - // okay, let's stash the tokens away - // first token tells us the type of the collection - switch ($collection[$tag_index]->name) { - case 'tbody': - $tbody_mode = true; - case 'tr': - $content[] = $collection; - break; - case 'caption': - if ($caption !== false) break; - $caption = $collection; - break; - case 'thead': - case 'tfoot': - $tbody_mode = true; - // XXX This breaks rendering properties with - // Firefox, which never floats a to - // the top. Ever. (Our scheme will float the - // first to the top.) So maybe - // s that are not first should be - // turned into ? Very tricky, indeed. - - // access the appropriate variable, $thead or $tfoot - $var = $collection[$tag_index]->name; - if ($$var === false) { - $$var = $collection; - } else { - // Oops, there's a second one! What - // should we do? Current behavior is to - // transmutate the first and last entries into - // tbody tags, and then put into content. - // Maybe a better idea is to *attach - // it* to the existing thead or tfoot? - // We don't do this, because Firefox - // doesn't float an extra tfoot to the - // bottom like it does for the first one. - $collection[$tag_index]->name = 'tbody'; - $collection[count($collection)-1]->name = 'tbody'; - $content[] = $collection; - } - break; - case 'colgroup': - $cols[] = $collection; - break; - } - $collection = array(); - $is_collecting = false; - $tag_index = 0; - } else { - // add the node to the collection - $collection[] = $token; - } - } - - // terminate - if ($token === false) break; - - if ($is_child) { - // determine what we're dealing with - if ($token->name == 'col') { - // the only empty tag in the possie, we can handle it - // immediately - $cols[] = array_merge($collection, array($token)); - $collection = array(); - $tag_index = 0; - continue; - } - switch($token->name) { - case 'caption': - case 'colgroup': - case 'thead': - case 'tfoot': - case 'tbody': - case 'tr': - $is_collecting = true; - $collection[] = $token; - continue; - default: - if (!empty($token->is_whitespace)) { - $collection[] = $token; - $tag_index++; - } - continue; - } - } - } - - if (empty($content)) return false; - - $ret = array(); - if ($caption !== false) $ret = array_merge($ret, $caption); - if ($cols !== false) foreach ($cols as $token_array) $ret = array_merge($ret, $token_array); - if ($thead !== false) $ret = array_merge($ret, $thead); - if ($tfoot !== false) $ret = array_merge($ret, $tfoot); - - if ($tbody_mode) { - // a little tricky, since the start of the collection may be - // whitespace - $inside_tbody = false; - foreach ($content as $token_array) { - // find the starting token - foreach ($token_array as $t) { - if ($t->name === 'tr' || $t->name === 'tbody') { - break; - } - } // iterator variable carries over - if ($t->name === 'tr') { - if ($inside_tbody) { - $ret = array_merge($ret, $token_array); - } else { - $ret[] = new HTMLPurifier_Token_Start('tbody'); - $ret = array_merge($ret, $token_array); - $inside_tbody = true; - } - } elseif ($t->name === 'tbody') { - if ($inside_tbody) { - $ret[] = new HTMLPurifier_Token_End('tbody'); - $inside_tbody = false; - $ret = array_merge($ret, $token_array); - } else { - $ret = array_merge($ret, $token_array); - } - } else { - trigger_error("tr/tbody in content invariant failed in Table ChildDef", E_USER_ERROR); - } - } - if ($inside_tbody) { - $ret[] = new HTMLPurifier_Token_End('tbody'); - } - } else { - foreach ($content as $token_array) { - // invariant: everything in here is s - $ret = array_merge($ret, $token_array); - } - } - - if (!empty($collection) && $is_collecting == false){ - // grab the trailing space - $ret = array_merge($ret, $collection); - } - - array_pop($tokens_of_children); // remove phantom token - - return ($ret === $tokens_of_children) ? true : $ret; - - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Config.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Config.php deleted file mode 100644 index 489ea0464..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Config.php +++ /dev/null @@ -1,710 +0,0 @@ -defaultPlist; - $this->plist = new HTMLPurifier_PropertyList($parent); - $this->def = $definition; // keep a copy around for checking - $this->parser = new HTMLPurifier_VarParser_Flexible(); - } - - /** - * Convenience constructor that creates a config object based on a mixed var - * @param mixed $config Variable that defines the state of the config - * object. Can be: a HTMLPurifier_Config() object, - * an array of directives based on loadArray(), - * or a string filename of an ini file. - * @param HTMLPurifier_ConfigSchema Schema object - * @return Configured HTMLPurifier_Config object - */ - public static function create($config, $schema = null) { - if ($config instanceof HTMLPurifier_Config) { - // pass-through - return $config; - } - if (!$schema) { - $ret = HTMLPurifier_Config::createDefault(); - } else { - $ret = new HTMLPurifier_Config($schema); - } - if (is_string($config)) $ret->loadIni($config); - elseif (is_array($config)) $ret->loadArray($config); - return $ret; - } - - /** - * Creates a new config object that inherits from a previous one. - * @param HTMLPurifier_Config $config Configuration object to inherit - * from. - * @return HTMLPurifier_Config object with $config as its parent. - */ - public static function inherit(HTMLPurifier_Config $config) { - return new HTMLPurifier_Config($config->def, $config->plist); - } - - /** - * Convenience constructor that creates a default configuration object. - * @return Default HTMLPurifier_Config object. - */ - public static function createDefault() { - $definition = HTMLPurifier_ConfigSchema::instance(); - $config = new HTMLPurifier_Config($definition); - return $config; - } - - /** - * Retreives a value from the configuration. - * @param $key String key - */ - public function get($key, $a = null) { - if ($a !== null) { - $this->triggerError("Using deprecated API: use \$config->get('$key.$a') instead", E_USER_WARNING); - $key = "$key.$a"; - } - if (!$this->finalized) $this->autoFinalize(); - if (!isset($this->def->info[$key])) { - // can't add % due to SimpleTest bug - $this->triggerError('Cannot retrieve value of undefined directive ' . htmlspecialchars($key), - E_USER_WARNING); - return; - } - if (isset($this->def->info[$key]->isAlias)) { - $d = $this->def->info[$key]; - $this->triggerError('Cannot get value from aliased directive, use real name ' . $d->key, - E_USER_ERROR); - return; - } - if ($this->lock) { - list($ns) = explode('.', $key); - if ($ns !== $this->lock) { - $this->triggerError('Cannot get value of namespace ' . $ns . ' when lock for ' . $this->lock . ' is active, this probably indicates a Definition setup method is accessing directives that are not within its namespace', E_USER_ERROR); - return; - } - } - return $this->plist->get($key); - } - - /** - * Retreives an array of directives to values from a given namespace - * @param $namespace String namespace - */ - public function getBatch($namespace) { - if (!$this->finalized) $this->autoFinalize(); - $full = $this->getAll(); - if (!isset($full[$namespace])) { - $this->triggerError('Cannot retrieve undefined namespace ' . htmlspecialchars($namespace), - E_USER_WARNING); - return; - } - return $full[$namespace]; - } - - /** - * Returns a SHA-1 signature of a segment of the configuration object - * that uniquely identifies that particular configuration - * @note Revision is handled specially and is removed from the batch - * before processing! - * @param $namespace Namespace to get serial for - */ - public function getBatchSerial($namespace) { - if (empty($this->serials[$namespace])) { - $batch = $this->getBatch($namespace); - unset($batch['DefinitionRev']); - $this->serials[$namespace] = sha1(serialize($batch)); - } - return $this->serials[$namespace]; - } - - /** - * Returns a SHA-1 signature for the entire configuration object - * that uniquely identifies that particular configuration - */ - public function getSerial() { - if (empty($this->serial)) { - $this->serial = sha1(serialize($this->getAll())); - } - return $this->serial; - } - - /** - * Retrieves all directives, organized by namespace - * @warning This is a pretty inefficient function, avoid if you can - */ - public function getAll() { - if (!$this->finalized) $this->autoFinalize(); - $ret = array(); - foreach ($this->plist->squash() as $name => $value) { - list($ns, $key) = explode('.', $name, 2); - $ret[$ns][$key] = $value; - } - return $ret; - } - - /** - * Sets a value to configuration. - * @param $key String key - * @param $value Mixed value - */ - public function set($key, $value, $a = null) { - if (strpos($key, '.') === false) { - $namespace = $key; - $directive = $value; - $value = $a; - $key = "$key.$directive"; - $this->triggerError("Using deprecated API: use \$config->set('$key', ...) instead", E_USER_NOTICE); - } else { - list($namespace) = explode('.', $key); - } - if ($this->isFinalized('Cannot set directive after finalization')) return; - if (!isset($this->def->info[$key])) { - $this->triggerError('Cannot set undefined directive ' . htmlspecialchars($key) . ' to value', - E_USER_WARNING); - return; - } - $def = $this->def->info[$key]; - - if (isset($def->isAlias)) { - if ($this->aliasMode) { - $this->triggerError('Double-aliases not allowed, please fix '. - 'ConfigSchema bug with' . $key, E_USER_ERROR); - return; - } - $this->aliasMode = true; - $this->set($def->key, $value); - $this->aliasMode = false; - $this->triggerError("$key is an alias, preferred directive name is {$def->key}", E_USER_NOTICE); - return; - } - - // Raw type might be negative when using the fully optimized form - // of stdclass, which indicates allow_null == true - $rtype = is_int($def) ? $def : $def->type; - if ($rtype < 0) { - $type = -$rtype; - $allow_null = true; - } else { - $type = $rtype; - $allow_null = isset($def->allow_null); - } - - try { - $value = $this->parser->parse($value, $type, $allow_null); - } catch (HTMLPurifier_VarParserException $e) { - $this->triggerError('Value for ' . $key . ' is of invalid type, should be ' . HTMLPurifier_VarParser::getTypeName($type), E_USER_WARNING); - return; - } - if (is_string($value) && is_object($def)) { - // resolve value alias if defined - if (isset($def->aliases[$value])) { - $value = $def->aliases[$value]; - } - // check to see if the value is allowed - if (isset($def->allowed) && !isset($def->allowed[$value])) { - $this->triggerError('Value not supported, valid values are: ' . - $this->_listify($def->allowed), E_USER_WARNING); - return; - } - } - $this->plist->set($key, $value); - - // reset definitions if the directives they depend on changed - // this is a very costly process, so it's discouraged - // with finalization - if ($namespace == 'HTML' || $namespace == 'CSS' || $namespace == 'URI') { - $this->definitions[$namespace] = null; - } - - $this->serials[$namespace] = false; - } - - /** - * Convenience function for error reporting - */ - private function _listify($lookup) { - $list = array(); - foreach ($lookup as $name => $b) $list[] = $name; - return implode(', ', $list); - } - - /** - * Retrieves object reference to the HTML definition. - * @param $raw Return a copy that has not been setup yet. Must be - * called before it's been setup, otherwise won't work. - * @param $optimized If true, this method may return null, to - * indicate that a cached version of the modified - * definition object is available and no further edits - * are necessary. Consider using - * maybeGetRawHTMLDefinition, which is more explicitly - * named, instead. - */ - public function getHTMLDefinition($raw = false, $optimized = false) { - return $this->getDefinition('HTML', $raw, $optimized); - } - - /** - * Retrieves object reference to the CSS definition - * @param $raw Return a copy that has not been setup yet. Must be - * called before it's been setup, otherwise won't work. - * @param $optimized If true, this method may return null, to - * indicate that a cached version of the modified - * definition object is available and no further edits - * are necessary. Consider using - * maybeGetRawCSSDefinition, which is more explicitly - * named, instead. - */ - public function getCSSDefinition($raw = false, $optimized = false) { - return $this->getDefinition('CSS', $raw, $optimized); - } - - /** - * Retrieves object reference to the URI definition - * @param $raw Return a copy that has not been setup yet. Must be - * called before it's been setup, otherwise won't work. - * @param $optimized If true, this method may return null, to - * indicate that a cached version of the modified - * definition object is available and no further edits - * are necessary. Consider using - * maybeGetRawURIDefinition, which is more explicitly - * named, instead. - */ - public function getURIDefinition($raw = false, $optimized = false) { - return $this->getDefinition('URI', $raw, $optimized); - } - - /** - * Retrieves a definition - * @param $type Type of definition: HTML, CSS, etc - * @param $raw Whether or not definition should be returned raw - * @param $optimized Only has an effect when $raw is true. Whether - * or not to return null if the result is already present in - * the cache. This is off by default for backwards - * compatibility reasons, but you need to do things this - * way in order to ensure that caching is done properly. - * Check out enduser-customize.html for more details. - * We probably won't ever change this default, as much as the - * maybe semantics is the "right thing to do." - */ - public function getDefinition($type, $raw = false, $optimized = false) { - if ($optimized && !$raw) { - throw new HTMLPurifier_Exception("Cannot set optimized = true when raw = false"); - } - if (!$this->finalized) $this->autoFinalize(); - // temporarily suspend locks, so we can handle recursive definition calls - $lock = $this->lock; - $this->lock = null; - $factory = HTMLPurifier_DefinitionCacheFactory::instance(); - $cache = $factory->create($type, $this); - $this->lock = $lock; - if (!$raw) { - // full definition - // --------------- - // check if definition is in memory - if (!empty($this->definitions[$type])) { - $def = $this->definitions[$type]; - // check if the definition is setup - if ($def->setup) { - return $def; - } else { - $def->setup($this); - if ($def->optimized) $cache->add($def, $this); - return $def; - } - } - // check if definition is in cache - $def = $cache->get($this); - if ($def) { - // definition in cache, save to memory and return it - $this->definitions[$type] = $def; - return $def; - } - // initialize it - $def = $this->initDefinition($type); - // set it up - $this->lock = $type; - $def->setup($this); - $this->lock = null; - // save in cache - $cache->add($def, $this); - // return it - return $def; - } else { - // raw definition - // -------------- - // check preconditions - $def = null; - if ($optimized) { - if (is_null($this->get($type . '.DefinitionID'))) { - // fatally error out if definition ID not set - throw new HTMLPurifier_Exception("Cannot retrieve raw version without specifying %$type.DefinitionID"); - } - } - if (!empty($this->definitions[$type])) { - $def = $this->definitions[$type]; - if ($def->setup && !$optimized) { - $extra = $this->chatty ? " (try moving this code block earlier in your initialization)" : ""; - throw new HTMLPurifier_Exception("Cannot retrieve raw definition after it has already been setup" . $extra); - } - if ($def->optimized === null) { - $extra = $this->chatty ? " (try flushing your cache)" : ""; - throw new HTMLPurifier_Exception("Optimization status of definition is unknown" . $extra); - } - if ($def->optimized !== $optimized) { - $msg = $optimized ? "optimized" : "unoptimized"; - $extra = $this->chatty ? " (this backtrace is for the first inconsistent call, which was for a $msg raw definition)" : ""; - throw new HTMLPurifier_Exception("Inconsistent use of optimized and unoptimized raw definition retrievals" . $extra); - } - } - // check if definition was in memory - if ($def) { - if ($def->setup) { - // invariant: $optimized === true (checked above) - return null; - } else { - return $def; - } - } - // if optimized, check if definition was in cache - // (because we do the memory check first, this formulation - // is prone to cache slamming, but I think - // guaranteeing that either /all/ of the raw - // setup code or /none/ of it is run is more important.) - if ($optimized) { - // This code path only gets run once; once we put - // something in $definitions (which is guaranteed by the - // trailing code), we always short-circuit above. - $def = $cache->get($this); - if ($def) { - // save the full definition for later, but don't - // return it yet - $this->definitions[$type] = $def; - return null; - } - } - // check invariants for creation - if (!$optimized) { - if (!is_null($this->get($type . '.DefinitionID'))) { - if ($this->chatty) { - $this->triggerError("Due to a documentation error in previous version of HTML Purifier, your definitions are not being cached. If this is OK, you can remove the %$type.DefinitionRev and %$type.DefinitionID declaration. Otherwise, modify your code to use maybeGetRawDefinition, and test if the returned value is null before making any edits (if it is null, that means that a cached version is available, and no raw operations are necessary). See Customize for more details", E_USER_WARNING); - } else { - $this->triggerError("Useless DefinitionID declaration", E_USER_WARNING); - } - } - } - // initialize it - $def = $this->initDefinition($type); - $def->optimized = $optimized; - return $def; - } - throw new HTMLPurifier_Exception("The impossible happened!"); - } - - private function initDefinition($type) { - // quick checks failed, let's create the object - if ($type == 'HTML') { - $def = new HTMLPurifier_HTMLDefinition(); - } elseif ($type == 'CSS') { - $def = new HTMLPurifier_CSSDefinition(); - } elseif ($type == 'URI') { - $def = new HTMLPurifier_URIDefinition(); - } else { - throw new HTMLPurifier_Exception("Definition of $type type not supported"); - } - $this->definitions[$type] = $def; - return $def; - } - - public function maybeGetRawDefinition($name) { - return $this->getDefinition($name, true, true); - } - - public function maybeGetRawHTMLDefinition() { - return $this->getDefinition('HTML', true, true); - } - - public function maybeGetRawCSSDefinition() { - return $this->getDefinition('CSS', true, true); - } - - public function maybeGetRawURIDefinition() { - return $this->getDefinition('URI', true, true); - } - - /** - * Loads configuration values from an array with the following structure: - * Namespace.Directive => Value - * @param $config_array Configuration associative array - */ - public function loadArray($config_array) { - if ($this->isFinalized('Cannot load directives after finalization')) return; - foreach ($config_array as $key => $value) { - $key = str_replace('_', '.', $key); - if (strpos($key, '.') !== false) { - $this->set($key, $value); - } else { - $namespace = $key; - $namespace_values = $value; - foreach ($namespace_values as $directive => $value) { - $this->set($namespace .'.'. $directive, $value); - } - } - } - } - - /** - * Returns a list of array(namespace, directive) for all directives - * that are allowed in a web-form context as per an allowed - * namespaces/directives list. - * @param $allowed List of allowed namespaces/directives - */ - public static function getAllowedDirectivesForForm($allowed, $schema = null) { - if (!$schema) { - $schema = HTMLPurifier_ConfigSchema::instance(); - } - if ($allowed !== true) { - if (is_string($allowed)) $allowed = array($allowed); - $allowed_ns = array(); - $allowed_directives = array(); - $blacklisted_directives = array(); - foreach ($allowed as $ns_or_directive) { - if (strpos($ns_or_directive, '.') !== false) { - // directive - if ($ns_or_directive[0] == '-') { - $blacklisted_directives[substr($ns_or_directive, 1)] = true; - } else { - $allowed_directives[$ns_or_directive] = true; - } - } else { - // namespace - $allowed_ns[$ns_or_directive] = true; - } - } - } - $ret = array(); - foreach ($schema->info as $key => $def) { - list($ns, $directive) = explode('.', $key, 2); - if ($allowed !== true) { - if (isset($blacklisted_directives["$ns.$directive"])) continue; - if (!isset($allowed_directives["$ns.$directive"]) && !isset($allowed_ns[$ns])) continue; - } - if (isset($def->isAlias)) continue; - if ($directive == 'DefinitionID' || $directive == 'DefinitionRev') continue; - $ret[] = array($ns, $directive); - } - return $ret; - } - - /** - * Loads configuration values from $_GET/$_POST that were posted - * via ConfigForm - * @param $array $_GET or $_POST array to import - * @param $index Index/name that the config variables are in - * @param $allowed List of allowed namespaces/directives - * @param $mq_fix Boolean whether or not to enable magic quotes fix - * @param $schema Instance of HTMLPurifier_ConfigSchema to use, if not global copy - */ - public static function loadArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) { - $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $schema); - $config = HTMLPurifier_Config::create($ret, $schema); - return $config; - } - - /** - * Merges in configuration values from $_GET/$_POST to object. NOT STATIC. - * @note Same parameters as loadArrayFromForm - */ - public function mergeArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true) { - $ret = HTMLPurifier_Config::prepareArrayFromForm($array, $index, $allowed, $mq_fix, $this->def); - $this->loadArray($ret); - } - - /** - * Prepares an array from a form into something usable for the more - * strict parts of HTMLPurifier_Config - */ - public static function prepareArrayFromForm($array, $index = false, $allowed = true, $mq_fix = true, $schema = null) { - if ($index !== false) $array = (isset($array[$index]) && is_array($array[$index])) ? $array[$index] : array(); - $mq = $mq_fix && function_exists('get_magic_quotes_gpc') && get_magic_quotes_gpc(); - - $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $schema); - $ret = array(); - foreach ($allowed as $key) { - list($ns, $directive) = $key; - $skey = "$ns.$directive"; - if (!empty($array["Null_$skey"])) { - $ret[$ns][$directive] = null; - continue; - } - if (!isset($array[$skey])) continue; - $value = $mq ? stripslashes($array[$skey]) : $array[$skey]; - $ret[$ns][$directive] = $value; - } - return $ret; - } - - /** - * Loads configuration values from an ini file - * @param $filename Name of ini file - */ - public function loadIni($filename) { - if ($this->isFinalized('Cannot load directives after finalization')) return; - $array = parse_ini_file($filename, true); - $this->loadArray($array); - } - - /** - * Checks whether or not the configuration object is finalized. - * @param $error String error message, or false for no error - */ - public function isFinalized($error = false) { - if ($this->finalized && $error) { - $this->triggerError($error, E_USER_ERROR); - } - return $this->finalized; - } - - /** - * Finalizes configuration only if auto finalize is on and not - * already finalized - */ - public function autoFinalize() { - if ($this->autoFinalize) { - $this->finalize(); - } else { - $this->plist->squash(true); - } - } - - /** - * Finalizes a configuration object, prohibiting further change - */ - public function finalize() { - $this->finalized = true; - $this->parser = null; - } - - /** - * Produces a nicely formatted error message by supplying the - * stack frame information OUTSIDE of HTMLPurifier_Config. - */ - protected function triggerError($msg, $no) { - // determine previous stack frame - $extra = ''; - if ($this->chatty) { - $trace = debug_backtrace(); - // zip(tail(trace), trace) -- but PHP is not Haskell har har - for ($i = 0, $c = count($trace); $i < $c - 1; $i++) { - // XXX this is not correct on some versions of HTML Purifier - if ($trace[$i + 1]['class'] === 'HTMLPurifier_Config') { - continue; - } - $frame = $trace[$i]; - $extra = " invoked on line {$frame['line']} in file {$frame['file']}"; - break; - } - } - trigger_error($msg . $extra, $no); - } - - /** - * Returns a serialized form of the configuration object that can - * be reconstituted. - */ - public function serialize() { - $this->getDefinition('HTML'); - $this->getDefinition('CSS'); - $this->getDefinition('URI'); - return serialize($this); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema.php deleted file mode 100644 index fadf7a589..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema.php +++ /dev/null @@ -1,164 +0,0 @@ - array( - * 'Directive' => new stdclass(), - * ) - * ) - * - * The stdclass may have the following properties: - * - * - If isAlias isn't set: - * - type: Integer type of directive, see HTMLPurifier_VarParser for definitions - * - allow_null: If set, this directive allows null values - * - aliases: If set, an associative array of value aliases to real values - * - allowed: If set, a lookup array of allowed (string) values - * - If isAlias is set: - * - namespace: Namespace this directive aliases to - * - name: Directive name this directive aliases to - * - * In certain degenerate cases, stdclass will actually be an integer. In - * that case, the value is equivalent to an stdclass with the type - * property set to the integer. If the integer is negative, type is - * equal to the absolute value of integer, and allow_null is true. - * - * This class is friendly with HTMLPurifier_Config. If you need introspection - * about the schema, you're better of using the ConfigSchema_Interchange, - * which uses more memory but has much richer information. - */ - public $info = array(); - - /** - * Application-wide singleton - */ - static protected $singleton; - - public function __construct() { - $this->defaultPlist = new HTMLPurifier_PropertyList(); - } - - /** - * Unserializes the default ConfigSchema. - */ - public static function makeFromSerial() { - $contents = file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema.ser'); - $r = unserialize($contents); - if (!$r) { - $hash = sha1($contents); - trigger_error("Unserialization of configuration schema failed, sha1 of file was $hash", E_USER_ERROR); - } - return $r; - } - - /** - * Retrieves an instance of the application-wide configuration definition. - */ - public static function instance($prototype = null) { - if ($prototype !== null) { - HTMLPurifier_ConfigSchema::$singleton = $prototype; - } elseif (HTMLPurifier_ConfigSchema::$singleton === null || $prototype === true) { - HTMLPurifier_ConfigSchema::$singleton = HTMLPurifier_ConfigSchema::makeFromSerial(); - } - return HTMLPurifier_ConfigSchema::$singleton; - } - - /** - * Defines a directive for configuration - * @warning Will fail of directive's namespace is defined. - * @warning This method's signature is slightly different from the legacy - * define() static method! Beware! - * @param $namespace Namespace the directive is in - * @param $name Key of directive - * @param $default Default value of directive - * @param $type Allowed type of the directive. See - * HTMLPurifier_DirectiveDef::$type for allowed values - * @param $allow_null Whether or not to allow null values - */ - public function add($key, $default, $type, $allow_null) { - $obj = new stdclass(); - $obj->type = is_int($type) ? $type : HTMLPurifier_VarParser::$types[$type]; - if ($allow_null) $obj->allow_null = true; - $this->info[$key] = $obj; - $this->defaults[$key] = $default; - $this->defaultPlist->set($key, $default); - } - - /** - * Defines a directive value alias. - * - * Directive value aliases are convenient for developers because it lets - * them set a directive to several values and get the same result. - * @param $namespace Directive's namespace - * @param $name Name of Directive - * @param $aliases Hash of aliased values to the real alias - */ - public function addValueAliases($key, $aliases) { - if (!isset($this->info[$key]->aliases)) { - $this->info[$key]->aliases = array(); - } - foreach ($aliases as $alias => $real) { - $this->info[$key]->aliases[$alias] = $real; - } - } - - /** - * Defines a set of allowed values for a directive. - * @warning This is slightly different from the corresponding static - * method definition. - * @param $namespace Namespace of directive - * @param $name Name of directive - * @param $allowed Lookup array of allowed values - */ - public function addAllowedValues($key, $allowed) { - $this->info[$key]->allowed = $allowed; - } - - /** - * Defines a directive alias for backwards compatibility - * @param $namespace - * @param $name Directive that will be aliased - * @param $new_namespace - * @param $new_name Directive that the alias will be to - */ - public function addAlias($key, $new_key) { - $obj = new stdclass; - $obj->key = $new_key; - $obj->isAlias = true; - $this->info[$key] = $obj; - } - - /** - * Replaces any stdclass that only has the type property with type integer. - */ - public function postProcess() { - foreach ($this->info as $key => $v) { - if (count((array) $v) == 1) { - $this->info[$key] = $v->type; - } elseif (count((array) $v) == 2 && isset($v->allow_null)) { - $this->info[$key] = -$v->type; - } - } - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php deleted file mode 100644 index c05668a70..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Builder/ConfigSchema.php +++ /dev/null @@ -1,44 +0,0 @@ -directives as $d) { - $schema->add( - $d->id->key, - $d->default, - $d->type, - $d->typeAllowsNull - ); - if ($d->allowed !== null) { - $schema->addAllowedValues( - $d->id->key, - $d->allowed - ); - } - foreach ($d->aliases as $alias) { - $schema->addAlias( - $alias->key, - $d->id->key - ); - } - if ($d->valueAliases !== null) { - $schema->addValueAliases( - $d->id->key, - $d->valueAliases - ); - } - } - $schema->postProcess(); - return $schema; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Builder/Xml.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Builder/Xml.php deleted file mode 100644 index 244561a37..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Builder/Xml.php +++ /dev/null @@ -1,106 +0,0 @@ -startElement('div'); - - $purifier = HTMLPurifier::getInstance(); - $html = $purifier->purify($html); - $this->writeAttribute('xmlns', 'http://www.w3.org/1999/xhtml'); - $this->writeRaw($html); - - $this->endElement(); // div - } - - protected function export($var) { - if ($var === array()) return 'array()'; - return var_export($var, true); - } - - public function build($interchange) { - // global access, only use as last resort - $this->interchange = $interchange; - - $this->setIndent(true); - $this->startDocument('1.0', 'UTF-8'); - $this->startElement('configdoc'); - $this->writeElement('title', $interchange->name); - - foreach ($interchange->directives as $directive) { - $this->buildDirective($directive); - } - - if ($this->namespace) $this->endElement(); // namespace - - $this->endElement(); // configdoc - $this->flush(); - } - - public function buildDirective($directive) { - - // Kludge, although I suppose having a notion of a "root namespace" - // certainly makes things look nicer when documentation is built. - // Depends on things being sorted. - if (!$this->namespace || $this->namespace !== $directive->id->getRootNamespace()) { - if ($this->namespace) $this->endElement(); // namespace - $this->namespace = $directive->id->getRootNamespace(); - $this->startElement('namespace'); - $this->writeAttribute('id', $this->namespace); - $this->writeElement('name', $this->namespace); - } - - $this->startElement('directive'); - $this->writeAttribute('id', $directive->id->toString()); - - $this->writeElement('name', $directive->id->getDirective()); - - $this->startElement('aliases'); - foreach ($directive->aliases as $alias) $this->writeElement('alias', $alias->toString()); - $this->endElement(); // aliases - - $this->startElement('constraints'); - if ($directive->version) $this->writeElement('version', $directive->version); - $this->startElement('type'); - if ($directive->typeAllowsNull) $this->writeAttribute('allow-null', 'yes'); - $this->text($directive->type); - $this->endElement(); // type - if ($directive->allowed) { - $this->startElement('allowed'); - foreach ($directive->allowed as $value => $x) $this->writeElement('value', $value); - $this->endElement(); // allowed - } - $this->writeElement('default', $this->export($directive->default)); - $this->writeAttribute('xml:space', 'preserve'); - if ($directive->external) { - $this->startElement('external'); - foreach ($directive->external as $project) $this->writeElement('project', $project); - $this->endElement(); - } - $this->endElement(); // constraints - - if ($directive->deprecatedVersion) { - $this->startElement('deprecated'); - $this->writeElement('version', $directive->deprecatedVersion); - $this->writeElement('use', $directive->deprecatedUse->toString()); - $this->endElement(); // deprecated - } - - $this->startElement('description'); - $this->writeHTMLDiv($directive->description); - $this->endElement(); // description - - $this->endElement(); // directive - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Exception.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Exception.php deleted file mode 100644 index 2671516c5..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Exception.php +++ /dev/null @@ -1,11 +0,0 @@ - array(directive info) - */ - public $directives = array(); - - /** - * Adds a directive array to $directives - */ - public function addDirective($directive) { - if (isset($this->directives[$i = $directive->id->toString()])) { - throw new HTMLPurifier_ConfigSchema_Exception("Cannot redefine directive '$i'"); - } - $this->directives[$i] = $directive; - } - - /** - * Convenience function to perform standard validation. Throws exception - * on failed validation. - */ - public function validate() { - $validator = new HTMLPurifier_ConfigSchema_Validator(); - return $validator->validate($this); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Interchange/Directive.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Interchange/Directive.php deleted file mode 100644 index ac8be0d97..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Interchange/Directive.php +++ /dev/null @@ -1,77 +0,0 @@ - true). - * Null if all values are allowed. - */ - public $allowed; - - /** - * List of aliases for the directive, - * e.g. array(new HTMLPurifier_ConfigSchema_Interchange_Id('Ns', 'Dir'))). - */ - public $aliases = array(); - - /** - * Hash of value aliases, e.g. array('alt' => 'real'). Null if value - * aliasing is disabled (necessary for non-scalar types). - */ - public $valueAliases; - - /** - * Version of HTML Purifier the directive was introduced, e.g. '1.3.1'. - * Null if the directive has always existed. - */ - public $version; - - /** - * ID of directive that supercedes this old directive, is an instance - * of HTMLPurifier_ConfigSchema_Interchange_Id. Null if not deprecated. - */ - public $deprecatedUse; - - /** - * Version of HTML Purifier this directive was deprecated. Null if not - * deprecated. - */ - public $deprecatedVersion; - - /** - * List of external projects this directive depends on, e.g. array('CSSTidy'). - */ - public $external = array(); - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Interchange/Id.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Interchange/Id.php deleted file mode 100644 index b9b3c6f5c..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Interchange/Id.php +++ /dev/null @@ -1,37 +0,0 @@ -key = $key; - } - - /** - * @warning This is NOT magic, to ensure that people don't abuse SPL and - * cause problems for PHP 5.0 support. - */ - public function toString() { - return $this->key; - } - - public function getRootNamespace() { - return substr($this->key, 0, strpos($this->key, ".")); - } - - public function getDirective() { - return substr($this->key, strpos($this->key, ".") + 1); - } - - public static function make($id) { - return new HTMLPurifier_ConfigSchema_Interchange_Id($id); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/InterchangeBuilder.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/InterchangeBuilder.php deleted file mode 100644 index 785b72ce8..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/InterchangeBuilder.php +++ /dev/null @@ -1,180 +0,0 @@ -varParser = $varParser ? $varParser : new HTMLPurifier_VarParser_Native(); - } - - public static function buildFromDirectory($dir = null) { - $builder = new HTMLPurifier_ConfigSchema_InterchangeBuilder(); - $interchange = new HTMLPurifier_ConfigSchema_Interchange(); - return $builder->buildDir($interchange, $dir); - } - - public function buildDir($interchange, $dir = null) { - if (!$dir) $dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier/ConfigSchema/schema'; - if (file_exists($dir . '/info.ini')) { - $info = parse_ini_file($dir . '/info.ini'); - $interchange->name = $info['name']; - } - - $files = array(); - $dh = opendir($dir); - while (false !== ($file = readdir($dh))) { - if (!$file || $file[0] == '.' || strrchr($file, '.') !== '.txt') { - continue; - } - $files[] = $file; - } - closedir($dh); - - sort($files); - foreach ($files as $file) { - $this->buildFile($interchange, $dir . '/' . $file); - } - - return $interchange; - } - - public function buildFile($interchange, $file) { - $parser = new HTMLPurifier_StringHashParser(); - $this->build( - $interchange, - new HTMLPurifier_StringHash( $parser->parseFile($file) ) - ); - } - - /** - * Builds an interchange object based on a hash. - * @param $interchange HTMLPurifier_ConfigSchema_Interchange object to build - * @param $hash HTMLPurifier_ConfigSchema_StringHash source data - */ - public function build($interchange, $hash) { - if (!$hash instanceof HTMLPurifier_StringHash) { - $hash = new HTMLPurifier_StringHash($hash); - } - if (!isset($hash['ID'])) { - throw new HTMLPurifier_ConfigSchema_Exception('Hash does not have any ID'); - } - if (strpos($hash['ID'], '.') === false) { - if (count($hash) == 2 && isset($hash['DESCRIPTION'])) { - $hash->offsetGet('DESCRIPTION'); // prevent complaining - } else { - throw new HTMLPurifier_ConfigSchema_Exception('All directives must have a namespace'); - } - } else { - $this->buildDirective($interchange, $hash); - } - $this->_findUnused($hash); - } - - public function buildDirective($interchange, $hash) { - $directive = new HTMLPurifier_ConfigSchema_Interchange_Directive(); - - // These are required elements: - $directive->id = $this->id($hash->offsetGet('ID')); - $id = $directive->id->toString(); // convenience - - if (isset($hash['TYPE'])) { - $type = explode('/', $hash->offsetGet('TYPE')); - if (isset($type[1])) $directive->typeAllowsNull = true; - $directive->type = $type[0]; - } else { - throw new HTMLPurifier_ConfigSchema_Exception("TYPE in directive hash '$id' not defined"); - } - - if (isset($hash['DEFAULT'])) { - try { - $directive->default = $this->varParser->parse($hash->offsetGet('DEFAULT'), $directive->type, $directive->typeAllowsNull); - } catch (HTMLPurifier_VarParserException $e) { - throw new HTMLPurifier_ConfigSchema_Exception($e->getMessage() . " in DEFAULT in directive hash '$id'"); - } - } - - if (isset($hash['DESCRIPTION'])) { - $directive->description = $hash->offsetGet('DESCRIPTION'); - } - - if (isset($hash['ALLOWED'])) { - $directive->allowed = $this->lookup($this->evalArray($hash->offsetGet('ALLOWED'))); - } - - if (isset($hash['VALUE-ALIASES'])) { - $directive->valueAliases = $this->evalArray($hash->offsetGet('VALUE-ALIASES')); - } - - if (isset($hash['ALIASES'])) { - $raw_aliases = trim($hash->offsetGet('ALIASES')); - $aliases = preg_split('/\s*,\s*/', $raw_aliases); - foreach ($aliases as $alias) { - $directive->aliases[] = $this->id($alias); - } - } - - if (isset($hash['VERSION'])) { - $directive->version = $hash->offsetGet('VERSION'); - } - - if (isset($hash['DEPRECATED-USE'])) { - $directive->deprecatedUse = $this->id($hash->offsetGet('DEPRECATED-USE')); - } - - if (isset($hash['DEPRECATED-VERSION'])) { - $directive->deprecatedVersion = $hash->offsetGet('DEPRECATED-VERSION'); - } - - if (isset($hash['EXTERNAL'])) { - $directive->external = preg_split('/\s*,\s*/', trim($hash->offsetGet('EXTERNAL'))); - } - - $interchange->addDirective($directive); - } - - /** - * Evaluates an array PHP code string without array() wrapper - */ - protected function evalArray($contents) { - return eval('return array('. $contents .');'); - } - - /** - * Converts an array list into a lookup array. - */ - protected function lookup($array) { - $ret = array(); - foreach ($array as $val) $ret[$val] = true; - return $ret; - } - - /** - * Convenience function that creates an HTMLPurifier_ConfigSchema_Interchange_Id - * object based on a string Id. - */ - protected function id($id) { - return HTMLPurifier_ConfigSchema_Interchange_Id::make($id); - } - - /** - * Triggers errors for any unused keys passed in the hash; such keys - * may indicate typos, missing values, etc. - * @param $hash Instance of ConfigSchema_StringHash to check. - */ - protected function _findUnused($hash) { - $accessed = $hash->getAccessed(); - foreach ($hash as $k => $v) { - if (!isset($accessed[$k])) { - trigger_error("String hash key '$k' not used by builder", E_USER_NOTICE); - } - } - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Validator.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Validator.php deleted file mode 100644 index f374f6a02..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/Validator.php +++ /dev/null @@ -1,206 +0,0 @@ -parser = new HTMLPurifier_VarParser(); - } - - /** - * Validates a fully-formed interchange object. Throws an - * HTMLPurifier_ConfigSchema_Exception if there's a problem. - */ - public function validate($interchange) { - $this->interchange = $interchange; - $this->aliases = array(); - // PHP is a bit lax with integer <=> string conversions in - // arrays, so we don't use the identical !== comparison - foreach ($interchange->directives as $i => $directive) { - $id = $directive->id->toString(); - if ($i != $id) $this->error(false, "Integrity violation: key '$i' does not match internal id '$id'"); - $this->validateDirective($directive); - } - return true; - } - - /** - * Validates a HTMLPurifier_ConfigSchema_Interchange_Id object. - */ - public function validateId($id) { - $id_string = $id->toString(); - $this->context[] = "id '$id_string'"; - if (!$id instanceof HTMLPurifier_ConfigSchema_Interchange_Id) { - // handled by InterchangeBuilder - $this->error(false, 'is not an instance of HTMLPurifier_ConfigSchema_Interchange_Id'); - } - // keys are now unconstrained (we might want to narrow down to A-Za-z0-9.) - // we probably should check that it has at least one namespace - $this->with($id, 'key') - ->assertNotEmpty() - ->assertIsString(); // implicit assertIsString handled by InterchangeBuilder - array_pop($this->context); - } - - /** - * Validates a HTMLPurifier_ConfigSchema_Interchange_Directive object. - */ - public function validateDirective($d) { - $id = $d->id->toString(); - $this->context[] = "directive '$id'"; - $this->validateId($d->id); - - $this->with($d, 'description') - ->assertNotEmpty(); - - // BEGIN - handled by InterchangeBuilder - $this->with($d, 'type') - ->assertNotEmpty(); - $this->with($d, 'typeAllowsNull') - ->assertIsBool(); - try { - // This also tests validity of $d->type - $this->parser->parse($d->default, $d->type, $d->typeAllowsNull); - } catch (HTMLPurifier_VarParserException $e) { - $this->error('default', 'had error: ' . $e->getMessage()); - } - // END - handled by InterchangeBuilder - - if (!is_null($d->allowed) || !empty($d->valueAliases)) { - // allowed and valueAliases require that we be dealing with - // strings, so check for that early. - $d_int = HTMLPurifier_VarParser::$types[$d->type]; - if (!isset(HTMLPurifier_VarParser::$stringTypes[$d_int])) { - $this->error('type', 'must be a string type when used with allowed or value aliases'); - } - } - - $this->validateDirectiveAllowed($d); - $this->validateDirectiveValueAliases($d); - $this->validateDirectiveAliases($d); - - array_pop($this->context); - } - - /** - * Extra validation if $allowed member variable of - * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. - */ - public function validateDirectiveAllowed($d) { - if (is_null($d->allowed)) return; - $this->with($d, 'allowed') - ->assertNotEmpty() - ->assertIsLookup(); // handled by InterchangeBuilder - if (is_string($d->default) && !isset($d->allowed[$d->default])) { - $this->error('default', 'must be an allowed value'); - } - $this->context[] = 'allowed'; - foreach ($d->allowed as $val => $x) { - if (!is_string($val)) $this->error("value $val", 'must be a string'); - } - array_pop($this->context); - } - - /** - * Extra validation if $valueAliases member variable of - * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. - */ - public function validateDirectiveValueAliases($d) { - if (is_null($d->valueAliases)) return; - $this->with($d, 'valueAliases') - ->assertIsArray(); // handled by InterchangeBuilder - $this->context[] = 'valueAliases'; - foreach ($d->valueAliases as $alias => $real) { - if (!is_string($alias)) $this->error("alias $alias", 'must be a string'); - if (!is_string($real)) $this->error("alias target $real from alias '$alias'", 'must be a string'); - if ($alias === $real) { - $this->error("alias '$alias'", "must not be an alias to itself"); - } - } - if (!is_null($d->allowed)) { - foreach ($d->valueAliases as $alias => $real) { - if (isset($d->allowed[$alias])) { - $this->error("alias '$alias'", 'must not be an allowed value'); - } elseif (!isset($d->allowed[$real])) { - $this->error("alias '$alias'", 'must be an alias to an allowed value'); - } - } - } - array_pop($this->context); - } - - /** - * Extra validation if $aliases member variable of - * HTMLPurifier_ConfigSchema_Interchange_Directive is defined. - */ - public function validateDirectiveAliases($d) { - $this->with($d, 'aliases') - ->assertIsArray(); // handled by InterchangeBuilder - $this->context[] = 'aliases'; - foreach ($d->aliases as $alias) { - $this->validateId($alias); - $s = $alias->toString(); - if (isset($this->interchange->directives[$s])) { - $this->error("alias '$s'", 'collides with another directive'); - } - if (isset($this->aliases[$s])) { - $other_directive = $this->aliases[$s]; - $this->error("alias '$s'", "collides with alias for directive '$other_directive'"); - } - $this->aliases[$s] = $d->id->toString(); - } - array_pop($this->context); - } - - // protected helper functions - - /** - * Convenience function for generating HTMLPurifier_ConfigSchema_ValidatorAtom - * for validating simple member variables of objects. - */ - protected function with($obj, $member) { - return new HTMLPurifier_ConfigSchema_ValidatorAtom($this->getFormattedContext(), $obj, $member); - } - - /** - * Emits an error, providing helpful context. - */ - protected function error($target, $msg) { - if ($target !== false) $prefix = ucfirst($target) . ' in ' . $this->getFormattedContext(); - else $prefix = ucfirst($this->getFormattedContext()); - throw new HTMLPurifier_ConfigSchema_Exception(trim($prefix . ' ' . $msg)); - } - - /** - * Returns a formatted context string. - */ - protected function getFormattedContext() { - return implode(' in ', array_reverse($this->context)); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/ValidatorAtom.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/ValidatorAtom.php deleted file mode 100644 index b95aea18c..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/ValidatorAtom.php +++ /dev/null @@ -1,66 +0,0 @@ -context = $context; - $this->obj = $obj; - $this->member = $member; - $this->contents =& $obj->$member; - } - - public function assertIsString() { - if (!is_string($this->contents)) $this->error('must be a string'); - return $this; - } - - public function assertIsBool() { - if (!is_bool($this->contents)) $this->error('must be a boolean'); - return $this; - } - - public function assertIsArray() { - if (!is_array($this->contents)) $this->error('must be an array'); - return $this; - } - - public function assertNotNull() { - if ($this->contents === null) $this->error('must not be null'); - return $this; - } - - public function assertAlnum() { - $this->assertIsString(); - if (!ctype_alnum($this->contents)) $this->error('must be alphanumeric'); - return $this; - } - - public function assertNotEmpty() { - if (empty($this->contents)) $this->error('must not be empty'); - return $this; - } - - public function assertIsLookup() { - $this->assertIsArray(); - foreach ($this->contents as $v) { - if ($v !== true) $this->error('must be a lookup array'); - } - return $this; - } - - protected function error($msg) { - throw new HTMLPurifier_ConfigSchema_Exception(ucfirst($this->member) . ' in ' . $this->context . ' ' . $msg); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema.ser b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema.ser deleted file mode 100644 index fa0bacb94..000000000 Binary files a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema.ser and /dev/null differ diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt deleted file mode 100644 index 0517fed0a..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedClasses.txt +++ /dev/null @@ -1,8 +0,0 @@ -Attr.AllowedClasses -TYPE: lookup/null -VERSION: 4.0.0 -DEFAULT: null ---DESCRIPTION-- -List of allowed class values in the class attribute. By default, this is null, -which means all classes are allowed. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt deleted file mode 100644 index 249edd647..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedFrameTargets.txt +++ /dev/null @@ -1,12 +0,0 @@ -Attr.AllowedFrameTargets -TYPE: lookup -DEFAULT: array() ---DESCRIPTION-- -Lookup table of all allowed link frame targets. Some commonly used link -targets include _blank, _self, _parent and _top. Values should be -lowercase, as validation will be done in a case-sensitive manner despite -W3C's recommendation. XHTML 1.0 Strict does not permit the target attribute -so this directive will have no effect in that doctype. XHTML 1.1 does not -enable the Target module by default, you will have to manually enable it -(see the module documentation for more details.) ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt deleted file mode 100644 index 9a8fa6a2e..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRel.txt +++ /dev/null @@ -1,9 +0,0 @@ -Attr.AllowedRel -TYPE: lookup -VERSION: 1.6.0 -DEFAULT: array() ---DESCRIPTION-- -List of allowed forward document relationships in the rel attribute. Common -values may be nofollow or print. By default, this is empty, meaning that no -document relationships are allowed. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt deleted file mode 100644 index b01788348..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.AllowedRev.txt +++ /dev/null @@ -1,9 +0,0 @@ -Attr.AllowedRev -TYPE: lookup -VERSION: 1.6.0 -DEFAULT: array() ---DESCRIPTION-- -List of allowed reverse document relationships in the rev attribute. This -attribute is a bit of an edge-case; if you don't know what it is for, stay -away. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt deleted file mode 100644 index e774b823b..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.ClassUseCDATA.txt +++ /dev/null @@ -1,19 +0,0 @@ -Attr.ClassUseCDATA -TYPE: bool/null -DEFAULT: null -VERSION: 4.0.0 ---DESCRIPTION-- -If null, class will auto-detect the doctype and, if matching XHTML 1.1 or -XHTML 2.0, will use the restrictive NMTOKENS specification of class. Otherwise, -it will use a relaxed CDATA definition. If true, the relaxed CDATA definition -is forced; if false, the NMTOKENS definition is forced. To get behavior -of HTML Purifier prior to 4.0.0, set this directive to false. - -Some rational behind the auto-detection: -in previous versions of HTML Purifier, it was assumed that the form of -class was NMTOKENS, as specified by the XHTML Modularization (representing -XHTML 1.1 and XHTML 2.0). The DTDs for HTML 4.01 and XHTML 1.0, however -specify class as CDATA. HTML 5 effectively defines it as CDATA, but -with the additional constraint that each name should be unique (this is not -explicitly outlined in previous specifications). ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt deleted file mode 100644 index 533165e17..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultImageAlt.txt +++ /dev/null @@ -1,11 +0,0 @@ -Attr.DefaultImageAlt -TYPE: string/null -DEFAULT: null -VERSION: 3.2.0 ---DESCRIPTION-- -This is the content of the alt tag of an image if the user had not -previously specified an alt attribute. This applies to all images without -a valid alt attribute, as opposed to %Attr.DefaultInvalidImageAlt, which -only applies to invalid images, and overrides in the case of an invalid image. -Default behavior with null is to use the basename of the src tag for the alt. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt deleted file mode 100644 index 9eb7e3846..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImage.txt +++ /dev/null @@ -1,9 +0,0 @@ -Attr.DefaultInvalidImage -TYPE: string -DEFAULT: '' ---DESCRIPTION-- -This is the default image an img tag will be pointed to if it does not have -a valid src attribute. In future versions, we may allow the image tag to -be removed completely, but due to design issues, this is not possible right -now. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt deleted file mode 100644 index 2f17bf477..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultInvalidImageAlt.txt +++ /dev/null @@ -1,8 +0,0 @@ -Attr.DefaultInvalidImageAlt -TYPE: string -DEFAULT: 'Invalid image' ---DESCRIPTION-- -This is the content of the alt tag of an invalid image if the user had not -previously specified an alt attribute. It has no effect when the image is -valid but there was no alt attribute present. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt deleted file mode 100644 index 52654b53a..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.DefaultTextDir.txt +++ /dev/null @@ -1,10 +0,0 @@ -Attr.DefaultTextDir -TYPE: string -DEFAULT: 'ltr' ---DESCRIPTION-- -Defines the default text direction (ltr or rtl) of the document being -parsed. This generally is the same as the value of the dir attribute in -HTML, or ltr if that is not specified. ---ALLOWED-- -'ltr', 'rtl' ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt deleted file mode 100644 index 6440d2103..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.EnableID.txt +++ /dev/null @@ -1,16 +0,0 @@ -Attr.EnableID -TYPE: bool -DEFAULT: false -VERSION: 1.2.0 ---DESCRIPTION-- -Allows the ID attribute in HTML. This is disabled by default due to the -fact that without proper configuration user input can easily break the -validation of a webpage by specifying an ID that is already on the -surrounding HTML. If you don't mind throwing caution to the wind, enable -this directive, but I strongly recommend you also consider blacklisting IDs -you use (%Attr.IDBlacklist) or prefixing all user supplied IDs -(%Attr.IDPrefix). When set to true HTML Purifier reverts to the behavior of -pre-1.2.0 versions. ---ALIASES-- -HTML.EnableAttrID ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt deleted file mode 100644 index f31d226f5..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.ForbiddenClasses.txt +++ /dev/null @@ -1,8 +0,0 @@ -Attr.ForbiddenClasses -TYPE: lookup -VERSION: 4.0.0 -DEFAULT: array() ---DESCRIPTION-- -List of forbidden class values in the class attribute. By default, this is -empty, which means that no classes are forbidden. See also %Attr.AllowedClasses. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt deleted file mode 100644 index 5f2b5e3d2..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklist.txt +++ /dev/null @@ -1,5 +0,0 @@ -Attr.IDBlacklist -TYPE: list -DEFAULT: array() -DESCRIPTION: Array of IDs not allowed in the document. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt deleted file mode 100644 index 6f5824586..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDBlacklistRegexp.txt +++ /dev/null @@ -1,9 +0,0 @@ -Attr.IDBlacklistRegexp -TYPE: string/null -VERSION: 1.6.0 -DEFAULT: NULL ---DESCRIPTION-- -PCRE regular expression to be matched against all IDs. If the expression is -matches, the ID is rejected. Use this with care: may cause significant -degradation. ID matching is done after all other validation. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt deleted file mode 100644 index cc49d43fd..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefix.txt +++ /dev/null @@ -1,12 +0,0 @@ -Attr.IDPrefix -TYPE: string -VERSION: 1.2.0 -DEFAULT: '' ---DESCRIPTION-- -String to prefix to IDs. If you have no idea what IDs your pages may use, -you may opt to simply add a prefix to all user-submitted ID attributes so -that they are still usable, but will not conflict with core page IDs. -Example: setting the directive to 'user_' will result in a user submitted -'foo' to become 'user_foo' Be sure to set %HTML.EnableAttrID to true -before using this. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt deleted file mode 100644 index 2c5924a7a..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Attr.IDPrefixLocal.txt +++ /dev/null @@ -1,14 +0,0 @@ -Attr.IDPrefixLocal -TYPE: string -VERSION: 1.2.0 -DEFAULT: '' ---DESCRIPTION-- -Temporary prefix for IDs used in conjunction with %Attr.IDPrefix. If you -need to allow multiple sets of user content on web page, you may need to -have a seperate prefix that changes with each iteration. This way, -seperately submitted user content displayed on the same page doesn't -clobber each other. Ideal values are unique identifiers for the content it -represents (i.e. the id of the row in the database). Be sure to add a -seperator (like an underscore) at the end. Warning: this directive will -not work unless %Attr.IDPrefix is set to a non-empty value! ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt deleted file mode 100644 index d5caa1bb9..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.AutoParagraph.txt +++ /dev/null @@ -1,31 +0,0 @@ -AutoFormat.AutoParagraph -TYPE: bool -VERSION: 2.0.1 -DEFAULT: false ---DESCRIPTION-- - -

    - This directive turns on auto-paragraphing, where double newlines are - converted in to paragraphs whenever possible. Auto-paragraphing: -

    -
      -
    • Always applies to inline elements or text in the root node,
    • -
    • Applies to inline elements or text with double newlines in nodes - that allow paragraph tags,
    • -
    • Applies to double newlines in paragraph tags
    • -
    -

    - p tags must be allowed for this directive to take effect. - We do not use br tags for paragraphing, as that is - semantically incorrect. -

    -

    - To prevent auto-paragraphing as a content-producer, refrain from using - double-newlines except to specify a new paragraph or in contexts where - it has special meaning (whitespace usually has no meaning except in - tags like pre, so this should not be difficult.) To prevent - the paragraphing of inline text adjacent to block elements, wrap them - in div tags (the behavior is slightly different outside of - the root node.) -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt deleted file mode 100644 index 2a476481a..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Custom.txt +++ /dev/null @@ -1,12 +0,0 @@ -AutoFormat.Custom -TYPE: list -VERSION: 2.0.1 -DEFAULT: array() ---DESCRIPTION-- - -

    - This directive can be used to add custom auto-format injectors. - Specify an array of injector names (class name minus the prefix) - or concrete implementations. Injector class must exist. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt deleted file mode 100644 index 663064a34..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.DisplayLinkURI.txt +++ /dev/null @@ -1,11 +0,0 @@ -AutoFormat.DisplayLinkURI -TYPE: bool -VERSION: 3.2.0 -DEFAULT: false ---DESCRIPTION-- -

    - This directive turns on the in-text display of URIs in <a> tags, and disables - those links. For example, example becomes - example (http://example.com). -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt deleted file mode 100644 index 3a48ba960..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.Linkify.txt +++ /dev/null @@ -1,12 +0,0 @@ -AutoFormat.Linkify -TYPE: bool -VERSION: 2.0.1 -DEFAULT: false ---DESCRIPTION-- - -

    - This directive turns on linkification, auto-linking http, ftp and - https URLs. a tags with the href attribute - must be allowed. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt deleted file mode 100644 index db58b1346..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.DocURL.txt +++ /dev/null @@ -1,12 +0,0 @@ -AutoFormat.PurifierLinkify.DocURL -TYPE: string -VERSION: 2.0.1 -DEFAULT: '#%s' -ALIASES: AutoFormatParam.PurifierLinkifyDocURL ---DESCRIPTION-- -

    - Location of configuration documentation to link to, let %s substitute - into the configuration's namespace and directive names sans the percent - sign. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt deleted file mode 100644 index 7996488be..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.PurifierLinkify.txt +++ /dev/null @@ -1,12 +0,0 @@ -AutoFormat.PurifierLinkify -TYPE: bool -VERSION: 2.0.1 -DEFAULT: false ---DESCRIPTION-- - -

    - Internal auto-formatter that converts configuration directives in - syntax %Namespace.Directive to links. a tags - with the href attribute must be allowed. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt deleted file mode 100644 index 35c393b4e..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions.txt +++ /dev/null @@ -1,11 +0,0 @@ -AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions -TYPE: lookup -VERSION: 4.0.0 -DEFAULT: array('td' => true, 'th' => true) ---DESCRIPTION-- -

    - When %AutoFormat.RemoveEmpty and %AutoFormat.RemoveEmpty.RemoveNbsp - are enabled, this directive defines what HTML elements should not be - removede if they have only a non-breaking space in them. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt deleted file mode 100644 index ca17eb1dc..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.RemoveNbsp.txt +++ /dev/null @@ -1,15 +0,0 @@ -AutoFormat.RemoveEmpty.RemoveNbsp -TYPE: bool -VERSION: 4.0.0 -DEFAULT: false ---DESCRIPTION-- -

    - When enabled, HTML Purifier will treat any elements that contain only - non-breaking spaces as well as regular whitespace as empty, and remove - them when %AutoForamt.RemoveEmpty is enabled. -

    -

    - See %AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions for a list of elements - that don't have this behavior applied to them. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt deleted file mode 100644 index 34657ba47..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveEmpty.txt +++ /dev/null @@ -1,46 +0,0 @@ -AutoFormat.RemoveEmpty -TYPE: bool -VERSION: 3.2.0 -DEFAULT: false ---DESCRIPTION-- -

    - When enabled, HTML Purifier will attempt to remove empty elements that - contribute no semantic information to the document. The following types - of nodes will be removed: -

    -
    • - Tags with no attributes and no content, and that are not empty - elements (remove <a></a> but not - <br />), and -
    • -
    • - Tags with no content, except for:
        -
      • The colgroup element, or
      • -
      • - Elements with the id or name attribute, - when those attributes are permitted on those elements. -
      • -
    • -
    -

    - Please be very careful when using this functionality; while it may not - seem that empty elements contain useful information, they can alter the - layout of a document given appropriate styling. This directive is most - useful when you are processing machine-generated HTML, please avoid using - it on regular user HTML. -

    -

    - Elements that contain only whitespace will be treated as empty. Non-breaking - spaces, however, do not count as whitespace. See - %AutoFormat.RemoveEmpty.RemoveNbsp for alternate behavior. -

    -

    - This algorithm is not perfect; you may still notice some empty tags, - particularly if a node had elements, but those elements were later removed - because they were not permitted in that context, or tags that, after - being auto-closed by another tag, where empty. This is for safety reasons - to prevent clever code from breaking validation. The general rule of thumb: - if a tag looked empty on the way in, it will get removed; if HTML Purifier - made it empty, it will stay. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt deleted file mode 100644 index dde990ab2..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/AutoFormat.RemoveSpansWithoutAttributes.txt +++ /dev/null @@ -1,11 +0,0 @@ -AutoFormat.RemoveSpansWithoutAttributes -TYPE: bool -VERSION: 4.0.1 -DEFAULT: false ---DESCRIPTION-- -

    - This directive causes span tags without any attributes - to be removed. It will also remove spans that had all attributes - removed during processing. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt deleted file mode 100644 index b324608f7..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowImportant.txt +++ /dev/null @@ -1,8 +0,0 @@ -CSS.AllowImportant -TYPE: bool -DEFAULT: false -VERSION: 3.1.0 ---DESCRIPTION-- -This parameter determines whether or not !important cascade modifiers should -be allowed in user CSS. If false, !important will stripped. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt deleted file mode 100644 index 748be0eec..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowTricky.txt +++ /dev/null @@ -1,11 +0,0 @@ -CSS.AllowTricky -TYPE: bool -DEFAULT: false -VERSION: 3.1.0 ---DESCRIPTION-- -This parameter determines whether or not to allow "tricky" CSS properties and -values. Tricky CSS properties/values can drastically modify page layout or -be used for deceptive practices but do not directly constitute a security risk. -For example, display:none; is considered a tricky property that -will only be allowed if this directive is set to true. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt deleted file mode 100644 index 3fd465406..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedFonts.txt +++ /dev/null @@ -1,12 +0,0 @@ -CSS.AllowedFonts -TYPE: lookup/null -VERSION: 4.3.0 -DEFAULT: NULL ---DESCRIPTION-- -

    - Allows you to manually specify a set of allowed fonts. If - NULL, all fonts are allowed. This directive - affects generic names (serif, sans-serif, monospace, cursive, - fantasy) as well as specific font families. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt deleted file mode 100644 index 460112ebe..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.AllowedProperties.txt +++ /dev/null @@ -1,18 +0,0 @@ -CSS.AllowedProperties -TYPE: lookup/null -VERSION: 3.1.0 -DEFAULT: NULL ---DESCRIPTION-- - -

    - If HTML Purifier's style attributes set is unsatisfactory for your needs, - you can overload it with your own list of tags to allow. Note that this - method is subtractive: it does its job by taking away from HTML Purifier - usual feature set, so you cannot add an attribute that HTML Purifier never - supported in the first place. -

    -

    - Warning: If another directive conflicts with the - elements here, that directive will win and override. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt deleted file mode 100644 index 5cb7dda3b..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.DefinitionRev.txt +++ /dev/null @@ -1,11 +0,0 @@ -CSS.DefinitionRev -TYPE: int -VERSION: 2.0.0 -DEFAULT: 1 ---DESCRIPTION-- - -

    - Revision identifier for your custom definition. See - %HTML.DefinitionRev for details. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt deleted file mode 100644 index f1f5c5f12..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.ForbiddenProperties.txt +++ /dev/null @@ -1,13 +0,0 @@ -CSS.ForbiddenProperties -TYPE: lookup -VERSION: 4.2.0 -DEFAULT: array() ---DESCRIPTION-- -

    - This is the logical inverse of %CSS.AllowedProperties, and it will - override that directive or any other directive. If possible, - %CSS.AllowedProperties is recommended over this directive, - because it can sometimes be difficult to tell whether or not you've - forbidden all of the CSS properties you truly would like to disallow. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt deleted file mode 100644 index 7a3291470..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.MaxImgLength.txt +++ /dev/null @@ -1,16 +0,0 @@ -CSS.MaxImgLength -TYPE: string/null -DEFAULT: '1200px' -VERSION: 3.1.1 ---DESCRIPTION-- -

    - This parameter sets the maximum allowed length on img tags, - effectively the width and height properties. - Only absolute units of measurement (in, pt, pc, mm, cm) and pixels (px) are allowed. This is - in place to prevent imagecrash attacks, disable with null at your own risk. - This directive is similar to %HTML.MaxImgLength, and both should be - concurrently edited, although there are - subtle differences in the input format (the CSS max is a number with - a unit). -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt deleted file mode 100644 index 148eedb8b..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.Proprietary.txt +++ /dev/null @@ -1,10 +0,0 @@ -CSS.Proprietary -TYPE: bool -VERSION: 3.0.0 -DEFAULT: false ---DESCRIPTION-- - -

    - Whether or not to allow safe, proprietary CSS values. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt deleted file mode 100644 index e733a61e8..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/CSS.Trusted.txt +++ /dev/null @@ -1,9 +0,0 @@ -CSS.Trusted -TYPE: bool -VERSION: 4.2.1 -DEFAULT: false ---DESCRIPTION-- -Indicates whether or not the user's CSS input is trusted or not. If the -input is trusted, a more expansive set of allowed properties. See -also %HTML.Trusted. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt deleted file mode 100644 index c486724c8..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Cache.DefinitionImpl.txt +++ /dev/null @@ -1,14 +0,0 @@ -Cache.DefinitionImpl -TYPE: string/null -VERSION: 2.0.0 -DEFAULT: 'Serializer' ---DESCRIPTION-- - -This directive defines which method to use when caching definitions, -the complex data-type that makes HTML Purifier tick. Set to null -to disable caching (not recommended, as you will see a definite -performance degradation). - ---ALIASES-- -Core.DefinitionCache ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt deleted file mode 100644 index 54036507d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPath.txt +++ /dev/null @@ -1,13 +0,0 @@ -Cache.SerializerPath -TYPE: string/null -VERSION: 2.0.0 -DEFAULT: NULL ---DESCRIPTION-- - -

    - Absolute path with no trailing slash to store serialized definitions in. - Default is within the - HTML Purifier library inside DefinitionCache/Serializer. This - path must be writable by the webserver. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt deleted file mode 100644 index b2b83d9ab..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Cache.SerializerPermissions.txt +++ /dev/null @@ -1,11 +0,0 @@ -Cache.SerializerPermissions -TYPE: int -VERSION: 4.3.0 -DEFAULT: 0755 ---DESCRIPTION-- - -

    - Directory permissions of the files and directories created inside - the DefinitionCache/Serializer or other custom serializer path. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt deleted file mode 100644 index 568cbf3b3..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.AggressivelyFixLt.txt +++ /dev/null @@ -1,18 +0,0 @@ -Core.AggressivelyFixLt -TYPE: bool -VERSION: 2.1.0 -DEFAULT: true ---DESCRIPTION-- -

    - This directive enables aggressive pre-filter fixes HTML Purifier can - perform in order to ensure that open angled-brackets do not get killed - during parsing stage. Enabling this will result in two preg_replace_callback - calls and at least two preg_replace calls for every HTML document parsed; - if your users make very well-formed HTML, you can set this directive false. - This has no effect when DirectLex is used. -

    -

    - Notice: This directive's default turned from false to true - in HTML Purifier 3.2.0. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt deleted file mode 100644 index d7317911f..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.CollectErrors.txt +++ /dev/null @@ -1,12 +0,0 @@ -Core.CollectErrors -TYPE: bool -VERSION: 2.0.0 -DEFAULT: false ---DESCRIPTION-- - -Whether or not to collect errors found while filtering the document. This -is a useful way to give feedback to your users. Warning: -Currently this feature is very patchy and experimental, with lots of -possible error messages not yet implemented. It will not cause any -problems, but it may not help your users either. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt deleted file mode 100644 index c572c14ec..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.ColorKeywords.txt +++ /dev/null @@ -1,29 +0,0 @@ -Core.ColorKeywords -TYPE: hash -VERSION: 2.0.0 ---DEFAULT-- -array ( - 'maroon' => '#800000', - 'red' => '#FF0000', - 'orange' => '#FFA500', - 'yellow' => '#FFFF00', - 'olive' => '#808000', - 'purple' => '#800080', - 'fuchsia' => '#FF00FF', - 'white' => '#FFFFFF', - 'lime' => '#00FF00', - 'green' => '#008000', - 'navy' => '#000080', - 'blue' => '#0000FF', - 'aqua' => '#00FFFF', - 'teal' => '#008080', - 'black' => '#000000', - 'silver' => '#C0C0C0', - 'gray' => '#808080', -) ---DESCRIPTION-- - -Lookup array of color names to six digit hexadecimal number corresponding -to color, with preceding hash mark. Used when parsing colors. The lookup -is done in a case-insensitive manner. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt deleted file mode 100644 index 64b114fce..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.ConvertDocumentToFragment.txt +++ /dev/null @@ -1,14 +0,0 @@ -Core.ConvertDocumentToFragment -TYPE: bool -DEFAULT: true ---DESCRIPTION-- - -This parameter determines whether or not the filter should convert -input that is a full document with html and body tags to a fragment -of just the contents of a body tag. This parameter is simply something -HTML Purifier can do during an edge-case: for most inputs, this -processing is not necessary. - ---ALIASES-- -Core.AcceptFullDocuments ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt deleted file mode 100644 index 36f16e07e..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.DirectLexLineNumberSyncInterval.txt +++ /dev/null @@ -1,17 +0,0 @@ -Core.DirectLexLineNumberSyncInterval -TYPE: int -VERSION: 2.0.0 -DEFAULT: 0 ---DESCRIPTION-- - -

    - Specifies the number of tokens the DirectLex line number tracking - implementations should process before attempting to resyncronize the - current line count by manually counting all previous new-lines. When - at 0, this functionality is disabled. Lower values will decrease - performance, and this is only strictly necessary if the counting - algorithm is buggy (in which case you should report it as a bug). - This has no effect when %Core.MaintainLineNumbers is disabled or DirectLex is - not being used. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt deleted file mode 100644 index 1cd4c2c96..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.DisableExcludes.txt +++ /dev/null @@ -1,14 +0,0 @@ -Core.DisableExcludes -TYPE: bool -DEFAULT: false -VERSION: 4.5.0 ---DESCRIPTION-- -

    - This directive disables SGML-style exclusions, e.g. the exclusion of - <object> in any descendant of a - <pre> tag. Disabling excludes will allow some - invalid documents to pass through HTML Purifier, but HTML Purifier - will also be less likely to accidentally remove large documents during - processing. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt deleted file mode 100644 index ce243c35d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EnableIDNA.txt +++ /dev/null @@ -1,9 +0,0 @@ -Core.EnableIDNA -TYPE: bool -DEFAULT: false -VERSION: 4.4.0 ---DESCRIPTION-- -Allows international domain names in URLs. This configuration option -requires the PEAR Net_IDNA2 module to be installed. It operates by -punycoding any internationalized host names for maximum portability. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt deleted file mode 100644 index 8bfb47c3a..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.Encoding.txt +++ /dev/null @@ -1,15 +0,0 @@ -Core.Encoding -TYPE: istring -DEFAULT: 'utf-8' ---DESCRIPTION-- -If for some reason you are unable to convert all webpages to UTF-8, you can -use this directive as a stop-gap compatibility change to let HTML Purifier -deal with non UTF-8 input. This technique has notable deficiencies: -absolutely no characters outside of the selected character encoding will be -preserved, not even the ones that have been ampersand escaped (this is due -to a UTF-8 specific feature that automatically resolves all -entities), making it pretty useless for anything except the most I18N-blind -applications, although %Core.EscapeNonASCIICharacters offers fixes this -trouble with another tradeoff. This directive only accepts ISO-8859-1 if -iconv is not enabled. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt deleted file mode 100644 index 4d5b5055c..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidChildren.txt +++ /dev/null @@ -1,10 +0,0 @@ -Core.EscapeInvalidChildren -TYPE: bool -DEFAULT: false ---DESCRIPTION-- -When true, a child is found that is not allowed in the context of the -parent element will be transformed into text as if it were ASCII. When -false, that element and all internal tags will be dropped, though text will -be preserved. There is no option for dropping the element but preserving -child nodes. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt deleted file mode 100644 index a7a5b249b..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeInvalidTags.txt +++ /dev/null @@ -1,7 +0,0 @@ -Core.EscapeInvalidTags -TYPE: bool -DEFAULT: false ---DESCRIPTION-- -When true, invalid tags will be written back to the document as plain text. -Otherwise, they are silently dropped. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt deleted file mode 100644 index abb499948..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.EscapeNonASCIICharacters.txt +++ /dev/null @@ -1,13 +0,0 @@ -Core.EscapeNonASCIICharacters -TYPE: bool -VERSION: 1.4.0 -DEFAULT: false ---DESCRIPTION-- -This directive overcomes a deficiency in %Core.Encoding by blindly -converting all non-ASCII characters into decimal numeric entities before -converting it to its native encoding. This means that even characters that -can be expressed in the non-UTF-8 encoding will be entity-ized, which can -be a real downer for encodings like Big5. It also assumes that the ASCII -repetoire is available, although this is the case for almost all encodings. -Anyway, use UTF-8! ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt deleted file mode 100644 index 915391edb..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.HiddenElements.txt +++ /dev/null @@ -1,19 +0,0 @@ -Core.HiddenElements -TYPE: lookup ---DEFAULT-- -array ( - 'script' => true, - 'style' => true, -) ---DESCRIPTION-- - -

    - This directive is a lookup array of elements which should have their - contents removed when they are not allowed by the HTML definition. - For example, the contents of a script tag are not - normally shown in a document, so if script tags are to be removed, - their contents should be removed to. This is opposed to a b - tag, which defines some presentational changes but does not hide its - contents. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.Language.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.Language.txt deleted file mode 100644 index 233fca14f..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.Language.txt +++ /dev/null @@ -1,10 +0,0 @@ -Core.Language -TYPE: string -VERSION: 2.0.0 -DEFAULT: 'en' ---DESCRIPTION-- - -ISO 639 language code for localizable things in HTML Purifier to use, -which is mainly error reporting. There is currently only an English (en) -translation, so this directive is currently useless. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt deleted file mode 100644 index 8983e2cca..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.LexerImpl.txt +++ /dev/null @@ -1,34 +0,0 @@ -Core.LexerImpl -TYPE: mixed/null -VERSION: 2.0.0 -DEFAULT: NULL ---DESCRIPTION-- - -

    - This parameter determines what lexer implementation can be used. The - valid values are: -

    -
    -
    null
    -
    - Recommended, the lexer implementation will be auto-detected based on - your PHP-version and configuration. -
    -
    string lexer identifier
    -
    - This is a slim way of manually overridding the implementation. - Currently recognized values are: DOMLex (the default PHP5 -implementation) - and DirectLex (the default PHP4 implementation). Only use this if - you know what you are doing: usually, the auto-detection will - manage things for cases you aren't even aware of. -
    -
    object lexer instance
    -
    - Super-advanced: you can specify your own, custom, implementation that - implements the interface defined by HTMLPurifier_Lexer. - I may remove this option simply because I don't expect anyone - to use it. -
    -
    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt deleted file mode 100644 index eb841a759..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.MaintainLineNumbers.txt +++ /dev/null @@ -1,16 +0,0 @@ -Core.MaintainLineNumbers -TYPE: bool/null -VERSION: 2.0.0 -DEFAULT: NULL ---DESCRIPTION-- - -

    - If true, HTML Purifier will add line number information to all tokens. - This is useful when error reporting is turned on, but can result in - significant performance degradation and should not be used when - unnecessary. This directive must be used with the DirectLex lexer, - as the DOMLex lexer does not (yet) support this functionality. - If the value is null, an appropriate value will be selected based - on other configuration. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt deleted file mode 100644 index d77f5360d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.NormalizeNewlines.txt +++ /dev/null @@ -1,11 +0,0 @@ -Core.NormalizeNewlines -TYPE: bool -VERSION: 4.2.0 -DEFAULT: true ---DESCRIPTION-- -

    - Whether or not to normalize newlines to the operating - system default. When false, HTML Purifier - will attempt to preserve mixed newline files. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt deleted file mode 100644 index 4070c2a0d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveInvalidImg.txt +++ /dev/null @@ -1,12 +0,0 @@ -Core.RemoveInvalidImg -TYPE: bool -DEFAULT: true -VERSION: 1.3.0 ---DESCRIPTION-- - -

    - This directive enables pre-emptive URI checking in img - tags, as the attribute validation strategy is not authorized to - remove elements from the document. Revert to pre-1.3.0 behavior by setting to false. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt deleted file mode 100644 index 3397d9f71..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveProcessingInstructions.txt +++ /dev/null @@ -1,11 +0,0 @@ -Core.RemoveProcessingInstructions -TYPE: bool -VERSION: 4.2.0 -DEFAULT: false ---DESCRIPTION-- -Instead of escaping processing instructions in the form <? ... -?>, remove it out-right. This may be useful if the HTML -you are validating contains XML processing instruction gunk, however, -it can also be user-unfriendly for people attempting to post PHP -snippets. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt deleted file mode 100644 index a4cd966df..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Core.RemoveScriptContents.txt +++ /dev/null @@ -1,12 +0,0 @@ -Core.RemoveScriptContents -TYPE: bool/null -DEFAULT: NULL -VERSION: 2.0.0 -DEPRECATED-VERSION: 2.1.0 -DEPRECATED-USE: Core.HiddenElements ---DESCRIPTION-- -

    - This directive enables HTML Purifier to remove not only script tags - but all of their contents. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt deleted file mode 100644 index 3db50ef20..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.Custom.txt +++ /dev/null @@ -1,11 +0,0 @@ -Filter.Custom -TYPE: list -VERSION: 3.1.0 -DEFAULT: array() ---DESCRIPTION-- -

    - This directive can be used to add custom filters; it is nearly the - equivalent of the now deprecated HTMLPurifier->addFilter() - method. Specify an array of concrete implementations. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt deleted file mode 100644 index 16829bcda..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Escaping.txt +++ /dev/null @@ -1,14 +0,0 @@ -Filter.ExtractStyleBlocks.Escaping -TYPE: bool -VERSION: 3.0.0 -DEFAULT: true -ALIASES: Filter.ExtractStyleBlocksEscaping, FilterParam.ExtractStyleBlocksEscaping ---DESCRIPTION-- - -

    - Whether or not to escape the dangerous characters <, > and & - as \3C, \3E and \26, respectively. This is can be safely set to false - if the contents of StyleBlocks will be placed in an external stylesheet, - where there is no risk of it being interpreted as HTML. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt deleted file mode 100644 index 7f95f54d1..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.Scope.txt +++ /dev/null @@ -1,29 +0,0 @@ -Filter.ExtractStyleBlocks.Scope -TYPE: string/null -VERSION: 3.0.0 -DEFAULT: NULL -ALIASES: Filter.ExtractStyleBlocksScope, FilterParam.ExtractStyleBlocksScope ---DESCRIPTION-- - -

    - If you would like users to be able to define external stylesheets, but - only allow them to specify CSS declarations for a specific node and - prevent them from fiddling with other elements, use this directive. - It accepts any valid CSS selector, and will prepend this to any - CSS declaration extracted from the document. For example, if this - directive is set to #user-content and a user uses the - selector a:hover, the final selector will be - #user-content a:hover. -

    -

    - The comma shorthand may be used; consider the above example, with - #user-content, #user-content2, the final selector will - be #user-content a:hover, #user-content2 a:hover. -

    -

    - Warning: It is possible for users to bypass this measure - using a naughty + selector. This is a bug in CSS Tidy 1.3, not HTML - Purifier, and I am working to get it fixed. Until then, HTML Purifier - performs a basic check to prevent this. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt deleted file mode 100644 index 6c231b2d7..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.TidyImpl.txt +++ /dev/null @@ -1,16 +0,0 @@ -Filter.ExtractStyleBlocks.TidyImpl -TYPE: mixed/null -VERSION: 3.1.0 -DEFAULT: NULL -ALIASES: FilterParam.ExtractStyleBlocksTidyImpl ---DESCRIPTION-- -

    - If left NULL, HTML Purifier will attempt to instantiate a csstidy - class to use for internal cleaning. This will usually be good enough. -

    -

    - However, for trusted user input, you can set this to false to - disable cleaning. In addition, you can supply your own concrete implementation - of Tidy's interface to use, although I don't know why you'd want to do that. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt deleted file mode 100644 index 078d08741..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.ExtractStyleBlocks.txt +++ /dev/null @@ -1,74 +0,0 @@ -Filter.ExtractStyleBlocks -TYPE: bool -VERSION: 3.1.0 -DEFAULT: false -EXTERNAL: CSSTidy ---DESCRIPTION-- -

    - This directive turns on the style block extraction filter, which removes - style blocks from input HTML, cleans them up with CSSTidy, - and places them in the StyleBlocks context variable, for further - use by you, usually to be placed in an external stylesheet, or a - style block in the head of your document. -

    -

    - Sample usage: -

    -
    ';
    -?>
    -
    -
    -
    -  Filter.ExtractStyleBlocks
    -body {color:#F00;} Some text';
    -
    -    $config = HTMLPurifier_Config::createDefault();
    -    $config->set('Filter', 'ExtractStyleBlocks', true);
    -    $purifier = new HTMLPurifier($config);
    -
    -    $html = $purifier->purify($dirty);
    -
    -    // This implementation writes the stylesheets to the styles/ directory.
    -    // You can also echo the styles inside the document, but it's a bit
    -    // more difficult to make sure they get interpreted properly by
    -    // browsers; try the usual CSS armoring techniques.
    -    $styles = $purifier->context->get('StyleBlocks');
    -    $dir = 'styles/';
    -    if (!is_dir($dir)) mkdir($dir);
    -    $hash = sha1($_GET['html']);
    -    foreach ($styles as $i => $style) {
    -        file_put_contents($name = $dir . $hash . "_$i");
    -        echo '';
    -    }
    -?>
    -
    -
    -  
    - -
    - - -]]>
    -

    - Warning: It is possible for a user to mount an - imagecrash attack using this CSS. Counter-measures are difficult; - it is not simply enough to limit the range of CSS lengths (using - relative lengths with many nesting levels allows for large values - to be attained without actually specifying them in the stylesheet), - and the flexible nature of selectors makes it difficult to selectively - disable lengths on image tags (HTML Purifier, however, does disable - CSS width and height in inline styling). There are probably two effective - counter measures: an explicit width and height set to auto in all - images in your document (unlikely) or the disabling of width and - height (somewhat reasonable). Whether or not these measures should be - used is left to the reader. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt deleted file mode 100644 index 321eaa2d8..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Filter.YouTube.txt +++ /dev/null @@ -1,16 +0,0 @@ -Filter.YouTube -TYPE: bool -VERSION: 3.1.0 -DEFAULT: false ---DESCRIPTION-- -

    - Warning: Deprecated in favor of %HTML.SafeObject and - %Output.FlashCompat (turn both on to allow YouTube videos and other - Flash content). -

    -

    - This directive enables YouTube video embedding in HTML Purifier. Check - this document - on embedding videos for more information on what this filter does. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt deleted file mode 100644 index 0b2c106da..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Allowed.txt +++ /dev/null @@ -1,25 +0,0 @@ -HTML.Allowed -TYPE: itext/null -VERSION: 2.0.0 -DEFAULT: NULL ---DESCRIPTION-- - -

    - This is a preferred convenience directive that combines - %HTML.AllowedElements and %HTML.AllowedAttributes. - Specify elements and attributes that are allowed using: - element1[attr1|attr2],element2.... For example, - if you would like to only allow paragraphs and links, specify - a[href],p. You can specify attributes that apply - to all elements using an asterisk, e.g. *[lang]. - You can also use newlines instead of commas to separate elements. -

    -

    - Warning: - All of the constraints on the component directives are still enforced. - The syntax is a subset of TinyMCE's valid_elements - whitelist: directly copy-pasting it here will probably result in - broken whitelists. If %HTML.AllowedElements or %HTML.AllowedAttributes - are set, this directive has no effect. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt deleted file mode 100644 index fcf093f17..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedAttributes.txt +++ /dev/null @@ -1,19 +0,0 @@ -HTML.AllowedAttributes -TYPE: lookup/null -VERSION: 1.3.0 -DEFAULT: NULL ---DESCRIPTION-- - -

    - If HTML Purifier's attribute set is unsatisfactory, overload it! - The syntax is "tag.attr" or "*.attr" for the global attributes - (style, id, class, dir, lang, xml:lang). -

    -

    - Warning: If another directive conflicts with the - elements here, that directive will win and override. For - example, %HTML.EnableAttrID will take precedence over *.id in this - directive. You must set that directive to true before you can use - IDs at all. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt deleted file mode 100644 index 140e21423..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedComments.txt +++ /dev/null @@ -1,10 +0,0 @@ -HTML.AllowedComments -TYPE: lookup -VERSION: 4.4.0 -DEFAULT: array() ---DESCRIPTION-- -A whitelist which indicates what explicit comment bodies should be -allowed, modulo leading and trailing whitespace. See also %HTML.AllowedCommentsRegexp -(these directives are union'ed together, so a comment is considered -valid if any directive deems it valid.) ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt deleted file mode 100644 index f22e977d4..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedCommentsRegexp.txt +++ /dev/null @@ -1,15 +0,0 @@ -HTML.AllowedCommentsRegexp -TYPE: string/null -VERSION: 4.4.0 -DEFAULT: NULL ---DESCRIPTION-- -A regexp, which if it matches the body of a comment, indicates that -it should be allowed. Trailing and leading spaces are removed prior -to running this regular expression. -Warning: Make sure you specify -correct anchor metacharacters ^regex$, otherwise you may accept -comments that you did not mean to! In particular, the regex /foo|bar/ -is probably not sufficiently strict, since it also allows foobar. -See also %HTML.AllowedComments (these directives are union'ed together, -so a comment is considered valid if any directive deems it valid.) ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt deleted file mode 100644 index 1d3fa7907..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedElements.txt +++ /dev/null @@ -1,23 +0,0 @@ -HTML.AllowedElements -TYPE: lookup/null -VERSION: 1.3.0 -DEFAULT: NULL ---DESCRIPTION-- -

    - If HTML Purifier's tag set is unsatisfactory for your needs, you can - overload it with your own list of tags to allow. If you change - this, you probably also want to change %HTML.AllowedAttributes; see - also %HTML.Allowed which lets you set allowed elements and - attributes at the same time. -

    -

    - If you attempt to allow an element that HTML Purifier does not know - about, HTML Purifier will raise an error. You will need to manually - tell HTML Purifier about this element by using the - advanced customization features. -

    -

    - Warning: If another directive conflicts with the - elements here, that directive will win and override. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt deleted file mode 100644 index 5a59a55c0..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.AllowedModules.txt +++ /dev/null @@ -1,20 +0,0 @@ -HTML.AllowedModules -TYPE: lookup/null -VERSION: 2.0.0 -DEFAULT: NULL ---DESCRIPTION-- - -

    - A doctype comes with a set of usual modules to use. Without having - to mucking about with the doctypes, you can quickly activate or - disable these modules by specifying which modules you wish to allow - with this directive. This is most useful for unit testing specific - modules, although end users may find it useful for their own ends. -

    -

    - If you specify a module that does not exist, the manager will silently - fail to use it, so be careful! User-defined modules are not affected - by this directive. Modules defined in %HTML.CoreModules are not - affected by this directive. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt deleted file mode 100644 index 151fb7b82..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Attr.Name.UseCDATA.txt +++ /dev/null @@ -1,11 +0,0 @@ -HTML.Attr.Name.UseCDATA -TYPE: bool -DEFAULT: false -VERSION: 4.0.0 ---DESCRIPTION-- -The W3C specification DTD defines the name attribute to be CDATA, not ID, due -to limitations of DTD. In certain documents, this relaxed behavior is desired, -whether it is to specify duplicate names, or to specify names that would be -illegal IDs (for example, names that begin with a digit.) Set this configuration -directive to true to use the relaxed parsing rules. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt deleted file mode 100644 index 45ae469ec..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.BlockWrapper.txt +++ /dev/null @@ -1,18 +0,0 @@ -HTML.BlockWrapper -TYPE: string -VERSION: 1.3.0 -DEFAULT: 'p' ---DESCRIPTION-- - -

    - String name of element to wrap inline elements that are inside a block - context. This only occurs in the children of blockquote in strict mode. -

    -

    - Example: by default value, - <blockquote>Foo</blockquote> would become - <blockquote><p>Foo</p></blockquote>. - The <p> tags can be replaced with whatever you desire, - as long as it is a block level element. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt deleted file mode 100644 index 524618879..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.CoreModules.txt +++ /dev/null @@ -1,23 +0,0 @@ -HTML.CoreModules -TYPE: lookup -VERSION: 2.0.0 ---DEFAULT-- -array ( - 'Structure' => true, - 'Text' => true, - 'Hypertext' => true, - 'List' => true, - 'NonXMLCommonAttributes' => true, - 'XMLCommonAttributes' => true, - 'CommonAttributes' => true, -) ---DESCRIPTION-- - -

    - Certain modularized doctypes (XHTML, namely), have certain modules - that must be included for the doctype to be an conforming document - type: put those modules here. By default, XHTML's core modules - are used. You can set this to a blank array to disable core module - protection, but this is not recommended. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt deleted file mode 100644 index a64e3d7c3..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.CustomDoctype.txt +++ /dev/null @@ -1,9 +0,0 @@ -HTML.CustomDoctype -TYPE: string/null -VERSION: 2.0.1 -DEFAULT: NULL ---DESCRIPTION-- - -A custom doctype for power-users who defined there own document -type. This directive only applies when %HTML.Doctype is blank. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt deleted file mode 100644 index 103db754a..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionID.txt +++ /dev/null @@ -1,33 +0,0 @@ -HTML.DefinitionID -TYPE: string/null -DEFAULT: NULL -VERSION: 2.0.0 ---DESCRIPTION-- - -

    - Unique identifier for a custom-built HTML definition. If you edit - the raw version of the HTMLDefinition, introducing changes that the - configuration object does not reflect, you must specify this variable. - If you change your custom edits, you should change this directive, or - clear your cache. Example: -

    -
    -$config = HTMLPurifier_Config::createDefault();
    -$config->set('HTML', 'DefinitionID', '1');
    -$def = $config->getHTMLDefinition();
    -$def->addAttribute('a', 'tabindex', 'Number');
    -
    -

    - In the above example, the configuration is still at the defaults, but - using the advanced API, an extra attribute has been added. The - configuration object normally has no way of knowing that this change - has taken place, so it needs an extra directive: %HTML.DefinitionID. - If someone else attempts to use the default configuration, these two - pieces of code will not clobber each other in the cache, since one has - an extra directive attached to it. -

    -

    - You must specify a value to this directive to use the - advanced API features. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt deleted file mode 100644 index 229ae0267..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.DefinitionRev.txt +++ /dev/null @@ -1,16 +0,0 @@ -HTML.DefinitionRev -TYPE: int -VERSION: 2.0.0 -DEFAULT: 1 ---DESCRIPTION-- - -

    - Revision identifier for your custom definition specified in - %HTML.DefinitionID. This serves the same purpose: uniquely identifying - your custom definition, but this one does so in a chronological - context: revision 3 is more up-to-date then revision 2. Thus, when - this gets incremented, the cache handling is smart enough to clean - up any older revisions of your definition as well as flush the - cache. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt deleted file mode 100644 index 9dab497f2..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Doctype.txt +++ /dev/null @@ -1,11 +0,0 @@ -HTML.Doctype -TYPE: string/null -DEFAULT: NULL ---DESCRIPTION-- -Doctype to use during filtering. Technically speaking this is not actually -a doctype (as it does not identify a corresponding DTD), but we are using -this name for sake of simplicity. When non-blank, this will override any -older directives like %HTML.XHTML or %HTML.Strict. ---ALLOWED-- -'HTML 4.01 Transitional', 'HTML 4.01 Strict', 'XHTML 1.0 Transitional', 'XHTML 1.0 Strict', 'XHTML 1.1' ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt deleted file mode 100644 index 7878dc0bf..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.FlashAllowFullScreen.txt +++ /dev/null @@ -1,11 +0,0 @@ -HTML.FlashAllowFullScreen -TYPE: bool -VERSION: 4.2.0 -DEFAULT: false ---DESCRIPTION-- -

    - Whether or not to permit embedded Flash content from - %HTML.SafeObject to expand to the full screen. Corresponds to - the allowFullScreen parameter. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt deleted file mode 100644 index 57358f9ba..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenAttributes.txt +++ /dev/null @@ -1,21 +0,0 @@ -HTML.ForbiddenAttributes -TYPE: lookup -VERSION: 3.1.0 -DEFAULT: array() ---DESCRIPTION-- -

    - While this directive is similar to %HTML.AllowedAttributes, for - forwards-compatibility with XML, this attribute has a different syntax. Instead of - tag.attr, use tag@attr. To disallow href - attributes in a tags, set this directive to - a@href. You can also disallow an attribute globally with - attr or *@attr (either syntax is fine; the latter - is provided for consistency with %HTML.AllowedAttributes). -

    -

    - Warning: This directive complements %HTML.ForbiddenElements, - accordingly, check - out that directive for a discussion of why you - should think twice before using this directive. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt deleted file mode 100644 index 93a53e14f..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.ForbiddenElements.txt +++ /dev/null @@ -1,20 +0,0 @@ -HTML.ForbiddenElements -TYPE: lookup -VERSION: 3.1.0 -DEFAULT: array() ---DESCRIPTION-- -

    - This was, perhaps, the most requested feature ever in HTML - Purifier. Please don't abuse it! This is the logical inverse of - %HTML.AllowedElements, and it will override that directive, or any - other directive. -

    -

    - If possible, %HTML.Allowed is recommended over this directive, because it - can sometimes be difficult to tell whether or not you've forbidden all of - the behavior you would like to disallow. If you forbid img - with the expectation of preventing images on your site, you'll be in for - a nasty surprise when people start using the background-image - CSS property. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt deleted file mode 100644 index e424c386e..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.MaxImgLength.txt +++ /dev/null @@ -1,14 +0,0 @@ -HTML.MaxImgLength -TYPE: int/null -DEFAULT: 1200 -VERSION: 3.1.1 ---DESCRIPTION-- -

    - This directive controls the maximum number of pixels in the width and - height attributes in img tags. This is - in place to prevent imagecrash attacks, disable with null at your own risk. - This directive is similar to %CSS.MaxImgLength, and both should be - concurrently edited, although there are - subtle differences in the input format (the HTML max is an integer). -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt deleted file mode 100644 index 700b30924..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Nofollow.txt +++ /dev/null @@ -1,7 +0,0 @@ -HTML.Nofollow -TYPE: bool -VERSION: 4.3.0 -DEFAULT: FALSE ---DESCRIPTION-- -If enabled, nofollow rel attributes are added to all outgoing links. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt deleted file mode 100644 index 62e8e160c..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Parent.txt +++ /dev/null @@ -1,12 +0,0 @@ -HTML.Parent -TYPE: string -VERSION: 1.3.0 -DEFAULT: 'div' ---DESCRIPTION-- - -

    - String name of element that HTML fragment passed to library will be - inserted in. An interesting variation would be using span as the - parent element, meaning that only inline tags would be allowed. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt deleted file mode 100644 index dfb720496..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Proprietary.txt +++ /dev/null @@ -1,12 +0,0 @@ -HTML.Proprietary -TYPE: bool -VERSION: 3.1.0 -DEFAULT: false ---DESCRIPTION-- -

    - Whether or not to allow proprietary elements and attributes in your - documents, as per HTMLPurifier_HTMLModule_Proprietary. - Warning: This can cause your documents to stop - validating! -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt deleted file mode 100644 index cdda09a4c..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeEmbed.txt +++ /dev/null @@ -1,13 +0,0 @@ -HTML.SafeEmbed -TYPE: bool -VERSION: 3.1.1 -DEFAULT: false ---DESCRIPTION-- -

    - Whether or not to permit embed tags in documents, with a number of extra - security features added to prevent script execution. This is similar to - what websites like MySpace do to embed tags. Embed is a proprietary - element and will cause your website to stop validating; you should - see if you can use %Output.FlashCompat with %HTML.SafeObject instead - first.

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt deleted file mode 100644 index 5eb6ec2b5..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeIframe.txt +++ /dev/null @@ -1,13 +0,0 @@ -HTML.SafeIframe -TYPE: bool -VERSION: 4.4.0 -DEFAULT: false ---DESCRIPTION-- -

    - Whether or not to permit iframe tags in untrusted documents. This - directive must be accompanied by a whitelist of permitted iframes, - such as %URI.SafeIframeRegexp, otherwise it will fatally error. - This directive has no effect on strict doctypes, as iframes are not - valid. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt deleted file mode 100644 index ceb342e22..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeObject.txt +++ /dev/null @@ -1,13 +0,0 @@ -HTML.SafeObject -TYPE: bool -VERSION: 3.1.1 -DEFAULT: false ---DESCRIPTION-- -

    - Whether or not to permit object tags in documents, with a number of extra - security features added to prevent script execution. This is similar to - what websites like MySpace do to object tags. You should also enable - %Output.FlashCompat in order to generate Internet Explorer - compatibility code for your object tags. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt deleted file mode 100644 index 5ebc7a19d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.SafeScripting.txt +++ /dev/null @@ -1,10 +0,0 @@ -HTML.SafeScripting -TYPE: lookup -VERSION: 4.5.0 -DEFAULT: array() ---DESCRIPTION-- -

    - Whether or not to permit script tags to external scripts in documents. - Inline scripting is not allowed, and the script must match an explicit whitelist. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt deleted file mode 100644 index a8b1de56b..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Strict.txt +++ /dev/null @@ -1,9 +0,0 @@ -HTML.Strict -TYPE: bool -VERSION: 1.3.0 -DEFAULT: false -DEPRECATED-VERSION: 1.7.0 -DEPRECATED-USE: HTML.Doctype ---DESCRIPTION-- -Determines whether or not to use Transitional (loose) or Strict rulesets. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt deleted file mode 100644 index 587a16778..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TargetBlank.txt +++ /dev/null @@ -1,8 +0,0 @@ -HTML.TargetBlank -TYPE: bool -VERSION: 4.4.0 -DEFAULT: FALSE ---DESCRIPTION-- -If enabled, target=blank attributes are added to all outgoing links. -(This includes links from an HTTPS version of a page to an HTTP version.) ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt deleted file mode 100644 index b4c271b7f..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyAdd.txt +++ /dev/null @@ -1,8 +0,0 @@ -HTML.TidyAdd -TYPE: lookup -VERSION: 2.0.0 -DEFAULT: array() ---DESCRIPTION-- - -Fixes to add to the default set of Tidy fixes as per your level. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt deleted file mode 100644 index 4186ccd0d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyLevel.txt +++ /dev/null @@ -1,24 +0,0 @@ -HTML.TidyLevel -TYPE: string -VERSION: 2.0.0 -DEFAULT: 'medium' ---DESCRIPTION-- - -

    General level of cleanliness the Tidy module should enforce. -There are four allowed values:

    -
    -
    none
    -
    No extra tidying should be done
    -
    light
    -
    Only fix elements that would be discarded otherwise due to - lack of support in doctype
    -
    medium
    -
    Enforce best practices
    -
    heavy
    -
    Transform all deprecated elements and attributes to standards - compliant equivalents
    -
    - ---ALLOWED-- -'none', 'light', 'medium', 'heavy' ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt deleted file mode 100644 index 996762bd1..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.TidyRemove.txt +++ /dev/null @@ -1,8 +0,0 @@ -HTML.TidyRemove -TYPE: lookup -VERSION: 2.0.0 -DEFAULT: array() ---DESCRIPTION-- - -Fixes to remove from the default set of Tidy fixes as per your level. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt deleted file mode 100644 index 1db9237e9..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.Trusted.txt +++ /dev/null @@ -1,9 +0,0 @@ -HTML.Trusted -TYPE: bool -VERSION: 2.0.0 -DEFAULT: false ---DESCRIPTION-- -Indicates whether or not the user input is trusted or not. If the input is -trusted, a more expansive set of allowed tags and attributes will be used. -See also %CSS.Trusted. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt deleted file mode 100644 index 2a47e384f..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/HTML.XHTML.txt +++ /dev/null @@ -1,11 +0,0 @@ -HTML.XHTML -TYPE: bool -DEFAULT: true -VERSION: 1.1.0 -DEPRECATED-VERSION: 1.7.0 -DEPRECATED-USE: HTML.Doctype ---DESCRIPTION-- -Determines whether or not output is XHTML 1.0 or HTML 4.01 flavor. ---ALIASES-- -Core.XHTML ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt deleted file mode 100644 index 08921fde7..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.CommentScriptContents.txt +++ /dev/null @@ -1,10 +0,0 @@ -Output.CommentScriptContents -TYPE: bool -VERSION: 2.0.0 -DEFAULT: true ---DESCRIPTION-- -Determines whether or not HTML Purifier should attempt to fix up the -contents of script tags for legacy browsers with comments. ---ALIASES-- -Core.CommentScriptContents ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt deleted file mode 100644 index d6f0d9f29..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.FixInnerHTML.txt +++ /dev/null @@ -1,15 +0,0 @@ -Output.FixInnerHTML -TYPE: bool -VERSION: 4.3.0 -DEFAULT: true ---DESCRIPTION-- -

    - If true, HTML Purifier will protect against Internet Explorer's - mishandling of the innerHTML attribute by appending - a space to any attribute that does not contain angled brackets, spaces - or quotes, but contains a backtick. This slightly changes the - semantics of any given attribute, so if this is unacceptable and - you do not use innerHTML on any of your pages, you can - turn this directive off. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt deleted file mode 100644 index 93398e859..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.FlashCompat.txt +++ /dev/null @@ -1,11 +0,0 @@ -Output.FlashCompat -TYPE: bool -VERSION: 4.1.0 -DEFAULT: false ---DESCRIPTION-- -

    - If true, HTML Purifier will generate Internet Explorer compatibility - code for all object code. This is highly recommended if you enable - %HTML.SafeObject. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt deleted file mode 100644 index 79f8ad82c..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.Newline.txt +++ /dev/null @@ -1,13 +0,0 @@ -Output.Newline -TYPE: string/null -VERSION: 2.0.1 -DEFAULT: NULL ---DESCRIPTION-- - -

    - Newline string to format final output with. If left null, HTML Purifier - will auto-detect the default newline type of the system and use that; - you can manually override it here. Remember, \r\n is Windows, \r - is Mac, and \n is Unix. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt deleted file mode 100644 index 232b02362..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.SortAttr.txt +++ /dev/null @@ -1,14 +0,0 @@ -Output.SortAttr -TYPE: bool -VERSION: 3.2.0 -DEFAULT: false ---DESCRIPTION-- -

    - If true, HTML Purifier will sort attributes by name before writing them back - to the document, converting a tag like: <el b="" a="" c="" /> - to <el a="" b="" c="" />. This is a workaround for - a bug in FCKeditor which causes it to swap attributes order, adding noise - to text diffs. If you're not seeing this bug, chances are, you don't need - this directive. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt deleted file mode 100644 index 06bab00a0..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Output.TidyFormat.txt +++ /dev/null @@ -1,25 +0,0 @@ -Output.TidyFormat -TYPE: bool -VERSION: 1.1.1 -DEFAULT: false ---DESCRIPTION-- -

    - Determines whether or not to run Tidy on the final output for pretty - formatting reasons, such as indentation and wrap. -

    -

    - This can greatly improve readability for editors who are hand-editing - the HTML, but is by no means necessary as HTML Purifier has already - fixed all major errors the HTML may have had. Tidy is a non-default - extension, and this directive will silently fail if Tidy is not - available. -

    -

    - If you are looking to make the overall look of your page's source - better, I recommend running Tidy on the entire page rather than just - user-content (after all, the indentation relative to the containing - blocks will be incorrect). -

    ---ALIASES-- -Core.TidyFormat ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt deleted file mode 100644 index 071bc0295..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/Test.ForceNoIconv.txt +++ /dev/null @@ -1,7 +0,0 @@ -Test.ForceNoIconv -TYPE: bool -DEFAULT: false ---DESCRIPTION-- -When set to true, HTMLPurifier_Encoder will act as if iconv does not exist -and use only pure PHP implementations. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt deleted file mode 100644 index 666635a5f..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.AllowedSchemes.txt +++ /dev/null @@ -1,17 +0,0 @@ -URI.AllowedSchemes -TYPE: lookup ---DEFAULT-- -array ( - 'http' => true, - 'https' => true, - 'mailto' => true, - 'ftp' => true, - 'nntp' => true, - 'news' => true, -) ---DESCRIPTION-- -Whitelist that defines the schemes that a URI is allowed to have. This -prevents XSS attacks from using pseudo-schemes like javascript or mocha. -There is also support for the data and file -URI schemes, but they are not enabled by default. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Base.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Base.txt deleted file mode 100644 index 876f0680c..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Base.txt +++ /dev/null @@ -1,17 +0,0 @@ -URI.Base -TYPE: string/null -VERSION: 2.1.0 -DEFAULT: NULL ---DESCRIPTION-- - -

    - The base URI is the URI of the document this purified HTML will be - inserted into. This information is important if HTML Purifier needs - to calculate absolute URIs from relative URIs, such as when %URI.MakeAbsolute - is on. You may use a non-absolute URI for this value, but behavior - may vary (%URI.MakeAbsolute deals nicely with both absolute and - relative paths, but forwards-compatibility is not guaranteed). - Warning: If set, the scheme on this URI - overrides the one specified by %URI.DefaultScheme. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt deleted file mode 100644 index 728e378cb..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefaultScheme.txt +++ /dev/null @@ -1,10 +0,0 @@ -URI.DefaultScheme -TYPE: string -DEFAULT: 'http' ---DESCRIPTION-- - -

    - Defines through what scheme the output will be served, in order to - select the proper object validator when no scheme information is present. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt deleted file mode 100644 index f05312ba8..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionID.txt +++ /dev/null @@ -1,11 +0,0 @@ -URI.DefinitionID -TYPE: string/null -VERSION: 2.1.0 -DEFAULT: NULL ---DESCRIPTION-- - -

    - Unique identifier for a custom-built URI definition. If you want - to add custom URIFilters, you must specify this value. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt deleted file mode 100644 index 80cfea93f..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DefinitionRev.txt +++ /dev/null @@ -1,11 +0,0 @@ -URI.DefinitionRev -TYPE: int -VERSION: 2.1.0 -DEFAULT: 1 ---DESCRIPTION-- - -

    - Revision identifier for your custom definition. See - %HTML.DefinitionRev for details. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt deleted file mode 100644 index 71ce025a2..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Disable.txt +++ /dev/null @@ -1,14 +0,0 @@ -URI.Disable -TYPE: bool -VERSION: 1.3.0 -DEFAULT: false ---DESCRIPTION-- - -

    - Disables all URIs in all forms. Not sure why you'd want to do that - (after all, the Internet's founded on the notion of a hyperlink). -

    - ---ALIASES-- -Attr.DisableURI ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt deleted file mode 100644 index 13c122c8c..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternal.txt +++ /dev/null @@ -1,11 +0,0 @@ -URI.DisableExternal -TYPE: bool -VERSION: 1.2.0 -DEFAULT: false ---DESCRIPTION-- -Disables links to external websites. This is a highly effective anti-spam -and anti-pagerank-leech measure, but comes at a hefty price: nolinks or -images outside of your domain will be allowed. Non-linkified URIs will -still be preserved. If you want to be able to link to subdomains or use -absolute URIs, specify %URI.Host for your website. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt deleted file mode 100644 index abcc1efd6..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableExternalResources.txt +++ /dev/null @@ -1,13 +0,0 @@ -URI.DisableExternalResources -TYPE: bool -VERSION: 1.3.0 -DEFAULT: false ---DESCRIPTION-- -Disables the embedding of external resources, preventing users from -embedding things like images from other hosts. This prevents access -tracking (good for email viewers), bandwidth leeching, cross-site request -forging, goatse.cx posting, and other nasties, but also results in a loss -of end-user functionality (they can't directly post a pic they posted from -Flickr anymore). Use it if you don't have a robust user-content moderation -team. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt deleted file mode 100644 index f891de499..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.DisableResources.txt +++ /dev/null @@ -1,15 +0,0 @@ -URI.DisableResources -TYPE: bool -VERSION: 4.2.0 -DEFAULT: false ---DESCRIPTION-- -

    - Disables embedding resources, essentially meaning no pictures. You can - still link to them though. See %URI.DisableExternalResources for why - this might be a good idea. -

    -

    - Note: While this directive has been available since 1.3.0, - it didn't actually start doing anything until 4.2.0. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Host.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Host.txt deleted file mode 100644 index ee83b121d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Host.txt +++ /dev/null @@ -1,19 +0,0 @@ -URI.Host -TYPE: string/null -VERSION: 1.2.0 -DEFAULT: NULL ---DESCRIPTION-- - -

    - Defines the domain name of the server, so we can determine whether or - an absolute URI is from your website or not. Not strictly necessary, - as users should be using relative URIs to reference resources on your - website. It will, however, let you use absolute URIs to link to - subdomains of the domain you post here: i.e. example.com will allow - sub.example.com. However, higher up domains will still be excluded: - if you set %URI.Host to sub.example.com, example.com will be blocked. - Note: This directive overrides %URI.Base because - a given page may be on a sub-domain, but you wish HTML Purifier to be - more relaxed and allow some of the parent domains too. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt deleted file mode 100644 index 0b6df7625..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.HostBlacklist.txt +++ /dev/null @@ -1,9 +0,0 @@ -URI.HostBlacklist -TYPE: list -VERSION: 1.3.0 -DEFAULT: array() ---DESCRIPTION-- -List of strings that are forbidden in the host of any URI. Use it to kill -domain names of spam, etc. Note that it will catch anything in the domain, -so moo.com will catch moo.com.example.com. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt deleted file mode 100644 index 4214900a5..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MakeAbsolute.txt +++ /dev/null @@ -1,13 +0,0 @@ -URI.MakeAbsolute -TYPE: bool -VERSION: 2.1.0 -DEFAULT: false ---DESCRIPTION-- - -

    - Converts all URIs into absolute forms. This is useful when the HTML - being filtered assumes a specific base path, but will actually be - viewed in a different context (and setting an alternate base URI is - not possible). %URI.Base must be set for this directive to work. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt deleted file mode 100644 index 58c81dcc4..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.Munge.txt +++ /dev/null @@ -1,83 +0,0 @@ -URI.Munge -TYPE: string/null -VERSION: 1.3.0 -DEFAULT: NULL ---DESCRIPTION-- - -

    - Munges all browsable (usually http, https and ftp) - absolute URIs into another URI, usually a URI redirection service. - This directive accepts a URI, formatted with a %s where - the url-encoded original URI should be inserted (sample: - http://www.google.com/url?q=%s). -

    -

    - Uses for this directive: -

    -
      -
    • - Prevent PageRank leaks, while being fairly transparent - to users (you may also want to add some client side JavaScript to - override the text in the statusbar). Notice: - Many security experts believe that this form of protection does not deter spam-bots. -
    • -
    • - Redirect users to a splash page telling them they are leaving your - website. While this is poor usability practice, it is often mandated - in corporate environments. -
    • -
    -

    - Prior to HTML Purifier 3.1.1, this directive also enabled the munging - of browsable external resources, which could break things if your redirection - script was a splash page or used meta tags. To revert to - previous behavior, please use %URI.MungeResources. -

    -

    - You may want to also use %URI.MungeSecretKey along with this directive - in order to enforce what URIs your redirector script allows. Open - redirector scripts can be a security risk and negatively affect the - reputation of your domain name. -

    -

    - Starting with HTML Purifier 3.1.1, there is also these substitutions: -

    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    KeyDescriptionExample <a href="">
    %r1 - The URI embeds a resource
    (blank) - The URI is merely a link
    %nThe name of the tag this URI came froma
    %mThe name of the attribute this URI came fromhref
    %pThe name of the CSS property this URI came from, or blank if irrelevant
    -

    - Admittedly, these letters are somewhat arbitrary; the only stipulation - was that they couldn't be a through f. r is for resource (I would have preferred - e, but you take what you can get), n is for name, m - was picked because it came after n (and I couldn't use a), p is for - property. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt deleted file mode 100644 index 6fce0fdc3..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeResources.txt +++ /dev/null @@ -1,17 +0,0 @@ -URI.MungeResources -TYPE: bool -VERSION: 3.1.1 -DEFAULT: false ---DESCRIPTION-- -

    - If true, any URI munging directives like %URI.Munge - will also apply to embedded resources, such as <img src="">. - Be careful enabling this directive if you have a redirector script - that does not use the Location HTTP header; all of your images - and other embedded resources will break. -

    -

    - Warning: It is strongly advised you use this in conjunction - %URI.MungeSecretKey to mitigate the security risk of an open redirector. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt deleted file mode 100644 index 0d00f62ea..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.MungeSecretKey.txt +++ /dev/null @@ -1,30 +0,0 @@ -URI.MungeSecretKey -TYPE: string/null -VERSION: 3.1.1 -DEFAULT: NULL ---DESCRIPTION-- -

    - This directive enables secure checksum generation along with %URI.Munge. - It should be set to a secure key that is not shared with anyone else. - The checksum can be placed in the URI using %t. Use of this checksum - affords an additional level of protection by allowing a redirector - to check if a URI has passed through HTML Purifier with this line: -

    - -
    $checksum === sha1($secret_key . ':' . $url)
    - -

    - If the output is TRUE, the redirector script should accept the URI. -

    - -

    - Please note that it would still be possible for an attacker to procure - secure hashes en-mass by abusing your website's Preview feature or the - like, but this service affords an additional level of protection - that should be combined with website blacklisting. -

    - -

    - Remember this has no effect if %URI.Munge is not on. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt deleted file mode 100644 index 23331a4e7..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.OverrideAllowedSchemes.txt +++ /dev/null @@ -1,9 +0,0 @@ -URI.OverrideAllowedSchemes -TYPE: bool -DEFAULT: true ---DESCRIPTION-- -If this is set to true (which it is by default), you can override -%URI.AllowedSchemes by simply registering a HTMLPurifier_URIScheme to the -registry. If false, you will also have to update that directive in order -to add more schemes. ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt deleted file mode 100644 index 79084832b..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/URI.SafeIframeRegexp.txt +++ /dev/null @@ -1,22 +0,0 @@ -URI.SafeIframeRegexp -TYPE: string/null -VERSION: 4.4.0 -DEFAULT: NULL ---DESCRIPTION-- -

    - A PCRE regular expression that will be matched against an iframe URI. This is - a relatively inflexible scheme, but works well enough for the most common - use-case of iframes: embedded video. This directive only has an effect if - %HTML.SafeIframe is enabled. Here are some example values: -

    -
      -
    • %^http://www.youtube.com/embed/% - Allow YouTube videos
    • -
    • %^http://player.vimeo.com/video/% - Allow Vimeo videos
    • -
    • %^http://(www.youtube.com/embed/|player.vimeo.com/video/)% - Allow both
    • -
    -

    - Note that this directive does not give you enough granularity to, say, disable - all autoplay videos. Pipe up on the HTML Purifier forums if this - is a capability you want. -

    ---# vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/info.ini b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/info.ini deleted file mode 100644 index 5de4505e1..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ConfigSchema/schema/info.ini +++ /dev/null @@ -1,3 +0,0 @@ -name = "HTML Purifier" - -; vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ContentSets.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ContentSets.php deleted file mode 100644 index 3b6e96f5f..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ContentSets.php +++ /dev/null @@ -1,155 +0,0 @@ - true) indexed by name. - * @note This is in HTMLPurifier_HTMLDefinition->info_content_sets - */ - public $lookup = array(); - - /** - * Synchronized list of defined content sets (keys of info) - */ - protected $keys = array(); - /** - * Synchronized list of defined content values (values of info) - */ - protected $values = array(); - - /** - * Merges in module's content sets, expands identifiers in the content - * sets and populates the keys, values and lookup member variables. - * @param $modules List of HTMLPurifier_HTMLModule - */ - public function __construct($modules) { - if (!is_array($modules)) $modules = array($modules); - // populate content_sets based on module hints - // sorry, no way of overloading - foreach ($modules as $module_i => $module) { - foreach ($module->content_sets as $key => $value) { - $temp = $this->convertToLookup($value); - if (isset($this->lookup[$key])) { - // add it into the existing content set - $this->lookup[$key] = array_merge($this->lookup[$key], $temp); - } else { - $this->lookup[$key] = $temp; - } - } - } - $old_lookup = false; - while ($old_lookup !== $this->lookup) { - $old_lookup = $this->lookup; - foreach ($this->lookup as $i => $set) { - $add = array(); - foreach ($set as $element => $x) { - if (isset($this->lookup[$element])) { - $add += $this->lookup[$element]; - unset($this->lookup[$i][$element]); - } - } - $this->lookup[$i] += $add; - } - } - - foreach ($this->lookup as $key => $lookup) { - $this->info[$key] = implode(' | ', array_keys($lookup)); - } - $this->keys = array_keys($this->info); - $this->values = array_values($this->info); - } - - /** - * Accepts a definition; generates and assigns a ChildDef for it - * @param $def HTMLPurifier_ElementDef reference - * @param $module Module that defined the ElementDef - */ - public function generateChildDef(&$def, $module) { - if (!empty($def->child)) return; // already done! - $content_model = $def->content_model; - if (is_string($content_model)) { - // Assume that $this->keys is alphanumeric - $def->content_model = preg_replace_callback( - '/\b(' . implode('|', $this->keys) . ')\b/', - array($this, 'generateChildDefCallback'), - $content_model - ); - //$def->content_model = str_replace( - // $this->keys, $this->values, $content_model); - } - $def->child = $this->getChildDef($def, $module); - } - - public function generateChildDefCallback($matches) { - return $this->info[$matches[0]]; - } - - /** - * Instantiates a ChildDef based on content_model and content_model_type - * member variables in HTMLPurifier_ElementDef - * @note This will also defer to modules for custom HTMLPurifier_ChildDef - * subclasses that need content set expansion - * @param $def HTMLPurifier_ElementDef to have ChildDef extracted - * @return HTMLPurifier_ChildDef corresponding to ElementDef - */ - public function getChildDef($def, $module) { - $value = $def->content_model; - if (is_object($value)) { - trigger_error( - 'Literal object child definitions should be stored in '. - 'ElementDef->child not ElementDef->content_model', - E_USER_NOTICE - ); - return $value; - } - switch ($def->content_model_type) { - case 'required': - return new HTMLPurifier_ChildDef_Required($value); - case 'optional': - return new HTMLPurifier_ChildDef_Optional($value); - case 'empty': - return new HTMLPurifier_ChildDef_Empty(); - case 'custom': - return new HTMLPurifier_ChildDef_Custom($value); - } - // defer to its module - $return = false; - if ($module->defines_child_def) { // save a func call - $return = $module->getChildDef($def); - } - if ($return !== false) return $return; - // error-out - trigger_error( - 'Could not determine which ChildDef class to instantiate', - E_USER_ERROR - ); - return false; - } - - /** - * Converts a string list of elements separated by pipes into - * a lookup array. - * @param $string List of elements - * @return Lookup array of elements - */ - protected function convertToLookup($string) { - $array = explode('|', str_replace(' ', '', $string)); - $ret = array(); - foreach ($array as $i => $k) { - $ret[$k] = true; - } - return $ret; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Context.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Context.php deleted file mode 100644 index 9ddf0c547..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Context.php +++ /dev/null @@ -1,82 +0,0 @@ -_storage[$name])) { - trigger_error("Name $name produces collision, cannot re-register", - E_USER_ERROR); - return; - } - $this->_storage[$name] =& $ref; - } - - /** - * Retrieves a variable reference from the context. - * @param $name String name - * @param $ignore_error Boolean whether or not to ignore error - */ - public function &get($name, $ignore_error = false) { - if (!isset($this->_storage[$name])) { - if (!$ignore_error) { - trigger_error("Attempted to retrieve non-existent variable $name", - E_USER_ERROR); - } - $var = null; // so we can return by reference - return $var; - } - return $this->_storage[$name]; - } - - /** - * Destorys a variable in the context. - * @param $name String name - */ - public function destroy($name) { - if (!isset($this->_storage[$name])) { - trigger_error("Attempted to destroy non-existent variable $name", - E_USER_ERROR); - return; - } - unset($this->_storage[$name]); - } - - /** - * Checks whether or not the variable exists. - * @param $name String name - */ - public function exists($name) { - return isset($this->_storage[$name]); - } - - /** - * Loads a series of variables from an associative array - * @param $context_array Assoc array of variables to load - */ - public function loadArray($context_array) { - foreach ($context_array as $key => $discard) { - $this->register($key, $context_array[$key]); - } - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Definition.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Definition.php deleted file mode 100644 index c7f82eba4..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Definition.php +++ /dev/null @@ -1,50 +0,0 @@ -setup) return; - $this->setup = true; - $this->doSetup($config); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCache.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCache.php deleted file mode 100644 index c6e1e388c..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCache.php +++ /dev/null @@ -1,108 +0,0 @@ -type = $type; - } - - /** - * Generates a unique identifier for a particular configuration - * @param Instance of HTMLPurifier_Config - */ - public function generateKey($config) { - return $config->version . ',' . // possibly replace with function calls - $config->getBatchSerial($this->type) . ',' . - $config->get($this->type . '.DefinitionRev'); - } - - /** - * Tests whether or not a key is old with respect to the configuration's - * version and revision number. - * @param $key Key to test - * @param $config Instance of HTMLPurifier_Config to test against - */ - public function isOld($key, $config) { - if (substr_count($key, ',') < 2) return true; - list($version, $hash, $revision) = explode(',', $key, 3); - $compare = version_compare($version, $config->version); - // version mismatch, is always old - if ($compare != 0) return true; - // versions match, ids match, check revision number - if ( - $hash == $config->getBatchSerial($this->type) && - $revision < $config->get($this->type . '.DefinitionRev') - ) return true; - return false; - } - - /** - * Checks if a definition's type jives with the cache's type - * @note Throws an error on failure - * @param $def Definition object to check - * @return Boolean true if good, false if not - */ - public function checkDefType($def) { - if ($def->type !== $this->type) { - trigger_error("Cannot use definition of type {$def->type} in cache for {$this->type}"); - return false; - } - return true; - } - - /** - * Adds a definition object to the cache - */ - abstract public function add($def, $config); - - /** - * Unconditionally saves a definition object to the cache - */ - abstract public function set($def, $config); - - /** - * Replace an object in the cache - */ - abstract public function replace($def, $config); - - /** - * Retrieves a definition object from the cache - */ - abstract public function get($config); - - /** - * Removes a definition object to the cache - */ - abstract public function remove($config); - - /** - * Clears all objects from cache - */ - abstract public function flush($config); - - /** - * Clears all expired (older version or revision) objects from cache - * @note Be carefuly implementing this method as flush. Flush must - * not interfere with other Definition types, and cleanup() - * should not be repeatedly called by userland code. - */ - abstract public function cleanup($config); - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator.php deleted file mode 100644 index b0fb6d0cd..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator.php +++ /dev/null @@ -1,62 +0,0 @@ -copy(); - // reference is necessary for mocks in PHP 4 - $decorator->cache =& $cache; - $decorator->type = $cache->type; - return $decorator; - } - - /** - * Cross-compatible clone substitute - */ - public function copy() { - return new HTMLPurifier_DefinitionCache_Decorator(); - } - - public function add($def, $config) { - return $this->cache->add($def, $config); - } - - public function set($def, $config) { - return $this->cache->set($def, $config); - } - - public function replace($def, $config) { - return $this->cache->replace($def, $config); - } - - public function get($config) { - return $this->cache->get($config); - } - - public function remove($config) { - return $this->cache->remove($config); - } - - public function flush($config) { - return $this->cache->flush($config); - } - - public function cleanup($config) { - return $this->cache->cleanup($config); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php deleted file mode 100644 index d4cc35c4b..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator/Cleanup.php +++ /dev/null @@ -1,43 +0,0 @@ -definitions[$this->generateKey($config)] = $def; - return $status; - } - - public function set($def, $config) { - $status = parent::set($def, $config); - if ($status) $this->definitions[$this->generateKey($config)] = $def; - return $status; - } - - public function replace($def, $config) { - $status = parent::replace($def, $config); - if ($status) $this->definitions[$this->generateKey($config)] = $def; - return $status; - } - - public function get($config) { - $key = $this->generateKey($config); - if (isset($this->definitions[$key])) return $this->definitions[$key]; - $this->definitions[$key] = parent::get($config); - return $this->definitions[$key]; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator/Template.php.in b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator/Template.php.in deleted file mode 100644 index 21a8fcfda..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCache/Decorator/Template.php.in +++ /dev/null @@ -1,47 +0,0 @@ -checkDefType($def)) return; - $file = $this->generateFilePath($config); - if (file_exists($file)) return false; - if (!$this->_prepareDir($config)) return false; - return $this->_write($file, serialize($def), $config); - } - - public function set($def, $config) { - if (!$this->checkDefType($def)) return; - $file = $this->generateFilePath($config); - if (!$this->_prepareDir($config)) return false; - return $this->_write($file, serialize($def), $config); - } - - public function replace($def, $config) { - if (!$this->checkDefType($def)) return; - $file = $this->generateFilePath($config); - if (!file_exists($file)) return false; - if (!$this->_prepareDir($config)) return false; - return $this->_write($file, serialize($def), $config); - } - - public function get($config) { - $file = $this->generateFilePath($config); - if (!file_exists($file)) return false; - return unserialize(file_get_contents($file)); - } - - public function remove($config) { - $file = $this->generateFilePath($config); - if (!file_exists($file)) return false; - return unlink($file); - } - - public function flush($config) { - if (!$this->_prepareDir($config)) return false; - $dir = $this->generateDirectoryPath($config); - $dh = opendir($dir); - while (false !== ($filename = readdir($dh))) { - if (empty($filename)) continue; - if ($filename[0] === '.') continue; - unlink($dir . '/' . $filename); - } - } - - public function cleanup($config) { - if (!$this->_prepareDir($config)) return false; - $dir = $this->generateDirectoryPath($config); - $dh = opendir($dir); - while (false !== ($filename = readdir($dh))) { - if (empty($filename)) continue; - if ($filename[0] === '.') continue; - $key = substr($filename, 0, strlen($filename) - 4); - if ($this->isOld($key, $config)) unlink($dir . '/' . $filename); - } - } - - /** - * Generates the file path to the serial file corresponding to - * the configuration and definition name - * @todo Make protected - */ - public function generateFilePath($config) { - $key = $this->generateKey($config); - return $this->generateDirectoryPath($config) . '/' . $key . '.ser'; - } - - /** - * Generates the path to the directory contain this cache's serial files - * @note No trailing slash - * @todo Make protected - */ - public function generateDirectoryPath($config) { - $base = $this->generateBaseDirectoryPath($config); - return $base . '/' . $this->type; - } - - /** - * Generates path to base directory that contains all definition type - * serials - * @todo Make protected - */ - public function generateBaseDirectoryPath($config) { - $base = $config->get('Cache.SerializerPath'); - $base = is_null($base) ? HTMLPURIFIER_PREFIX . '/HTMLPurifier/DefinitionCache/Serializer' : $base; - return $base; - } - - /** - * Convenience wrapper function for file_put_contents - * @param $file File name to write to - * @param $data Data to write into file - * @param $config Config object - * @return Number of bytes written if success, or false if failure. - */ - private function _write($file, $data, $config) { - $result = file_put_contents($file, $data); - if ($result !== false) { - // set permissions of the new file (no execute) - $chmod = $config->get('Cache.SerializerPermissions'); - if (!$chmod) { - $chmod = 0644; // invalid config or simpletest - } - $chmod = $chmod & 0666; - chmod($file, $chmod); - } - return $result; - } - - /** - * Prepares the directory that this type stores the serials in - * @param $config Config object - * @return True if successful - */ - private function _prepareDir($config) { - $directory = $this->generateDirectoryPath($config); - $chmod = $config->get('Cache.SerializerPermissions'); - if (!$chmod) { - $chmod = 0755; // invalid config or simpletest - } - if (!is_dir($directory)) { - $base = $this->generateBaseDirectoryPath($config); - if (!is_dir($base)) { - trigger_error('Base directory '.$base.' does not exist, - please create or change using %Cache.SerializerPath', - E_USER_WARNING); - return false; - } elseif (!$this->_testPermissions($base, $chmod)) { - return false; - } - $old = umask(0000); - mkdir($directory, $chmod); - umask($old); - } elseif (!$this->_testPermissions($directory, $chmod)) { - return false; - } - return true; - } - - /** - * Tests permissions on a directory and throws out friendly - * error messages and attempts to chmod it itself if possible - * @param $dir Directory path - * @param $chmod Permissions - * @return True if directory writable - */ - private function _testPermissions($dir, $chmod) { - // early abort, if it is writable, everything is hunky-dory - if (is_writable($dir)) return true; - if (!is_dir($dir)) { - // generally, you'll want to handle this beforehand - // so a more specific error message can be given - trigger_error('Directory '.$dir.' does not exist', - E_USER_WARNING); - return false; - } - if (function_exists('posix_getuid')) { - // POSIX system, we can give more specific advice - if (fileowner($dir) === posix_getuid()) { - // we can chmod it ourselves - $chmod = $chmod | 0700; - if (chmod($dir, $chmod)) return true; - } elseif (filegroup($dir) === posix_getgid()) { - $chmod = $chmod | 0070; - } else { - // PHP's probably running as nobody, so we'll - // need to give global permissions - $chmod = $chmod | 0777; - } - trigger_error('Directory '.$dir.' not writable, '. - 'please chmod to ' . decoct($chmod), - E_USER_WARNING); - } else { - // generic error message - trigger_error('Directory '.$dir.' not writable, '. - 'please alter file permissions', - E_USER_WARNING); - } - return false; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCache/Serializer/README b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCache/Serializer/README deleted file mode 100644 index 2e35c1c3d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCache/Serializer/README +++ /dev/null @@ -1,3 +0,0 @@ -This is a dummy file to prevent Git from ignoring this empty directory. - - vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCacheFactory.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCacheFactory.php deleted file mode 100644 index a6ead6281..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DefinitionCacheFactory.php +++ /dev/null @@ -1,91 +0,0 @@ - array()); - protected $implementations = array(); - protected $decorators = array(); - - /** - * Initialize default decorators - */ - public function setup() { - $this->addDecorator('Cleanup'); - } - - /** - * Retrieves an instance of global definition cache factory. - */ - public static function instance($prototype = null) { - static $instance; - if ($prototype !== null) { - $instance = $prototype; - } elseif ($instance === null || $prototype === true) { - $instance = new HTMLPurifier_DefinitionCacheFactory(); - $instance->setup(); - } - return $instance; - } - - /** - * Registers a new definition cache object - * @param $short Short name of cache object, for reference - * @param $long Full class name of cache object, for construction - */ - public function register($short, $long) { - $this->implementations[$short] = $long; - } - - /** - * Factory method that creates a cache object based on configuration - * @param $name Name of definitions handled by cache - * @param $config Instance of HTMLPurifier_Config - */ - public function create($type, $config) { - $method = $config->get('Cache.DefinitionImpl'); - if ($method === null) { - return new HTMLPurifier_DefinitionCache_Null($type); - } - if (!empty($this->caches[$method][$type])) { - return $this->caches[$method][$type]; - } - if ( - isset($this->implementations[$method]) && - class_exists($class = $this->implementations[$method], false) - ) { - $cache = new $class($type); - } else { - if ($method != 'Serializer') { - trigger_error("Unrecognized DefinitionCache $method, using Serializer instead", E_USER_WARNING); - } - $cache = new HTMLPurifier_DefinitionCache_Serializer($type); - } - foreach ($this->decorators as $decorator) { - $new_cache = $decorator->decorate($cache); - // prevent infinite recursion in PHP 4 - unset($cache); - $cache = $new_cache; - } - $this->caches[$method][$type] = $cache; - return $this->caches[$method][$type]; - } - - /** - * Registers a decorator to add to all new cache objects - * @param - */ - public function addDecorator($decorator) { - if (is_string($decorator)) { - $class = "HTMLPurifier_DefinitionCache_Decorator_$decorator"; - $decorator = new $class; - } - $this->decorators[$decorator->name] = $decorator; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Doctype.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Doctype.php deleted file mode 100644 index 1e3c574c0..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Doctype.php +++ /dev/null @@ -1,60 +0,0 @@ -renderDoctype. - * If structure changes, please update that function. - */ -class HTMLPurifier_Doctype -{ - /** - * Full name of doctype - */ - public $name; - - /** - * List of standard modules (string identifiers or literal objects) - * that this doctype uses - */ - public $modules = array(); - - /** - * List of modules to use for tidying up code - */ - public $tidyModules = array(); - - /** - * Is the language derived from XML (i.e. XHTML)? - */ - public $xml = true; - - /** - * List of aliases for this doctype - */ - public $aliases = array(); - - /** - * Public DTD identifier - */ - public $dtdPublic; - - /** - * System DTD identifier - */ - public $dtdSystem; - - public function __construct($name = null, $xml = true, $modules = array(), - $tidyModules = array(), $aliases = array(), $dtd_public = null, $dtd_system = null - ) { - $this->name = $name; - $this->xml = $xml; - $this->modules = $modules; - $this->tidyModules = $tidyModules; - $this->aliases = $aliases; - $this->dtdPublic = $dtd_public; - $this->dtdSystem = $dtd_system; - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DoctypeRegistry.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DoctypeRegistry.php deleted file mode 100644 index 86049e939..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/DoctypeRegistry.php +++ /dev/null @@ -1,103 +0,0 @@ -doctypes[$doctype->name] = $doctype; - $name = $doctype->name; - // hookup aliases - foreach ($doctype->aliases as $alias) { - if (isset($this->doctypes[$alias])) continue; - $this->aliases[$alias] = $name; - } - // remove old aliases - if (isset($this->aliases[$name])) unset($this->aliases[$name]); - return $doctype; - } - - /** - * Retrieves reference to a doctype of a certain name - * @note This function resolves aliases - * @note When possible, use the more fully-featured make() - * @param $doctype Name of doctype - * @return Editable doctype object - */ - public function get($doctype) { - if (isset($this->aliases[$doctype])) $doctype = $this->aliases[$doctype]; - if (!isset($this->doctypes[$doctype])) { - trigger_error('Doctype ' . htmlspecialchars($doctype) . ' does not exist', E_USER_ERROR); - $anon = new HTMLPurifier_Doctype($doctype); - return $anon; - } - return $this->doctypes[$doctype]; - } - - /** - * Creates a doctype based on a configuration object, - * will perform initialization on the doctype - * @note Use this function to get a copy of doctype that config - * can hold on to (this is necessary in order to tell - * Generator whether or not the current document is XML - * based or not). - */ - public function make($config) { - return clone $this->get($this->getDoctypeFromConfig($config)); - } - - /** - * Retrieves the doctype from the configuration object - */ - public function getDoctypeFromConfig($config) { - // recommended test - $doctype = $config->get('HTML.Doctype'); - if (!empty($doctype)) return $doctype; - $doctype = $config->get('HTML.CustomDoctype'); - if (!empty($doctype)) return $doctype; - // backwards-compatibility - if ($config->get('HTML.XHTML')) { - $doctype = 'XHTML 1.0'; - } else { - $doctype = 'HTML 4.01'; - } - if ($config->get('HTML.Strict')) { - $doctype .= ' Strict'; - } else { - $doctype .= ' Transitional'; - } - return $doctype; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ElementDef.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ElementDef.php deleted file mode 100644 index 10f7ab7f8..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ElementDef.php +++ /dev/null @@ -1,195 +0,0 @@ -setup(), this array may also - * contain an array at index 0 that indicates which attribute - * collections to load into the full array. It may also - * contain string indentifiers in lieu of HTMLPurifier_AttrDef, - * see HTMLPurifier_AttrTypes on how they are expanded during - * HTMLPurifier_HTMLDefinition->setup() processing. - */ - public $attr = array(); - - // XXX: Design note: currently, it's not possible to override - // previously defined AttrTransforms without messing around with - // the final generated config. This is by design; a previous version - // used an associated list of attr_transform, but it was extremely - // easy to accidentally override other attribute transforms by - // forgetting to specify an index (and just using 0.) While we - // could check this by checking the index number and complaining, - // there is a second problem which is that it is not at all easy to - // tell when something is getting overridden. Combine this with a - // codebase where this isn't really being used, and it's perfect for - // nuking. - - /** - * List of tags HTMLPurifier_AttrTransform to be done before validation - */ - public $attr_transform_pre = array(); - - /** - * List of tags HTMLPurifier_AttrTransform to be done after validation - */ - public $attr_transform_post = array(); - - /** - * HTMLPurifier_ChildDef of this tag. - */ - public $child; - - /** - * Abstract string representation of internal ChildDef rules. See - * HTMLPurifier_ContentSets for how this is parsed and then transformed - * into an HTMLPurifier_ChildDef. - * @warning This is a temporary variable that is not available after - * being processed by HTMLDefinition - */ - public $content_model; - - /** - * Value of $child->type, used to determine which ChildDef to use, - * used in combination with $content_model. - * @warning This must be lowercase - * @warning This is a temporary variable that is not available after - * being processed by HTMLDefinition - */ - public $content_model_type; - - - - /** - * Does the element have a content model (#PCDATA | Inline)*? This - * is important for chameleon ins and del processing in - * HTMLPurifier_ChildDef_Chameleon. Dynamically set: modules don't - * have to worry about this one. - */ - public $descendants_are_inline = false; - - /** - * List of the names of required attributes this element has. Dynamically - * populated by HTMLPurifier_HTMLDefinition::getElement - */ - public $required_attr = array(); - - /** - * Lookup table of tags excluded from all descendants of this tag. - * @note SGML permits exclusions for all descendants, but this is - * not possible with DTDs or XML Schemas. W3C has elected to - * use complicated compositions of content_models to simulate - * exclusion for children, but we go the simpler, SGML-style - * route of flat-out exclusions, which correctly apply to - * all descendants and not just children. Note that the XHTML - * Modularization Abstract Modules are blithely unaware of such - * distinctions. - */ - public $excludes = array(); - - /** - * This tag is explicitly auto-closed by the following tags. - */ - public $autoclose = array(); - - /** - * If a foreign element is found in this element, test if it is - * allowed by this sub-element; if it is, instead of closing the - * current element, place it inside this element. - */ - public $wrap; - - /** - * Whether or not this is a formatting element affected by the - * "Active Formatting Elements" algorithm. - */ - public $formatting; - - /** - * Low-level factory constructor for creating new standalone element defs - */ - public static function create($content_model, $content_model_type, $attr) { - $def = new HTMLPurifier_ElementDef(); - $def->content_model = $content_model; - $def->content_model_type = $content_model_type; - $def->attr = $attr; - return $def; - } - - /** - * Merges the values of another element definition into this one. - * Values from the new element def take precedence if a value is - * not mergeable. - */ - public function mergeIn($def) { - - // later keys takes precedence - foreach($def->attr as $k => $v) { - if ($k === 0) { - // merge in the includes - // sorry, no way to override an include - foreach ($v as $v2) { - $this->attr[0][] = $v2; - } - continue; - } - if ($v === false) { - if (isset($this->attr[$k])) unset($this->attr[$k]); - continue; - } - $this->attr[$k] = $v; - } - $this->_mergeAssocArray($this->excludes, $def->excludes); - $this->attr_transform_pre = array_merge($this->attr_transform_pre, $def->attr_transform_pre); - $this->attr_transform_post = array_merge($this->attr_transform_post, $def->attr_transform_post); - - if(!empty($def->content_model)) { - $this->content_model = - str_replace("#SUPER", $this->content_model, $def->content_model); - $this->child = false; - } - if(!empty($def->content_model_type)) { - $this->content_model_type = $def->content_model_type; - $this->child = false; - } - if(!is_null($def->child)) $this->child = $def->child; - if(!is_null($def->formatting)) $this->formatting = $def->formatting; - if($def->descendants_are_inline) $this->descendants_are_inline = $def->descendants_are_inline; - - } - - /** - * Merges one array into another, removes values which equal false - * @param $a1 Array by reference that is merged into - * @param $a2 Array that merges into $a1 - */ - private function _mergeAssocArray(&$a1, $a2) { - foreach ($a2 as $k => $v) { - if ($v === false) { - if (isset($a1[$k])) unset($a1[$k]); - continue; - } - $a1[$k] = $v; - } - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Encoder.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Encoder.php deleted file mode 100644 index 77988a192..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Encoder.php +++ /dev/null @@ -1,545 +0,0 @@ -= $c) { - $r .= self::unsafeIconv($in, $out, substr($text, $i)); - break; - } - // wibble the boundary - if (0x80 != (0xC0 & ord($text[$i + $max_chunk_size]))) { - $chunk_size = $max_chunk_size; - } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 1]))) { - $chunk_size = $max_chunk_size - 1; - } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 2]))) { - $chunk_size = $max_chunk_size - 2; - } elseif (0x80 != (0xC0 & ord($text[$i + $max_chunk_size - 3]))) { - $chunk_size = $max_chunk_size - 3; - } else { - return false; // rather confusing UTF-8... - } - $chunk = substr($text, $i, $chunk_size); // substr doesn't mind overlong lengths - $r .= self::unsafeIconv($in, $out, $chunk); - $i += $chunk_size; - } - return $r; - } else { - return false; - } - } else { - return false; - } - } - - /** - * Cleans a UTF-8 string for well-formedness and SGML validity - * - * It will parse according to UTF-8 and return a valid UTF8 string, with - * non-SGML codepoints excluded. - * - * @note Just for reference, the non-SGML code points are 0 to 31 and - * 127 to 159, inclusive. However, we allow code points 9, 10 - * and 13, which are the tab, line feed and carriage return - * respectively. 128 and above the code points map to multibyte - * UTF-8 representations. - * - * @note Fallback code adapted from utf8ToUnicode by Henri Sivonen and - * hsivonen@iki.fi at under the - * LGPL license. Notes on what changed are inside, but in general, - * the original code transformed UTF-8 text into an array of integer - * Unicode codepoints. Understandably, transforming that back to - * a string would be somewhat expensive, so the function was modded to - * directly operate on the string. However, this discourages code - * reuse, and the logic enumerated here would be useful for any - * function that needs to be able to understand UTF-8 characters. - * As of right now, only smart lossless character encoding converters - * would need that, and I'm probably not going to implement them. - * Once again, PHP 6 should solve all our problems. - */ - public static function cleanUTF8($str, $force_php = false) { - - // UTF-8 validity is checked since PHP 4.3.5 - // This is an optimization: if the string is already valid UTF-8, no - // need to do PHP stuff. 99% of the time, this will be the case. - // The regexp matches the XML char production, as well as well as excluding - // non-SGML codepoints U+007F to U+009F - if (preg_match('/^[\x{9}\x{A}\x{D}\x{20}-\x{7E}\x{A0}-\x{D7FF}\x{E000}-\x{FFFD}\x{10000}-\x{10FFFF}]*$/Du', $str)) { - return $str; - } - - $mState = 0; // cached expected number of octets after the current octet - // until the beginning of the next UTF8 character sequence - $mUcs4 = 0; // cached Unicode character - $mBytes = 1; // cached expected number of octets in the current sequence - - // original code involved an $out that was an array of Unicode - // codepoints. Instead of having to convert back into UTF-8, we've - // decided to directly append valid UTF-8 characters onto a string - // $out once they're done. $char accumulates raw bytes, while $mUcs4 - // turns into the Unicode code point, so there's some redundancy. - - $out = ''; - $char = ''; - - $len = strlen($str); - for($i = 0; $i < $len; $i++) { - $in = ord($str{$i}); - $char .= $str[$i]; // append byte to char - if (0 == $mState) { - // When mState is zero we expect either a US-ASCII character - // or a multi-octet sequence. - if (0 == (0x80 & ($in))) { - // US-ASCII, pass straight through. - if (($in <= 31 || $in == 127) && - !($in == 9 || $in == 13 || $in == 10) // save \r\t\n - ) { - // control characters, remove - } else { - $out .= $char; - } - // reset - $char = ''; - $mBytes = 1; - } elseif (0xC0 == (0xE0 & ($in))) { - // First octet of 2 octet sequence - $mUcs4 = ($in); - $mUcs4 = ($mUcs4 & 0x1F) << 6; - $mState = 1; - $mBytes = 2; - } elseif (0xE0 == (0xF0 & ($in))) { - // First octet of 3 octet sequence - $mUcs4 = ($in); - $mUcs4 = ($mUcs4 & 0x0F) << 12; - $mState = 2; - $mBytes = 3; - } elseif (0xF0 == (0xF8 & ($in))) { - // First octet of 4 octet sequence - $mUcs4 = ($in); - $mUcs4 = ($mUcs4 & 0x07) << 18; - $mState = 3; - $mBytes = 4; - } elseif (0xF8 == (0xFC & ($in))) { - // First octet of 5 octet sequence. - // - // This is illegal because the encoded codepoint must be - // either: - // (a) not the shortest form or - // (b) outside the Unicode range of 0-0x10FFFF. - // Rather than trying to resynchronize, we will carry on - // until the end of the sequence and let the later error - // handling code catch it. - $mUcs4 = ($in); - $mUcs4 = ($mUcs4 & 0x03) << 24; - $mState = 4; - $mBytes = 5; - } elseif (0xFC == (0xFE & ($in))) { - // First octet of 6 octet sequence, see comments for 5 - // octet sequence. - $mUcs4 = ($in); - $mUcs4 = ($mUcs4 & 1) << 30; - $mState = 5; - $mBytes = 6; - } else { - // Current octet is neither in the US-ASCII range nor a - // legal first octet of a multi-octet sequence. - $mState = 0; - $mUcs4 = 0; - $mBytes = 1; - $char = ''; - } - } else { - // When mState is non-zero, we expect a continuation of the - // multi-octet sequence - if (0x80 == (0xC0 & ($in))) { - // Legal continuation. - $shift = ($mState - 1) * 6; - $tmp = $in; - $tmp = ($tmp & 0x0000003F) << $shift; - $mUcs4 |= $tmp; - - if (0 == --$mState) { - // End of the multi-octet sequence. mUcs4 now contains - // the final Unicode codepoint to be output - - // Check for illegal sequences and codepoints. - - // From Unicode 3.1, non-shortest form is illegal - if (((2 == $mBytes) && ($mUcs4 < 0x0080)) || - ((3 == $mBytes) && ($mUcs4 < 0x0800)) || - ((4 == $mBytes) && ($mUcs4 < 0x10000)) || - (4 < $mBytes) || - // From Unicode 3.2, surrogate characters = illegal - (($mUcs4 & 0xFFFFF800) == 0xD800) || - // Codepoints outside the Unicode range are illegal - ($mUcs4 > 0x10FFFF) - ) { - - } elseif (0xFEFF != $mUcs4 && // omit BOM - // check for valid Char unicode codepoints - ( - 0x9 == $mUcs4 || - 0xA == $mUcs4 || - 0xD == $mUcs4 || - (0x20 <= $mUcs4 && 0x7E >= $mUcs4) || - // 7F-9F is not strictly prohibited by XML, - // but it is non-SGML, and thus we don't allow it - (0xA0 <= $mUcs4 && 0xD7FF >= $mUcs4) || - (0x10000 <= $mUcs4 && 0x10FFFF >= $mUcs4) - ) - ) { - $out .= $char; - } - // initialize UTF8 cache (reset) - $mState = 0; - $mUcs4 = 0; - $mBytes = 1; - $char = ''; - } - } else { - // ((0xC0 & (*in) != 0x80) && (mState != 0)) - // Incomplete multi-octet sequence. - // used to result in complete fail, but we'll reset - $mState = 0; - $mUcs4 = 0; - $mBytes = 1; - $char =''; - } - } - } - return $out; - } - - /** - * Translates a Unicode codepoint into its corresponding UTF-8 character. - * @note Based on Feyd's function at - * , - * which is in public domain. - * @note While we're going to do code point parsing anyway, a good - * optimization would be to refuse to translate code points that - * are non-SGML characters. However, this could lead to duplication. - * @note This is very similar to the unichr function in - * maintenance/generate-entity-file.php (although this is superior, - * due to its sanity checks). - */ - - // +----------+----------+----------+----------+ - // | 33222222 | 22221111 | 111111 | | - // | 10987654 | 32109876 | 54321098 | 76543210 | bit - // +----------+----------+----------+----------+ - // | | | | 0xxxxxxx | 1 byte 0x00000000..0x0000007F - // | | | 110yyyyy | 10xxxxxx | 2 byte 0x00000080..0x000007FF - // | | 1110zzzz | 10yyyyyy | 10xxxxxx | 3 byte 0x00000800..0x0000FFFF - // | 11110www | 10wwzzzz | 10yyyyyy | 10xxxxxx | 4 byte 0x00010000..0x0010FFFF - // +----------+----------+----------+----------+ - // | 00000000 | 00011111 | 11111111 | 11111111 | Theoretical upper limit of legal scalars: 2097151 (0x001FFFFF) - // | 00000000 | 00010000 | 11111111 | 11111111 | Defined upper limit of legal scalar codes - // +----------+----------+----------+----------+ - - public static function unichr($code) { - if($code > 1114111 or $code < 0 or - ($code >= 55296 and $code <= 57343) ) { - // bits are set outside the "valid" range as defined - // by UNICODE 4.1.0 - return ''; - } - - $x = $y = $z = $w = 0; - if ($code < 128) { - // regular ASCII character - $x = $code; - } else { - // set up bits for UTF-8 - $x = ($code & 63) | 128; - if ($code < 2048) { - $y = (($code & 2047) >> 6) | 192; - } else { - $y = (($code & 4032) >> 6) | 128; - if($code < 65536) { - $z = (($code >> 12) & 15) | 224; - } else { - $z = (($code >> 12) & 63) | 128; - $w = (($code >> 18) & 7) | 240; - } - } - } - // set up the actual character - $ret = ''; - if($w) $ret .= chr($w); - if($z) $ret .= chr($z); - if($y) $ret .= chr($y); - $ret .= chr($x); - - return $ret; - } - - public static function iconvAvailable() { - static $iconv = null; - if ($iconv === null) { - $iconv = function_exists('iconv') && self::testIconvTruncateBug() != self::ICONV_UNUSABLE; - } - return $iconv; - } - - /** - * Converts a string to UTF-8 based on configuration. - */ - public static function convertToUTF8($str, $config, $context) { - $encoding = $config->get('Core.Encoding'); - if ($encoding === 'utf-8') return $str; - static $iconv = null; - if ($iconv === null) $iconv = self::iconvAvailable(); - if ($iconv && !$config->get('Test.ForceNoIconv')) { - // unaffected by bugs, since UTF-8 support all characters - $str = self::unsafeIconv($encoding, 'utf-8//IGNORE', $str); - if ($str === false) { - // $encoding is not a valid encoding - trigger_error('Invalid encoding ' . $encoding, E_USER_ERROR); - return ''; - } - // If the string is bjorked by Shift_JIS or a similar encoding - // that doesn't support all of ASCII, convert the naughty - // characters to their true byte-wise ASCII/UTF-8 equivalents. - $str = strtr($str, self::testEncodingSupportsASCII($encoding)); - return $str; - } elseif ($encoding === 'iso-8859-1') { - $str = utf8_encode($str); - return $str; - } - $bug = HTMLPurifier_Encoder::testIconvTruncateBug(); - if ($bug == self::ICONV_OK) { - trigger_error('Encoding not supported, please install iconv', E_USER_ERROR); - } else { - trigger_error('You have a buggy version of iconv, see https://bugs.php.net/bug.php?id=48147 and http://sourceware.org/bugzilla/show_bug.cgi?id=13541', E_USER_ERROR); - } - } - - /** - * Converts a string from UTF-8 based on configuration. - * @note Currently, this is a lossy conversion, with unexpressable - * characters being omitted. - */ - public static function convertFromUTF8($str, $config, $context) { - $encoding = $config->get('Core.Encoding'); - if ($escape = $config->get('Core.EscapeNonASCIICharacters')) { - $str = self::convertToASCIIDumbLossless($str); - } - if ($encoding === 'utf-8') return $str; - static $iconv = null; - if ($iconv === null) $iconv = self::iconvAvailable(); - if ($iconv && !$config->get('Test.ForceNoIconv')) { - // Undo our previous fix in convertToUTF8, otherwise iconv will barf - $ascii_fix = self::testEncodingSupportsASCII($encoding); - if (!$escape && !empty($ascii_fix)) { - $clear_fix = array(); - foreach ($ascii_fix as $utf8 => $native) $clear_fix[$utf8] = ''; - $str = strtr($str, $clear_fix); - } - $str = strtr($str, array_flip($ascii_fix)); - // Normal stuff - $str = self::iconv('utf-8', $encoding . '//IGNORE', $str); - return $str; - } elseif ($encoding === 'iso-8859-1') { - $str = utf8_decode($str); - return $str; - } - trigger_error('Encoding not supported', E_USER_ERROR); - // You might be tempted to assume that the ASCII representation - // might be OK, however, this is *not* universally true over all - // encodings. So we take the conservative route here, rather - // than forcibly turn on %Core.EscapeNonASCIICharacters - } - - /** - * Lossless (character-wise) conversion of HTML to ASCII - * @param $str UTF-8 string to be converted to ASCII - * @returns ASCII encoded string with non-ASCII character entity-ized - * @warning Adapted from MediaWiki, claiming fair use: this is a common - * algorithm. If you disagree with this license fudgery, - * implement it yourself. - * @note Uses decimal numeric entities since they are best supported. - * @note This is a DUMB function: it has no concept of keeping - * character entities that the projected character encoding - * can allow. We could possibly implement a smart version - * but that would require it to also know which Unicode - * codepoints the charset supported (not an easy task). - * @note Sort of with cleanUTF8() but it assumes that $str is - * well-formed UTF-8 - */ - public static function convertToASCIIDumbLossless($str) { - $bytesleft = 0; - $result = ''; - $working = 0; - $len = strlen($str); - for( $i = 0; $i < $len; $i++ ) { - $bytevalue = ord( $str[$i] ); - if( $bytevalue <= 0x7F ) { //0xxx xxxx - $result .= chr( $bytevalue ); - $bytesleft = 0; - } elseif( $bytevalue <= 0xBF ) { //10xx xxxx - $working = $working << 6; - $working += ($bytevalue & 0x3F); - $bytesleft--; - if( $bytesleft <= 0 ) { - $result .= "&#" . $working . ";"; - } - } elseif( $bytevalue <= 0xDF ) { //110x xxxx - $working = $bytevalue & 0x1F; - $bytesleft = 1; - } elseif( $bytevalue <= 0xEF ) { //1110 xxxx - $working = $bytevalue & 0x0F; - $bytesleft = 2; - } else { //1111 0xxx - $working = $bytevalue & 0x07; - $bytesleft = 3; - } - } - return $result; - } - - /** No bugs detected in iconv. */ - const ICONV_OK = 0; - - /** Iconv truncates output if converting from UTF-8 to another - * character set with //IGNORE, and a non-encodable character is found */ - const ICONV_TRUNCATES = 1; - - /** Iconv does not support //IGNORE, making it unusable for - * transcoding purposes */ - const ICONV_UNUSABLE = 2; - - /** - * glibc iconv has a known bug where it doesn't handle the magic - * //IGNORE stanza correctly. In particular, rather than ignore - * characters, it will return an EILSEQ after consuming some number - * of characters, and expect you to restart iconv as if it were - * an E2BIG. Old versions of PHP did not respect the errno, and - * returned the fragment, so as a result you would see iconv - * mysteriously truncating output. We can work around this by - * manually chopping our input into segments of about 8000 - * characters, as long as PHP ignores the error code. If PHP starts - * paying attention to the error code, iconv becomes unusable. - * - * @returns Error code indicating severity of bug. - */ - public static function testIconvTruncateBug() { - static $code = null; - if ($code === null) { - // better not use iconv, otherwise infinite loop! - $r = self::unsafeIconv('utf-8', 'ascii//IGNORE', "\xCE\xB1" . str_repeat('a', 9000)); - if ($r === false) { - $code = self::ICONV_UNUSABLE; - } elseif (($c = strlen($r)) < 9000) { - $code = self::ICONV_TRUNCATES; - } elseif ($c > 9000) { - trigger_error('Your copy of iconv is extremely buggy. Please notify HTML Purifier maintainers: include your iconv version as per phpversion()', E_USER_ERROR); - } else { - $code = self::ICONV_OK; - } - } - return $code; - } - - /** - * This expensive function tests whether or not a given character - * encoding supports ASCII. 7/8-bit encodings like Shift_JIS will - * fail this test, and require special processing. Variable width - * encodings shouldn't ever fail. - * - * @param string $encoding Encoding name to test, as per iconv format - * @param bool $bypass Whether or not to bypass the precompiled arrays. - * @return Array of UTF-8 characters to their corresponding ASCII, - * which can be used to "undo" any overzealous iconv action. - */ - public static function testEncodingSupportsASCII($encoding, $bypass = false) { - // All calls to iconv here are unsafe, proof by case analysis: - // If ICONV_OK, no difference. - // If ICONV_TRUNCATE, all calls involve one character inputs, - // so bug is not triggered. - // If ICONV_UNUSABLE, this call is irrelevant - static $encodings = array(); - if (!$bypass) { - if (isset($encodings[$encoding])) return $encodings[$encoding]; - $lenc = strtolower($encoding); - switch ($lenc) { - case 'shift_jis': - return array("\xC2\xA5" => '\\', "\xE2\x80\xBE" => '~'); - case 'johab': - return array("\xE2\x82\xA9" => '\\'); - } - if (strpos($lenc, 'iso-8859-') === 0) return array(); - } - $ret = array(); - if (self::unsafeIconv('UTF-8', $encoding, 'a') === false) return false; - for ($i = 0x20; $i <= 0x7E; $i++) { // all printable ASCII chars - $c = chr($i); // UTF-8 char - $r = self::unsafeIconv('UTF-8', "$encoding//IGNORE", $c); // initial conversion - if ( - $r === '' || - // This line is needed for iconv implementations that do not - // omit characters that do not exist in the target character set - ($r === $c && self::unsafeIconv($encoding, 'UTF-8//IGNORE', $r) !== $c) - ) { - // Reverse engineer: what's the UTF-8 equiv of this byte - // sequence? This assumes that there's no variable width - // encoding that doesn't support ASCII. - $ret[self::unsafeIconv($encoding, 'UTF-8//IGNORE', $c)] = $c; - } - } - $encodings[$encoding] = $ret; - return $ret; - } - - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/EntityLookup.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/EntityLookup.php deleted file mode 100644 index b4dfce94c..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/EntityLookup.php +++ /dev/null @@ -1,44 +0,0 @@ -table = unserialize(file_get_contents($file)); - } - - /** - * Retrieves sole instance of the object. - * @param Optional prototype of custom lookup table to overload with. - */ - public static function instance($prototype = false) { - // no references, since PHP doesn't copy unless modified - static $instance = null; - if ($prototype) { - $instance = $prototype; - } elseif (!$instance) { - $instance = new HTMLPurifier_EntityLookup(); - $instance->setup(); - } - return $instance; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/EntityLookup/entities.ser b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/EntityLookup/entities.ser deleted file mode 100644 index e8b08128b..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/EntityLookup/entities.ser +++ /dev/null @@ -1 +0,0 @@ -a:253:{s:4:"fnof";s:2:"Æ’";s:5:"Alpha";s:2:"Α";s:4:"Beta";s:2:"Î’";s:5:"Gamma";s:2:"Γ";s:5:"Delta";s:2:"Δ";s:7:"Epsilon";s:2:"Ε";s:4:"Zeta";s:2:"Ζ";s:3:"Eta";s:2:"Η";s:5:"Theta";s:2:"Θ";s:4:"Iota";s:2:"Ι";s:5:"Kappa";s:2:"Κ";s:6:"Lambda";s:2:"Λ";s:2:"Mu";s:2:"Μ";s:2:"Nu";s:2:"Î";s:2:"Xi";s:2:"Ξ";s:7:"Omicron";s:2:"Ο";s:2:"Pi";s:2:"Π";s:3:"Rho";s:2:"Ρ";s:5:"Sigma";s:2:"Σ";s:3:"Tau";s:2:"Τ";s:7:"Upsilon";s:2:"Î¥";s:3:"Phi";s:2:"Φ";s:3:"Chi";s:2:"Χ";s:3:"Psi";s:2:"Ψ";s:5:"Omega";s:2:"Ω";s:5:"alpha";s:2:"α";s:4:"beta";s:2:"β";s:5:"gamma";s:2:"γ";s:5:"delta";s:2:"δ";s:7:"epsilon";s:2:"ε";s:4:"zeta";s:2:"ζ";s:3:"eta";s:2:"η";s:5:"theta";s:2:"θ";s:4:"iota";s:2:"ι";s:5:"kappa";s:2:"κ";s:6:"lambda";s:2:"λ";s:2:"mu";s:2:"μ";s:2:"nu";s:2:"ν";s:2:"xi";s:2:"ξ";s:7:"omicron";s:2:"ο";s:2:"pi";s:2:"Ï€";s:3:"rho";s:2:"Ï";s:6:"sigmaf";s:2:"Ï‚";s:5:"sigma";s:2:"σ";s:3:"tau";s:2:"Ï„";s:7:"upsilon";s:2:"Ï…";s:3:"phi";s:2:"φ";s:3:"chi";s:2:"χ";s:3:"psi";s:2:"ψ";s:5:"omega";s:2:"ω";s:8:"thetasym";s:2:"Ï‘";s:5:"upsih";s:2:"Ï’";s:3:"piv";s:2:"Ï–";s:4:"bull";s:3:"•";s:6:"hellip";s:3:"…";s:5:"prime";s:3:"′";s:5:"Prime";s:3:"″";s:5:"oline";s:3:"‾";s:5:"frasl";s:3:"â„";s:6:"weierp";s:3:"℘";s:5:"image";s:3:"â„‘";s:4:"real";s:3:"ℜ";s:5:"trade";s:3:"â„¢";s:7:"alefsym";s:3:"ℵ";s:4:"larr";s:3:"â†";s:4:"uarr";s:3:"↑";s:4:"rarr";s:3:"→";s:4:"darr";s:3:"↓";s:4:"harr";s:3:"↔";s:5:"crarr";s:3:"↵";s:4:"lArr";s:3:"â‡";s:4:"uArr";s:3:"⇑";s:4:"rArr";s:3:"⇒";s:4:"dArr";s:3:"⇓";s:4:"hArr";s:3:"⇔";s:6:"forall";s:3:"∀";s:4:"part";s:3:"∂";s:5:"exist";s:3:"∃";s:5:"empty";s:3:"∅";s:5:"nabla";s:3:"∇";s:4:"isin";s:3:"∈";s:5:"notin";s:3:"∉";s:2:"ni";s:3:"∋";s:4:"prod";s:3:"âˆ";s:3:"sum";s:3:"∑";s:5:"minus";s:3:"−";s:6:"lowast";s:3:"∗";s:5:"radic";s:3:"√";s:4:"prop";s:3:"âˆ";s:5:"infin";s:3:"∞";s:3:"ang";s:3:"∠";s:3:"and";s:3:"∧";s:2:"or";s:3:"∨";s:3:"cap";s:3:"∩";s:3:"cup";s:3:"∪";s:3:"int";s:3:"∫";s:6:"there4";s:3:"∴";s:3:"sim";s:3:"∼";s:4:"cong";s:3:"≅";s:5:"asymp";s:3:"≈";s:2:"ne";s:3:"≠";s:5:"equiv";s:3:"≡";s:2:"le";s:3:"≤";s:2:"ge";s:3:"≥";s:3:"sub";s:3:"⊂";s:3:"sup";s:3:"⊃";s:4:"nsub";s:3:"⊄";s:4:"sube";s:3:"⊆";s:4:"supe";s:3:"⊇";s:5:"oplus";s:3:"⊕";s:6:"otimes";s:3:"⊗";s:4:"perp";s:3:"⊥";s:4:"sdot";s:3:"â‹…";s:5:"lceil";s:3:"⌈";s:5:"rceil";s:3:"⌉";s:6:"lfloor";s:3:"⌊";s:6:"rfloor";s:3:"⌋";s:4:"lang";s:3:"〈";s:4:"rang";s:3:"〉";s:3:"loz";s:3:"â—Š";s:6:"spades";s:3:"â™ ";s:5:"clubs";s:3:"♣";s:6:"hearts";s:3:"♥";s:5:"diams";s:3:"♦";s:4:"quot";s:1:""";s:3:"amp";s:1:"&";s:2:"lt";s:1:"<";s:2:"gt";s:1:">";s:4:"apos";s:1:"'";s:5:"OElig";s:2:"Å’";s:5:"oelig";s:2:"Å“";s:6:"Scaron";s:2:"Å ";s:6:"scaron";s:2:"Å¡";s:4:"Yuml";s:2:"Ÿ";s:4:"circ";s:2:"ˆ";s:5:"tilde";s:2:"Ëœ";s:4:"ensp";s:3:" ";s:4:"emsp";s:3:" ";s:6:"thinsp";s:3:" ";s:4:"zwnj";s:3:"‌";s:3:"zwj";s:3:"â€";s:3:"lrm";s:3:"‎";s:3:"rlm";s:3:"â€";s:5:"ndash";s:3:"–";s:5:"mdash";s:3:"—";s:5:"lsquo";s:3:"‘";s:5:"rsquo";s:3:"’";s:5:"sbquo";s:3:"‚";s:5:"ldquo";s:3:"“";s:5:"rdquo";s:3:"â€";s:5:"bdquo";s:3:"„";s:6:"dagger";s:3:"†";s:6:"Dagger";s:3:"‡";s:6:"permil";s:3:"‰";s:6:"lsaquo";s:3:"‹";s:6:"rsaquo";s:3:"›";s:4:"euro";s:3:"€";s:4:"nbsp";s:2:" ";s:5:"iexcl";s:2:"¡";s:4:"cent";s:2:"¢";s:5:"pound";s:2:"£";s:6:"curren";s:2:"¤";s:3:"yen";s:2:"Â¥";s:6:"brvbar";s:2:"¦";s:4:"sect";s:2:"§";s:3:"uml";s:2:"¨";s:4:"copy";s:2:"©";s:4:"ordf";s:2:"ª";s:5:"laquo";s:2:"«";s:3:"not";s:2:"¬";s:3:"shy";s:2:"­";s:3:"reg";s:2:"®";s:4:"macr";s:2:"¯";s:3:"deg";s:2:"°";s:6:"plusmn";s:2:"±";s:4:"sup2";s:2:"²";s:4:"sup3";s:2:"³";s:5:"acute";s:2:"´";s:5:"micro";s:2:"µ";s:4:"para";s:2:"¶";s:6:"middot";s:2:"·";s:5:"cedil";s:2:"¸";s:4:"sup1";s:2:"¹";s:4:"ordm";s:2:"º";s:5:"raquo";s:2:"»";s:6:"frac14";s:2:"¼";s:6:"frac12";s:2:"½";s:6:"frac34";s:2:"¾";s:6:"iquest";s:2:"¿";s:6:"Agrave";s:2:"À";s:6:"Aacute";s:2:"Ã";s:5:"Acirc";s:2:"Â";s:6:"Atilde";s:2:"Ã";s:4:"Auml";s:2:"Ä";s:5:"Aring";s:2:"Ã…";s:5:"AElig";s:2:"Æ";s:6:"Ccedil";s:2:"Ç";s:6:"Egrave";s:2:"È";s:6:"Eacute";s:2:"É";s:5:"Ecirc";s:2:"Ê";s:4:"Euml";s:2:"Ë";s:6:"Igrave";s:2:"ÃŒ";s:6:"Iacute";s:2:"Ã";s:5:"Icirc";s:2:"ÃŽ";s:4:"Iuml";s:2:"Ã";s:3:"ETH";s:2:"Ã";s:6:"Ntilde";s:2:"Ñ";s:6:"Ograve";s:2:"Ã’";s:6:"Oacute";s:2:"Ó";s:5:"Ocirc";s:2:"Ô";s:6:"Otilde";s:2:"Õ";s:4:"Ouml";s:2:"Ö";s:5:"times";s:2:"×";s:6:"Oslash";s:2:"Ø";s:6:"Ugrave";s:2:"Ù";s:6:"Uacute";s:2:"Ú";s:5:"Ucirc";s:2:"Û";s:4:"Uuml";s:2:"Ü";s:6:"Yacute";s:2:"Ã";s:5:"THORN";s:2:"Þ";s:5:"szlig";s:2:"ß";s:6:"agrave";s:2:"à";s:6:"aacute";s:2:"á";s:5:"acirc";s:2:"â";s:6:"atilde";s:2:"ã";s:4:"auml";s:2:"ä";s:5:"aring";s:2:"Ã¥";s:5:"aelig";s:2:"æ";s:6:"ccedil";s:2:"ç";s:6:"egrave";s:2:"è";s:6:"eacute";s:2:"é";s:5:"ecirc";s:2:"ê";s:4:"euml";s:2:"ë";s:6:"igrave";s:2:"ì";s:6:"iacute";s:2:"í";s:5:"icirc";s:2:"î";s:4:"iuml";s:2:"ï";s:3:"eth";s:2:"ð";s:6:"ntilde";s:2:"ñ";s:6:"ograve";s:2:"ò";s:6:"oacute";s:2:"ó";s:5:"ocirc";s:2:"ô";s:6:"otilde";s:2:"õ";s:4:"ouml";s:2:"ö";s:6:"divide";s:2:"÷";s:6:"oslash";s:2:"ø";s:6:"ugrave";s:2:"ù";s:6:"uacute";s:2:"ú";s:5:"ucirc";s:2:"û";s:4:"uuml";s:2:"ü";s:6:"yacute";s:2:"ý";s:5:"thorn";s:2:"þ";s:4:"yuml";s:2:"ÿ";} \ No newline at end of file diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/EntityParser.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/EntityParser.php deleted file mode 100644 index 8c384472d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/EntityParser.php +++ /dev/null @@ -1,144 +0,0 @@ - '"', - 38 => '&', - 39 => "'", - 60 => '<', - 62 => '>' - ); - - /** - * Stripped entity names to decimal conversion table for special entities. - */ - protected $_special_ent2dec = - array( - 'quot' => 34, - 'amp' => 38, - 'lt' => 60, - 'gt' => 62 - ); - - /** - * Substitutes non-special entities with their parsed equivalents. Since - * running this whenever you have parsed character is t3h 5uck, we run - * it before everything else. - * - * @param $string String to have non-special entities parsed. - * @returns Parsed string. - */ - public function substituteNonSpecialEntities($string) { - // it will try to detect missing semicolons, but don't rely on it - return preg_replace_callback( - $this->_substituteEntitiesRegex, - array($this, 'nonSpecialEntityCallback'), - $string - ); - } - - /** - * Callback function for substituteNonSpecialEntities() that does the work. - * - * @param $matches PCRE matches array, with 0 the entire match, and - * either index 1, 2 or 3 set with a hex value, dec value, - * or string (respectively). - * @returns Replacement string. - */ - - protected function nonSpecialEntityCallback($matches) { - // replaces all but big five - $entity = $matches[0]; - $is_num = (@$matches[0][1] === '#'); - if ($is_num) { - $is_hex = (@$entity[2] === 'x'); - $code = $is_hex ? hexdec($matches[1]) : (int) $matches[2]; - - // abort for special characters - if (isset($this->_special_dec2str[$code])) return $entity; - - return HTMLPurifier_Encoder::unichr($code); - } else { - if (isset($this->_special_ent2dec[$matches[3]])) return $entity; - if (!$this->_entity_lookup) { - $this->_entity_lookup = HTMLPurifier_EntityLookup::instance(); - } - if (isset($this->_entity_lookup->table[$matches[3]])) { - return $this->_entity_lookup->table[$matches[3]]; - } else { - return $entity; - } - } - } - - /** - * Substitutes only special entities with their parsed equivalents. - * - * @notice We try to avoid calling this function because otherwise, it - * would have to be called a lot (for every parsed section). - * - * @param $string String to have non-special entities parsed. - * @returns Parsed string. - */ - public function substituteSpecialEntities($string) { - return preg_replace_callback( - $this->_substituteEntitiesRegex, - array($this, 'specialEntityCallback'), - $string); - } - - /** - * Callback function for substituteSpecialEntities() that does the work. - * - * This callback has same syntax as nonSpecialEntityCallback(). - * - * @param $matches PCRE-style matches array, with 0 the entire match, and - * either index 1, 2 or 3 set with a hex value, dec value, - * or string (respectively). - * @returns Replacement string. - */ - protected function specialEntityCallback($matches) { - $entity = $matches[0]; - $is_num = (@$matches[0][1] === '#'); - if ($is_num) { - $is_hex = (@$entity[2] === 'x'); - $int = $is_hex ? hexdec($matches[1]) : (int) $matches[2]; - return isset($this->_special_dec2str[$int]) ? - $this->_special_dec2str[$int] : - $entity; - } else { - return isset($this->_special_ent2dec[$matches[3]]) ? - $this->_special_ent2dec[$matches[3]] : - $entity; - } - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ErrorCollector.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ErrorCollector.php deleted file mode 100644 index 6713eaf77..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ErrorCollector.php +++ /dev/null @@ -1,209 +0,0 @@ -locale =& $context->get('Locale'); - $this->context = $context; - $this->_current =& $this->_stacks[0]; - $this->errors =& $this->_stacks[0]; - } - - /** - * Sends an error message to the collector for later use - * @param $severity int Error severity, PHP error style (don't use E_USER_) - * @param $msg string Error message text - * @param $subst1 string First substitution for $msg - * @param $subst2 string ... - */ - public function send($severity, $msg) { - - $args = array(); - if (func_num_args() > 2) { - $args = func_get_args(); - array_shift($args); - unset($args[0]); - } - - $token = $this->context->get('CurrentToken', true); - $line = $token ? $token->line : $this->context->get('CurrentLine', true); - $col = $token ? $token->col : $this->context->get('CurrentCol', true); - $attr = $this->context->get('CurrentAttr', true); - - // perform special substitutions, also add custom parameters - $subst = array(); - if (!is_null($token)) { - $args['CurrentToken'] = $token; - } - if (!is_null($attr)) { - $subst['$CurrentAttr.Name'] = $attr; - if (isset($token->attr[$attr])) $subst['$CurrentAttr.Value'] = $token->attr[$attr]; - } - - if (empty($args)) { - $msg = $this->locale->getMessage($msg); - } else { - $msg = $this->locale->formatMessage($msg, $args); - } - - if (!empty($subst)) $msg = strtr($msg, $subst); - - // (numerically indexed) - $error = array( - self::LINENO => $line, - self::SEVERITY => $severity, - self::MESSAGE => $msg, - self::CHILDREN => array() - ); - $this->_current[] = $error; - - - // NEW CODE BELOW ... - - $struct = null; - // Top-level errors are either: - // TOKEN type, if $value is set appropriately, or - // "syntax" type, if $value is null - $new_struct = new HTMLPurifier_ErrorStruct(); - $new_struct->type = HTMLPurifier_ErrorStruct::TOKEN; - if ($token) $new_struct->value = clone $token; - if (is_int($line) && is_int($col)) { - if (isset($this->lines[$line][$col])) { - $struct = $this->lines[$line][$col]; - } else { - $struct = $this->lines[$line][$col] = $new_struct; - } - // These ksorts may present a performance problem - ksort($this->lines[$line], SORT_NUMERIC); - } else { - if (isset($this->lines[-1])) { - $struct = $this->lines[-1]; - } else { - $struct = $this->lines[-1] = $new_struct; - } - } - ksort($this->lines, SORT_NUMERIC); - - // Now, check if we need to operate on a lower structure - if (!empty($attr)) { - $struct = $struct->getChild(HTMLPurifier_ErrorStruct::ATTR, $attr); - if (!$struct->value) { - $struct->value = array($attr, 'PUT VALUE HERE'); - } - } - if (!empty($cssprop)) { - $struct = $struct->getChild(HTMLPurifier_ErrorStruct::CSSPROP, $cssprop); - if (!$struct->value) { - // if we tokenize CSS this might be a little more difficult to do - $struct->value = array($cssprop, 'PUT VALUE HERE'); - } - } - - // Ok, structs are all setup, now time to register the error - $struct->addError($severity, $msg); - } - - /** - * Retrieves raw error data for custom formatter to use - * @param List of arrays in format of array(line of error, - * error severity, error message, - * recursive sub-errors array) - */ - public function getRaw() { - return $this->errors; - } - - /** - * Default HTML formatting implementation for error messages - * @param $config Configuration array, vital for HTML output nature - * @param $errors Errors array to display; used for recursion. - */ - public function getHTMLFormatted($config, $errors = null) { - $ret = array(); - - $this->generator = new HTMLPurifier_Generator($config, $this->context); - if ($errors === null) $errors = $this->errors; - - // 'At line' message needs to be removed - - // generation code for new structure goes here. It needs to be recursive. - foreach ($this->lines as $line => $col_array) { - if ($line == -1) continue; - foreach ($col_array as $col => $struct) { - $this->_renderStruct($ret, $struct, $line, $col); - } - } - if (isset($this->lines[-1])) { - $this->_renderStruct($ret, $this->lines[-1]); - } - - if (empty($errors)) { - return '

    ' . $this->locale->getMessage('ErrorCollector: No errors') . '

    '; - } else { - return '
    • ' . implode('
    • ', $ret) . '
    '; - } - - } - - private function _renderStruct(&$ret, $struct, $line = null, $col = null) { - $stack = array($struct); - $context_stack = array(array()); - while ($current = array_pop($stack)) { - $context = array_pop($context_stack); - foreach ($current->errors as $error) { - list($severity, $msg) = $error; - $string = ''; - $string .= '
    '; - // W3C uses an icon to indicate the severity of the error. - $error = $this->locale->getErrorName($severity); - $string .= "$error "; - if (!is_null($line) && !is_null($col)) { - $string .= "Line $line, Column $col: "; - } else { - $string .= 'End of Document: '; - } - $string .= '' . $this->generator->escape($msg) . ' '; - $string .= '
    '; - // Here, have a marker for the character on the column appropriate. - // Be sure to clip extremely long lines. - //$string .= '
    ';
    -                //$string .= '';
    -                //$string .= '
    '; - $ret[] = $string; - } - foreach ($current->children as $type => $array) { - $context[] = $current; - $stack = array_merge($stack, array_reverse($array, true)); - for ($i = count($array); $i > 0; $i--) { - $context_stack[] = $context; - } - } - } - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ErrorStruct.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ErrorStruct.php deleted file mode 100644 index 9bc8996ec..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/ErrorStruct.php +++ /dev/null @@ -1,60 +0,0 @@ -children[$type][$id])) { - $this->children[$type][$id] = new HTMLPurifier_ErrorStruct(); - $this->children[$type][$id]->type = $type; - } - return $this->children[$type][$id]; - } - - public function addError($severity, $message) { - $this->errors[] = array($severity, $message); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Exception.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Exception.php deleted file mode 100644 index be85b4c56..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Exception.php +++ /dev/null @@ -1,12 +0,0 @@ -preFilter, - * 2->preFilter, 3->preFilter, purify, 3->postFilter, 2->postFilter, - * 1->postFilter. - * - * @note Methods are not declared abstract as it is perfectly legitimate - * for an implementation not to want anything to happen on a step - */ - -class HTMLPurifier_Filter -{ - - /** - * Name of the filter for identification purposes - */ - public $name; - - /** - * Pre-processor function, handles HTML before HTML Purifier - */ - public function preFilter($html, $config, $context) { - return $html; - } - - /** - * Post-processor function, handles HTML after HTML Purifier - */ - public function postFilter($html, $config, $context) { - return $html; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Filter/ExtractStyleBlocks.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Filter/ExtractStyleBlocks.php deleted file mode 100644 index df937ace7..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Filter/ExtractStyleBlocks.php +++ /dev/null @@ -1,289 +0,0 @@ - blocks from input HTML, cleans them up - * using CSSTidy, and then places them in $purifier->context->get('StyleBlocks') - * so they can be used elsewhere in the document. - * - * @note - * See tests/HTMLPurifier/Filter/ExtractStyleBlocksTest.php for - * sample usage. - * - * @note - * This filter can also be used on stylesheets not included in the - * document--something purists would probably prefer. Just directly - * call HTMLPurifier_Filter_ExtractStyleBlocks->cleanCSS() - */ -class HTMLPurifier_Filter_ExtractStyleBlocks extends HTMLPurifier_Filter -{ - - public $name = 'ExtractStyleBlocks'; - private $_styleMatches = array(); - private $_tidy; - - private $_id_attrdef; - private $_class_attrdef; - private $_enum_attrdef; - - public function __construct() { - $this->_tidy = new csstidy(); - $this->_tidy->set_cfg('lowercase_s', false); - $this->_id_attrdef = new HTMLPurifier_AttrDef_HTML_ID(true); - $this->_class_attrdef = new HTMLPurifier_AttrDef_CSS_Ident(); - $this->_enum_attrdef = new HTMLPurifier_AttrDef_Enum(array('first-child', 'link', 'visited', 'active', 'hover', 'focus')); - } - - /** - * Save the contents of CSS blocks to style matches - * @param $matches preg_replace style $matches array - */ - protected function styleCallback($matches) { - $this->_styleMatches[] = $matches[1]; - } - - /** - * Removes inline #isU', array($this, 'styleCallback'), $html); - $style_blocks = $this->_styleMatches; - $this->_styleMatches = array(); // reset - $context->register('StyleBlocks', $style_blocks); // $context must not be reused - if ($this->_tidy) { - foreach ($style_blocks as &$style) { - $style = $this->cleanCSS($style, $config, $context); - } - } - return $html; - } - - /** - * Takes CSS (the stuff found in in a font-family prop). - if ($config->get('Filter.ExtractStyleBlocks.Escaping')) { - $css = str_replace( - array('<', '>', '&'), - array('\3C ', '\3E ', '\26 '), - $css - ); - } - return $css; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Filter/YouTube.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Filter/YouTube.php deleted file mode 100644 index 23df221ea..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Filter/YouTube.php +++ /dev/null @@ -1,39 +0,0 @@ -]+>.+?'. - 'http://www.youtube.com/((?:v|cp)/[A-Za-z0-9\-_=]+).+?#s'; - $pre_replace = '\1'; - return preg_replace($pre_regex, $pre_replace, $html); - } - - public function postFilter($html, $config, $context) { - $post_regex = '#((?:v|cp)/[A-Za-z0-9\-_=]+)#'; - return preg_replace_callback($post_regex, array($this, 'postFilterCallback'), $html); - } - - protected function armorUrl($url) { - return str_replace('--', '--', $url); - } - - protected function postFilterCallback($matches) { - $url = $this->armorUrl($matches[1]); - return ''. - ''. - ''. - ''; - - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Generator.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Generator.php deleted file mode 100644 index fee1a5f84..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Generator.php +++ /dev/null @@ -1,254 +0,0 @@ - tags - */ - private $_scriptFix = false; - - /** - * Cache of HTMLDefinition during HTML output to determine whether or - * not attributes should be minimized. - */ - private $_def; - - /** - * Cache of %Output.SortAttr - */ - private $_sortAttr; - - /** - * Cache of %Output.FlashCompat - */ - private $_flashCompat; - - /** - * Cache of %Output.FixInnerHTML - */ - private $_innerHTMLFix; - - /** - * Stack for keeping track of object information when outputting IE - * compatibility code. - */ - private $_flashStack = array(); - - /** - * Configuration for the generator - */ - protected $config; - - /** - * @param $config Instance of HTMLPurifier_Config - * @param $context Instance of HTMLPurifier_Context - */ - public function __construct($config, $context) { - $this->config = $config; - $this->_scriptFix = $config->get('Output.CommentScriptContents'); - $this->_innerHTMLFix = $config->get('Output.FixInnerHTML'); - $this->_sortAttr = $config->get('Output.SortAttr'); - $this->_flashCompat = $config->get('Output.FlashCompat'); - $this->_def = $config->getHTMLDefinition(); - $this->_xhtml = $this->_def->doctype->xml; - } - - /** - * Generates HTML from an array of tokens. - * @param $tokens Array of HTMLPurifier_Token - * @param $config HTMLPurifier_Config object - * @return Generated HTML - */ - public function generateFromTokens($tokens) { - if (!$tokens) return ''; - - // Basic algorithm - $html = ''; - for ($i = 0, $size = count($tokens); $i < $size; $i++) { - if ($this->_scriptFix && $tokens[$i]->name === 'script' - && $i + 2 < $size && $tokens[$i+2] instanceof HTMLPurifier_Token_End) { - // script special case - // the contents of the script block must be ONE token - // for this to work. - $html .= $this->generateFromToken($tokens[$i++]); - $html .= $this->generateScriptFromToken($tokens[$i++]); - } - $html .= $this->generateFromToken($tokens[$i]); - } - - // Tidy cleanup - if (extension_loaded('tidy') && $this->config->get('Output.TidyFormat')) { - $tidy = new Tidy; - $tidy->parseString($html, array( - 'indent'=> true, - 'output-xhtml' => $this->_xhtml, - 'show-body-only' => true, - 'indent-spaces' => 2, - 'wrap' => 68, - ), 'utf8'); - $tidy->cleanRepair(); - $html = (string) $tidy; // explicit cast necessary - } - - // Normalize newlines to system defined value - if ($this->config->get('Core.NormalizeNewlines')) { - $nl = $this->config->get('Output.Newline'); - if ($nl === null) $nl = PHP_EOL; - if ($nl !== "\n") $html = str_replace("\n", $nl, $html); - } - return $html; - } - - /** - * Generates HTML from a single token. - * @param $token HTMLPurifier_Token object. - * @return Generated HTML - */ - public function generateFromToken($token) { - if (!$token instanceof HTMLPurifier_Token) { - trigger_error('Cannot generate HTML from non-HTMLPurifier_Token object', E_USER_WARNING); - return ''; - - } elseif ($token instanceof HTMLPurifier_Token_Start) { - $attr = $this->generateAttributes($token->attr, $token->name); - if ($this->_flashCompat) { - if ($token->name == "object") { - $flash = new stdclass(); - $flash->attr = $token->attr; - $flash->param = array(); - $this->_flashStack[] = $flash; - } - } - return '<' . $token->name . ($attr ? ' ' : '') . $attr . '>'; - - } elseif ($token instanceof HTMLPurifier_Token_End) { - $_extra = ''; - if ($this->_flashCompat) { - if ($token->name == "object" && !empty($this->_flashStack)) { - // doesn't do anything for now - } - } - return $_extra . 'name . '>'; - - } elseif ($token instanceof HTMLPurifier_Token_Empty) { - if ($this->_flashCompat && $token->name == "param" && !empty($this->_flashStack)) { - $this->_flashStack[count($this->_flashStack)-1]->param[$token->attr['name']] = $token->attr['value']; - } - $attr = $this->generateAttributes($token->attr, $token->name); - return '<' . $token->name . ($attr ? ' ' : '') . $attr . - ( $this->_xhtml ? ' /': '' ) //
    v.
    - . '>'; - - } elseif ($token instanceof HTMLPurifier_Token_Text) { - return $this->escape($token->data, ENT_NOQUOTES); - - } elseif ($token instanceof HTMLPurifier_Token_Comment) { - return ''; - } else { - return ''; - - } - } - - /** - * Special case processor for the contents of script tags - * @warning This runs into problems if there's already a literal - * --> somewhere inside the script contents. - */ - public function generateScriptFromToken($token) { - if (!$token instanceof HTMLPurifier_Token_Text) return $this->generateFromToken($token); - // Thanks - $data = preg_replace('#//\s*$#', '', $token->data); - return ''; - } - - /** - * Generates attribute declarations from attribute array. - * @note This does not include the leading or trailing space. - * @param $assoc_array_of_attributes Attribute array - * @param $element Name of element attributes are for, used to check - * attribute minimization. - * @return Generate HTML fragment for insertion. - */ - public function generateAttributes($assoc_array_of_attributes, $element = false) { - $html = ''; - if ($this->_sortAttr) ksort($assoc_array_of_attributes); - foreach ($assoc_array_of_attributes as $key => $value) { - if (!$this->_xhtml) { - // Remove namespaced attributes - if (strpos($key, ':') !== false) continue; - // Check if we should minimize the attribute: val="val" -> val - if ($element && !empty($this->_def->info[$element]->attr[$key]->minimized)) { - $html .= $key . ' '; - continue; - } - } - // Workaround for Internet Explorer innerHTML bug. - // Essentially, Internet Explorer, when calculating - // innerHTML, omits quotes if there are no instances of - // angled brackets, quotes or spaces. However, when parsing - // HTML (for example, when you assign to innerHTML), it - // treats backticks as quotes. Thus, - // `` - // becomes - // `` - // becomes - // - // Fortunately, all we need to do is trigger an appropriate - // quoting style, which we do by adding an extra space. - // This also is consistent with the W3C spec, which states - // that user agents may ignore leading or trailing - // whitespace (in fact, most don't, at least for attributes - // like alt, but an extra space at the end is barely - // noticeable). Still, we have a configuration knob for - // this, since this transformation is not necesary if you - // don't process user input with innerHTML or you don't plan - // on supporting Internet Explorer. - if ($this->_innerHTMLFix) { - if (strpos($value, '`') !== false) { - // check if correct quoting style would not already be - // triggered - if (strcspn($value, '"\' <>') === strlen($value)) { - // protect! - $value .= ' '; - } - } - } - $html .= $key.'="'.$this->escape($value).'" '; - } - return rtrim($html); - } - - /** - * Escapes raw text data. - * @todo This really ought to be protected, but until we have a facility - * for properly generating HTML here w/o using tokens, it stays - * public. - * @param $string String data to escape for HTML. - * @param $quote Quoting style, like htmlspecialchars. ENT_NOQUOTES is - * permissible for non-attribute output. - * @return String escaped data. - */ - public function escape($string, $quote = null) { - // Workaround for APC bug on Mac Leopard reported by sidepodcast - // http://htmlpurifier.org/phorum/read.php?3,4823,4846 - if ($quote === null) $quote = ENT_COMPAT; - return htmlspecialchars($string, $quote, 'UTF-8'); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLDefinition.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLDefinition.php deleted file mode 100644 index b079d44c1..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLDefinition.php +++ /dev/null @@ -1,425 +0,0 @@ -getAnonymousModule(); - if (!isset($module->info[$element_name])) { - $element = $module->addBlankElement($element_name); - } else { - $element = $module->info[$element_name]; - } - $element->attr[$attr_name] = $def; - } - - /** - * Adds a custom element to your HTML definition - * @note See HTMLPurifier_HTMLModule::addElement for detailed - * parameter and return value descriptions. - */ - public function addElement($element_name, $type, $contents, $attr_collections, $attributes = array()) { - $module = $this->getAnonymousModule(); - // assume that if the user is calling this, the element - // is safe. This may not be a good idea - $element = $module->addElement($element_name, $type, $contents, $attr_collections, $attributes); - return $element; - } - - /** - * Adds a blank element to your HTML definition, for overriding - * existing behavior - * @note See HTMLPurifier_HTMLModule::addBlankElement for detailed - * parameter and return value descriptions. - */ - public function addBlankElement($element_name) { - $module = $this->getAnonymousModule(); - $element = $module->addBlankElement($element_name); - return $element; - } - - /** - * Retrieves a reference to the anonymous module, so you can - * bust out advanced features without having to make your own - * module. - */ - public function getAnonymousModule() { - if (!$this->_anonModule) { - $this->_anonModule = new HTMLPurifier_HTMLModule(); - $this->_anonModule->name = 'Anonymous'; - } - return $this->_anonModule; - } - - private $_anonModule = null; - - - // PUBLIC BUT INTERNAL VARIABLES -------------------------------------- - - public $type = 'HTML'; - public $manager; /**< Instance of HTMLPurifier_HTMLModuleManager */ - - /** - * Performs low-cost, preliminary initialization. - */ - public function __construct() { - $this->manager = new HTMLPurifier_HTMLModuleManager(); - } - - protected function doSetup($config) { - $this->processModules($config); - $this->setupConfigStuff($config); - unset($this->manager); - - // cleanup some of the element definitions - foreach ($this->info as $k => $v) { - unset($this->info[$k]->content_model); - unset($this->info[$k]->content_model_type); - } - } - - /** - * Extract out the information from the manager - */ - protected function processModules($config) { - - if ($this->_anonModule) { - // for user specific changes - // this is late-loaded so we don't have to deal with PHP4 - // reference wonky-ness - $this->manager->addModule($this->_anonModule); - unset($this->_anonModule); - } - - $this->manager->setup($config); - $this->doctype = $this->manager->doctype; - - foreach ($this->manager->modules as $module) { - foreach($module->info_tag_transform as $k => $v) { - if ($v === false) unset($this->info_tag_transform[$k]); - else $this->info_tag_transform[$k] = $v; - } - foreach($module->info_attr_transform_pre as $k => $v) { - if ($v === false) unset($this->info_attr_transform_pre[$k]); - else $this->info_attr_transform_pre[$k] = $v; - } - foreach($module->info_attr_transform_post as $k => $v) { - if ($v === false) unset($this->info_attr_transform_post[$k]); - else $this->info_attr_transform_post[$k] = $v; - } - foreach ($module->info_injector as $k => $v) { - if ($v === false) unset($this->info_injector[$k]); - else $this->info_injector[$k] = $v; - } - } - - $this->info = $this->manager->getElements(); - $this->info_content_sets = $this->manager->contentSets->lookup; - - } - - /** - * Sets up stuff based on config. We need a better way of doing this. - */ - protected function setupConfigStuff($config) { - - $block_wrapper = $config->get('HTML.BlockWrapper'); - if (isset($this->info_content_sets['Block'][$block_wrapper])) { - $this->info_block_wrapper = $block_wrapper; - } else { - trigger_error('Cannot use non-block element as block wrapper', - E_USER_ERROR); - } - - $parent = $config->get('HTML.Parent'); - $def = $this->manager->getElement($parent, true); - if ($def) { - $this->info_parent = $parent; - $this->info_parent_def = $def; - } else { - trigger_error('Cannot use unrecognized element as parent', - E_USER_ERROR); - $this->info_parent_def = $this->manager->getElement($this->info_parent, true); - } - - // support template text - $support = "(for information on implementing this, see the ". - "support forums) "; - - // setup allowed elements ----------------------------------------- - - $allowed_elements = $config->get('HTML.AllowedElements'); - $allowed_attributes = $config->get('HTML.AllowedAttributes'); // retrieve early - - if (!is_array($allowed_elements) && !is_array($allowed_attributes)) { - $allowed = $config->get('HTML.Allowed'); - if (is_string($allowed)) { - list($allowed_elements, $allowed_attributes) = $this->parseTinyMCEAllowedList($allowed); - } - } - - if (is_array($allowed_elements)) { - foreach ($this->info as $name => $d) { - if(!isset($allowed_elements[$name])) unset($this->info[$name]); - unset($allowed_elements[$name]); - } - // emit errors - foreach ($allowed_elements as $element => $d) { - $element = htmlspecialchars($element); // PHP doesn't escape errors, be careful! - trigger_error("Element '$element' is not supported $support", E_USER_WARNING); - } - } - - // setup allowed attributes --------------------------------------- - - $allowed_attributes_mutable = $allowed_attributes; // by copy! - if (is_array($allowed_attributes)) { - - // This actually doesn't do anything, since we went away from - // global attributes. It's possible that userland code uses - // it, but HTMLModuleManager doesn't! - foreach ($this->info_global_attr as $attr => $x) { - $keys = array($attr, "*@$attr", "*.$attr"); - $delete = true; - foreach ($keys as $key) { - if ($delete && isset($allowed_attributes[$key])) { - $delete = false; - } - if (isset($allowed_attributes_mutable[$key])) { - unset($allowed_attributes_mutable[$key]); - } - } - if ($delete) unset($this->info_global_attr[$attr]); - } - - foreach ($this->info as $tag => $info) { - foreach ($info->attr as $attr => $x) { - $keys = array("$tag@$attr", $attr, "*@$attr", "$tag.$attr", "*.$attr"); - $delete = true; - foreach ($keys as $key) { - if ($delete && isset($allowed_attributes[$key])) { - $delete = false; - } - if (isset($allowed_attributes_mutable[$key])) { - unset($allowed_attributes_mutable[$key]); - } - } - if ($delete) { - if ($this->info[$tag]->attr[$attr]->required) { - trigger_error("Required attribute '$attr' in element '$tag' was not allowed, which means '$tag' will not be allowed either", E_USER_WARNING); - } - unset($this->info[$tag]->attr[$attr]); - } - } - } - // emit errors - foreach ($allowed_attributes_mutable as $elattr => $d) { - $bits = preg_split('/[.@]/', $elattr, 2); - $c = count($bits); - switch ($c) { - case 2: - if ($bits[0] !== '*') { - $element = htmlspecialchars($bits[0]); - $attribute = htmlspecialchars($bits[1]); - if (!isset($this->info[$element])) { - trigger_error("Cannot allow attribute '$attribute' if element '$element' is not allowed/supported $support"); - } else { - trigger_error("Attribute '$attribute' in element '$element' not supported $support", - E_USER_WARNING); - } - break; - } - // otherwise fall through - case 1: - $attribute = htmlspecialchars($bits[0]); - trigger_error("Global attribute '$attribute' is not ". - "supported in any elements $support", - E_USER_WARNING); - break; - } - } - - } - - // setup forbidden elements --------------------------------------- - - $forbidden_elements = $config->get('HTML.ForbiddenElements'); - $forbidden_attributes = $config->get('HTML.ForbiddenAttributes'); - - foreach ($this->info as $tag => $info) { - if (isset($forbidden_elements[$tag])) { - unset($this->info[$tag]); - continue; - } - foreach ($info->attr as $attr => $x) { - if ( - isset($forbidden_attributes["$tag@$attr"]) || - isset($forbidden_attributes["*@$attr"]) || - isset($forbidden_attributes[$attr]) - ) { - unset($this->info[$tag]->attr[$attr]); - continue; - } // this segment might get removed eventually - elseif (isset($forbidden_attributes["$tag.$attr"])) { - // $tag.$attr are not user supplied, so no worries! - trigger_error("Error with $tag.$attr: tag.attr syntax not supported for HTML.ForbiddenAttributes; use tag@attr instead", E_USER_WARNING); - } - } - } - foreach ($forbidden_attributes as $key => $v) { - if (strlen($key) < 2) continue; - if ($key[0] != '*') continue; - if ($key[1] == '.') { - trigger_error("Error with $key: *.attr syntax not supported for HTML.ForbiddenAttributes; use attr instead", E_USER_WARNING); - } - } - - // setup injectors ----------------------------------------------------- - foreach ($this->info_injector as $i => $injector) { - if ($injector->checkNeeded($config) !== false) { - // remove injector that does not have it's required - // elements/attributes present, and is thus not needed. - unset($this->info_injector[$i]); - } - } - } - - /** - * Parses a TinyMCE-flavored Allowed Elements and Attributes list into - * separate lists for processing. Format is element[attr1|attr2],element2... - * @warning Although it's largely drawn from TinyMCE's implementation, - * it is different, and you'll probably have to modify your lists - * @param $list String list to parse - * @param array($allowed_elements, $allowed_attributes) - * @todo Give this its own class, probably static interface - */ - public function parseTinyMCEAllowedList($list) { - - $list = str_replace(array(' ', "\t"), '', $list); - - $elements = array(); - $attributes = array(); - - $chunks = preg_split('/(,|[\n\r]+)/', $list); - foreach ($chunks as $chunk) { - if (empty($chunk)) continue; - // remove TinyMCE element control characters - if (!strpos($chunk, '[')) { - $element = $chunk; - $attr = false; - } else { - list($element, $attr) = explode('[', $chunk); - } - if ($element !== '*') $elements[$element] = true; - if (!$attr) continue; - $attr = substr($attr, 0, strlen($attr) - 1); // remove trailing ] - $attr = explode('|', $attr); - foreach ($attr as $key) { - $attributes["$element.$key"] = true; - } - } - - return array($elements, $attributes); - - } - - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule.php deleted file mode 100644 index 072cf6808..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule.php +++ /dev/null @@ -1,244 +0,0 @@ -info, since the object's data is only info, - * with extra behavior associated with it. - */ - public $attr_collections = array(); - - /** - * Associative array of deprecated tag name to HTMLPurifier_TagTransform - */ - public $info_tag_transform = array(); - - /** - * List of HTMLPurifier_AttrTransform to be performed before validation. - */ - public $info_attr_transform_pre = array(); - - /** - * List of HTMLPurifier_AttrTransform to be performed after validation. - */ - public $info_attr_transform_post = array(); - - /** - * List of HTMLPurifier_Injector to be performed during well-formedness fixing. - * An injector will only be invoked if all of it's pre-requisites are met; - * if an injector fails setup, there will be no error; it will simply be - * silently disabled. - */ - public $info_injector = array(); - - /** - * Boolean flag that indicates whether or not getChildDef is implemented. - * For optimization reasons: may save a call to a function. Be sure - * to set it if you do implement getChildDef(), otherwise it will have - * no effect! - */ - public $defines_child_def = false; - - /** - * Boolean flag whether or not this module is safe. If it is not safe, all - * of its members are unsafe. Modules are safe by default (this might be - * slightly dangerous, but it doesn't make much sense to force HTML Purifier, - * which is based off of safe HTML, to explicitly say, "This is safe," even - * though there are modules which are "unsafe") - * - * @note Previously, safety could be applied at an element level granularity. - * We've removed this ability, so in order to add "unsafe" elements - * or attributes, a dedicated module with this property set to false - * must be used. - */ - public $safe = true; - - /** - * Retrieves a proper HTMLPurifier_ChildDef subclass based on - * content_model and content_model_type member variables of - * the HTMLPurifier_ElementDef class. There is a similar function - * in HTMLPurifier_HTMLDefinition. - * @param $def HTMLPurifier_ElementDef instance - * @return HTMLPurifier_ChildDef subclass - */ - public function getChildDef($def) {return false;} - - // -- Convenience ----------------------------------------------------- - - /** - * Convenience function that sets up a new element - * @param $element Name of element to add - * @param $type What content set should element be registered to? - * Set as false to skip this step. - * @param $contents Allowed children in form of: - * "$content_model_type: $content_model" - * @param $attr_includes What attribute collections to register to - * element? - * @param $attr What unique attributes does the element define? - * @note See ElementDef for in-depth descriptions of these parameters. - * @return Created element definition object, so you - * can set advanced parameters - */ - public function addElement($element, $type, $contents, $attr_includes = array(), $attr = array()) { - $this->elements[] = $element; - // parse content_model - list($content_model_type, $content_model) = $this->parseContents($contents); - // merge in attribute inclusions - $this->mergeInAttrIncludes($attr, $attr_includes); - // add element to content sets - if ($type) $this->addElementToContentSet($element, $type); - // create element - $this->info[$element] = HTMLPurifier_ElementDef::create( - $content_model, $content_model_type, $attr - ); - // literal object $contents means direct child manipulation - if (!is_string($contents)) $this->info[$element]->child = $contents; - return $this->info[$element]; - } - - /** - * Convenience function that creates a totally blank, non-standalone - * element. - * @param $element Name of element to create - * @return Created element - */ - public function addBlankElement($element) { - if (!isset($this->info[$element])) { - $this->elements[] = $element; - $this->info[$element] = new HTMLPurifier_ElementDef(); - $this->info[$element]->standalone = false; - } else { - trigger_error("Definition for $element already exists in module, cannot redefine"); - } - return $this->info[$element]; - } - - /** - * Convenience function that registers an element to a content set - * @param Element to register - * @param Name content set (warning: case sensitive, usually upper-case - * first letter) - */ - public function addElementToContentSet($element, $type) { - if (!isset($this->content_sets[$type])) $this->content_sets[$type] = ''; - else $this->content_sets[$type] .= ' | '; - $this->content_sets[$type] .= $element; - } - - /** - * Convenience function that transforms single-string contents - * into separate content model and content model type - * @param $contents Allowed children in form of: - * "$content_model_type: $content_model" - * @note If contents is an object, an array of two nulls will be - * returned, and the callee needs to take the original $contents - * and use it directly. - */ - public function parseContents($contents) { - if (!is_string($contents)) return array(null, null); // defer - switch ($contents) { - // check for shorthand content model forms - case 'Empty': - return array('empty', ''); - case 'Inline': - return array('optional', 'Inline | #PCDATA'); - case 'Flow': - return array('optional', 'Flow | #PCDATA'); - } - list($content_model_type, $content_model) = explode(':', $contents); - $content_model_type = strtolower(trim($content_model_type)); - $content_model = trim($content_model); - return array($content_model_type, $content_model); - } - - /** - * Convenience function that merges a list of attribute includes into - * an attribute array. - * @param $attr Reference to attr array to modify - * @param $attr_includes Array of includes / string include to merge in - */ - public function mergeInAttrIncludes(&$attr, $attr_includes) { - if (!is_array($attr_includes)) { - if (empty($attr_includes)) $attr_includes = array(); - else $attr_includes = array($attr_includes); - } - $attr[0] = $attr_includes; - } - - /** - * Convenience function that generates a lookup table with boolean - * true as value. - * @param $list List of values to turn into a lookup - * @note You can also pass an arbitrary number of arguments in - * place of the regular argument - * @return Lookup array equivalent of list - */ - public function makeLookup($list) { - if (is_string($list)) $list = func_get_args(); - $ret = array(); - foreach ($list as $value) { - if (is_null($value)) continue; - $ret[$value] = true; - } - return $ret; - } - - /** - * Lazy load construction of the module after determining whether - * or not it's needed, and also when a finalized configuration object - * is available. - * @param $config Instance of HTMLPurifier_Config - */ - public function setup($config) {} - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Bdo.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Bdo.php deleted file mode 100644 index 23ac3da3a..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Bdo.php +++ /dev/null @@ -1,31 +0,0 @@ - array('dir' => false) - ); - - public function setup($config) { - $bdo = $this->addElement( - 'bdo', 'Inline', 'Inline', array('Core', 'Lang'), - array( - 'dir' => 'Enum#ltr,rtl', // required - // The Abstract Module specification has the attribute - // inclusions wrong for bdo: bdo allows Lang - ) - ); - $bdo->attr_transform_post[] = new HTMLPurifier_AttrTransform_BdoDir(); - - $this->attr_collections['I18N']['dir'] = 'Enum#ltr,rtl'; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/CommonAttributes.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/CommonAttributes.php deleted file mode 100644 index 7c15da84f..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/CommonAttributes.php +++ /dev/null @@ -1,26 +0,0 @@ - array( - 0 => array('Style'), - // 'xml:space' => false, - 'class' => 'Class', - 'id' => 'ID', - 'title' => 'CDATA', - ), - 'Lang' => array(), - 'I18N' => array( - 0 => array('Lang'), // proprietary, for xml:lang/lang - ), - 'Common' => array( - 0 => array('Core', 'I18N') - ) - ); - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Edit.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Edit.php deleted file mode 100644 index ff9369055..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Edit.php +++ /dev/null @@ -1,38 +0,0 @@ - 'URI', - // 'datetime' => 'Datetime', // not implemented - ); - $this->addElement('del', 'Inline', $contents, 'Common', $attr); - $this->addElement('ins', 'Inline', $contents, 'Common', $attr); - } - - // HTML 4.01 specifies that ins/del must not contain block - // elements when used in an inline context, chameleon is - // a complicated workaround to acheive this effect - - // Inline context ! Block context (exclamation mark is - // separator, see getChildDef for parsing) - - public $defines_child_def = true; - public function getChildDef($def) { - if ($def->content_model_type != 'chameleon') return false; - $value = explode('!', $def->content_model); - return new HTMLPurifier_ChildDef_Chameleon($value[0], $value[1]); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Forms.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Forms.php deleted file mode 100644 index b963529a7..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Forms.php +++ /dev/null @@ -1,119 +0,0 @@ - 'Form', - 'Inline' => 'Formctrl', - ); - - public function setup($config) { - $form = $this->addElement('form', 'Form', - 'Required: Heading | List | Block | fieldset', 'Common', array( - 'accept' => 'ContentTypes', - 'accept-charset' => 'Charsets', - 'action*' => 'URI', - 'method' => 'Enum#get,post', - // really ContentType, but these two are the only ones used today - 'enctype' => 'Enum#application/x-www-form-urlencoded,multipart/form-data', - )); - $form->excludes = array('form' => true); - - $input = $this->addElement('input', 'Formctrl', 'Empty', 'Common', array( - 'accept' => 'ContentTypes', - 'accesskey' => 'Character', - 'alt' => 'Text', - 'checked' => 'Bool#checked', - 'disabled' => 'Bool#disabled', - 'maxlength' => 'Number', - 'name' => 'CDATA', - 'readonly' => 'Bool#readonly', - 'size' => 'Number', - 'src' => 'URI#embedded', - 'tabindex' => 'Number', - 'type' => 'Enum#text,password,checkbox,button,radio,submit,reset,file,hidden,image', - 'value' => 'CDATA', - )); - $input->attr_transform_post[] = new HTMLPurifier_AttrTransform_Input(); - - $this->addElement('select', 'Formctrl', 'Required: optgroup | option', 'Common', array( - 'disabled' => 'Bool#disabled', - 'multiple' => 'Bool#multiple', - 'name' => 'CDATA', - 'size' => 'Number', - 'tabindex' => 'Number', - )); - - $this->addElement('option', false, 'Optional: #PCDATA', 'Common', array( - 'disabled' => 'Bool#disabled', - 'label' => 'Text', - 'selected' => 'Bool#selected', - 'value' => 'CDATA', - )); - // It's illegal for there to be more than one selected, but not - // be multiple. Also, no selected means undefined behavior. This might - // be difficult to implement; perhaps an injector, or a context variable. - - $textarea = $this->addElement('textarea', 'Formctrl', 'Optional: #PCDATA', 'Common', array( - 'accesskey' => 'Character', - 'cols*' => 'Number', - 'disabled' => 'Bool#disabled', - 'name' => 'CDATA', - 'readonly' => 'Bool#readonly', - 'rows*' => 'Number', - 'tabindex' => 'Number', - )); - $textarea->attr_transform_pre[] = new HTMLPurifier_AttrTransform_Textarea(); - - $button = $this->addElement('button', 'Formctrl', 'Optional: #PCDATA | Heading | List | Block | Inline', 'Common', array( - 'accesskey' => 'Character', - 'disabled' => 'Bool#disabled', - 'name' => 'CDATA', - 'tabindex' => 'Number', - 'type' => 'Enum#button,submit,reset', - 'value' => 'CDATA', - )); - - // For exclusions, ideally we'd specify content sets, not literal elements - $button->excludes = $this->makeLookup( - 'form', 'fieldset', // Form - 'input', 'select', 'textarea', 'label', 'button', // Formctrl - 'a', // as per HTML 4.01 spec, this is omitted by modularization - 'isindex', 'iframe' // legacy items - ); - - // Extra exclusion: img usemap="" is not permitted within this element. - // We'll omit this for now, since we don't have any good way of - // indicating it yet. - - // This is HIGHLY user-unfriendly; we need a custom child-def for this - $this->addElement('fieldset', 'Form', 'Custom: (#WS?,legend,(Flow|#PCDATA)*)', 'Common'); - - $label = $this->addElement('label', 'Formctrl', 'Optional: #PCDATA | Inline', 'Common', array( - 'accesskey' => 'Character', - // 'for' => 'IDREF', // IDREF not implemented, cannot allow - )); - $label->excludes = array('label' => true); - - $this->addElement('legend', false, 'Optional: #PCDATA | Inline', 'Common', array( - 'accesskey' => 'Character', - )); - - $this->addElement('optgroup', false, 'Required: option', 'Common', array( - 'disabled' => 'Bool#disabled', - 'label*' => 'Text', - )); - - // Don't forget an injector for . This one's a little complex - // because it maps to multiple elements. - - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Hypertext.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Hypertext.php deleted file mode 100644 index d7e9bdd27..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Hypertext.php +++ /dev/null @@ -1,31 +0,0 @@ -addElement( - 'a', 'Inline', 'Inline', 'Common', - array( - // 'accesskey' => 'Character', - // 'charset' => 'Charset', - 'href' => 'URI', - // 'hreflang' => 'LanguageCode', - 'rel' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rel'), - 'rev' => new HTMLPurifier_AttrDef_HTML_LinkTypes('rev'), - // 'tabindex' => 'Number', - // 'type' => 'ContentType', - ) - ); - $a->formatting = true; - $a->excludes = array('a' => true); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Iframe.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Iframe.php deleted file mode 100644 index 287071edf..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Iframe.php +++ /dev/null @@ -1,38 +0,0 @@ -get('HTML.SafeIframe')) { - $this->safe = true; - } - $this->addElement( - 'iframe', 'Inline', 'Flow', 'Common', - array( - 'src' => 'URI#embedded', - 'width' => 'Length', - 'height' => 'Length', - 'name' => 'ID', - 'scrolling' => 'Enum#yes,no,auto', - 'frameborder' => 'Enum#0,1', - 'longdesc' => 'URI', - 'marginheight' => 'Pixels', - 'marginwidth' => 'Pixels', - ) - ); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Image.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Image.php deleted file mode 100644 index 948d435bc..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Image.php +++ /dev/null @@ -1,40 +0,0 @@ -get('HTML.MaxImgLength'); - $img = $this->addElement( - 'img', 'Inline', 'Empty', 'Common', - array( - 'alt*' => 'Text', - // According to the spec, it's Length, but percents can - // be abused, so we allow only Pixels. - 'height' => 'Pixels#' . $max, - 'width' => 'Pixels#' . $max, - 'longdesc' => 'URI', - 'src*' => new HTMLPurifier_AttrDef_URI(true), // embedded - ) - ); - if ($max === null || $config->get('HTML.Trusted')) { - $img->attr['height'] = - $img->attr['width'] = 'Length'; - } - - // kind of strange, but splitting things up would be inefficient - $img->attr_transform_pre[] = - $img->attr_transform_post[] = - new HTMLPurifier_AttrTransform_ImgRequired(); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Legacy.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Legacy.php deleted file mode 100644 index f278eeced..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Legacy.php +++ /dev/null @@ -1,159 +0,0 @@ -addElement('basefont', 'Inline', 'Empty', false, array( - 'color' => 'Color', - 'face' => 'Text', // extremely broad, we should - 'size' => 'Text', // tighten it - 'id' => 'ID' - )); - $this->addElement('center', 'Block', 'Flow', 'Common'); - $this->addElement('dir', 'Block', 'Required: li', 'Common', array( - 'compact' => 'Bool#compact' - )); - $this->addElement('font', 'Inline', 'Inline', array('Core', 'I18N'), array( - 'color' => 'Color', - 'face' => 'Text', // extremely broad, we should - 'size' => 'Text', // tighten it - )); - $this->addElement('menu', 'Block', 'Required: li', 'Common', array( - 'compact' => 'Bool#compact' - )); - - $s = $this->addElement('s', 'Inline', 'Inline', 'Common'); - $s->formatting = true; - - $strike = $this->addElement('strike', 'Inline', 'Inline', 'Common'); - $strike->formatting = true; - - $u = $this->addElement('u', 'Inline', 'Inline', 'Common'); - $u->formatting = true; - - // setup modifications to old elements - - $align = 'Enum#left,right,center,justify'; - - $address = $this->addBlankElement('address'); - $address->content_model = 'Inline | #PCDATA | p'; - $address->content_model_type = 'optional'; - $address->child = false; - - $blockquote = $this->addBlankElement('blockquote'); - $blockquote->content_model = 'Flow | #PCDATA'; - $blockquote->content_model_type = 'optional'; - $blockquote->child = false; - - $br = $this->addBlankElement('br'); - $br->attr['clear'] = 'Enum#left,all,right,none'; - - $caption = $this->addBlankElement('caption'); - $caption->attr['align'] = 'Enum#top,bottom,left,right'; - - $div = $this->addBlankElement('div'); - $div->attr['align'] = $align; - - $dl = $this->addBlankElement('dl'); - $dl->attr['compact'] = 'Bool#compact'; - - for ($i = 1; $i <= 6; $i++) { - $h = $this->addBlankElement("h$i"); - $h->attr['align'] = $align; - } - - $hr = $this->addBlankElement('hr'); - $hr->attr['align'] = $align; - $hr->attr['noshade'] = 'Bool#noshade'; - $hr->attr['size'] = 'Pixels'; - $hr->attr['width'] = 'Length'; - - $img = $this->addBlankElement('img'); - $img->attr['align'] = 'IAlign'; - $img->attr['border'] = 'Pixels'; - $img->attr['hspace'] = 'Pixels'; - $img->attr['vspace'] = 'Pixels'; - - // figure out this integer business - - $li = $this->addBlankElement('li'); - $li->attr['value'] = new HTMLPurifier_AttrDef_Integer(); - $li->attr['type'] = 'Enum#s:1,i,I,a,A,disc,square,circle'; - - $ol = $this->addBlankElement('ol'); - $ol->attr['compact'] = 'Bool#compact'; - $ol->attr['start'] = new HTMLPurifier_AttrDef_Integer(); - $ol->attr['type'] = 'Enum#s:1,i,I,a,A'; - - $p = $this->addBlankElement('p'); - $p->attr['align'] = $align; - - $pre = $this->addBlankElement('pre'); - $pre->attr['width'] = 'Number'; - - // script omitted - - $table = $this->addBlankElement('table'); - $table->attr['align'] = 'Enum#left,center,right'; - $table->attr['bgcolor'] = 'Color'; - - $tr = $this->addBlankElement('tr'); - $tr->attr['bgcolor'] = 'Color'; - - $th = $this->addBlankElement('th'); - $th->attr['bgcolor'] = 'Color'; - $th->attr['height'] = 'Length'; - $th->attr['nowrap'] = 'Bool#nowrap'; - $th->attr['width'] = 'Length'; - - $td = $this->addBlankElement('td'); - $td->attr['bgcolor'] = 'Color'; - $td->attr['height'] = 'Length'; - $td->attr['nowrap'] = 'Bool#nowrap'; - $td->attr['width'] = 'Length'; - - $ul = $this->addBlankElement('ul'); - $ul->attr['compact'] = 'Bool#compact'; - $ul->attr['type'] = 'Enum#square,disc,circle'; - - // "safe" modifications to "unsafe" elements - // WARNING: If you want to add support for an unsafe, legacy - // attribute, make a new TrustedLegacy module with the trusted - // bit set appropriately - - $form = $this->addBlankElement('form'); - $form->content_model = 'Flow | #PCDATA'; - $form->content_model_type = 'optional'; - $form->attr['target'] = 'FrameTarget'; - - $input = $this->addBlankElement('input'); - $input->attr['align'] = 'IAlign'; - - $legend = $this->addBlankElement('legend'); - $legend->attr['align'] = 'LAlign'; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/List.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/List.php deleted file mode 100644 index 79ccefafd..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/List.php +++ /dev/null @@ -1,43 +0,0 @@ - 'List'); - - public function setup($config) { - $ol = $this->addElement('ol', 'List', new HTMLPurifier_ChildDef_List(), 'Common'); - $ul = $this->addElement('ul', 'List', new HTMLPurifier_ChildDef_List(), 'Common'); - // XXX The wrap attribute is handled by MakeWellFormed. This is all - // quite unsatisfactory, because we generated this - // *specifically* for lists, and now a big chunk of the handling - // is done properly by the List ChildDef. So actually, we just - // want enough information to make autoclosing work properly, - // and then hand off the tricky stuff to the ChildDef. - $ol->wrap = 'li'; - $ul->wrap = 'li'; - $this->addElement('dl', 'List', 'Required: dt | dd', 'Common'); - - $this->addElement('li', false, 'Flow', 'Common'); - - $this->addElement('dd', false, 'Flow', 'Common'); - $this->addElement('dt', false, 'Inline', 'Common'); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Name.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Name.php deleted file mode 100644 index 3a1271a97..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Name.php +++ /dev/null @@ -1,21 +0,0 @@ -addBlankElement($name); - $element->attr['name'] = 'CDATA'; - if (!$config->get('HTML.Attr.Name.UseCDATA')) { - $element->attr_transform_post[] = new HTMLPurifier_AttrTransform_NameSync(); - } - } - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Nofollow.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Nofollow.php deleted file mode 100644 index 3aa6654a5..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Nofollow.php +++ /dev/null @@ -1,19 +0,0 @@ -addBlankElement('a'); - $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_Nofollow(); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php deleted file mode 100644 index 5f1b14abb..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/NonXMLCommonAttributes.php +++ /dev/null @@ -1,14 +0,0 @@ - array( - 'lang' => 'LanguageCode', - ) - ); -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Object.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Object.php deleted file mode 100644 index 193c1011f..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Object.php +++ /dev/null @@ -1,47 +0,0 @@ - to cater to legacy browsers: this - * module does not allow this sort of behavior - */ -class HTMLPurifier_HTMLModule_Object extends HTMLPurifier_HTMLModule -{ - - public $name = 'Object'; - public $safe = false; - - public function setup($config) { - - $this->addElement('object', 'Inline', 'Optional: #PCDATA | Flow | param', 'Common', - array( - 'archive' => 'URI', - 'classid' => 'URI', - 'codebase' => 'URI', - 'codetype' => 'Text', - 'data' => 'URI', - 'declare' => 'Bool#declare', - 'height' => 'Length', - 'name' => 'CDATA', - 'standby' => 'Text', - 'tabindex' => 'Number', - 'type' => 'ContentType', - 'width' => 'Length' - ) - ); - - $this->addElement('param', false, 'Empty', false, - array( - 'id' => 'ID', - 'name*' => 'Text', - 'type' => 'Text', - 'value' => 'Text', - 'valuetype' => 'Enum#data,ref,object' - ) - ); - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Presentation.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Presentation.php deleted file mode 100644 index 8ff0b5ed7..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Presentation.php +++ /dev/null @@ -1,36 +0,0 @@ -addElement('hr', 'Block', 'Empty', 'Common'); - $this->addElement('sub', 'Inline', 'Inline', 'Common'); - $this->addElement('sup', 'Inline', 'Inline', 'Common'); - $b = $this->addElement('b', 'Inline', 'Inline', 'Common'); - $b->formatting = true; - $big = $this->addElement('big', 'Inline', 'Inline', 'Common'); - $big->formatting = true; - $i = $this->addElement('i', 'Inline', 'Inline', 'Common'); - $i->formatting = true; - $small = $this->addElement('small', 'Inline', 'Inline', 'Common'); - $small->formatting = true; - $tt = $this->addElement('tt', 'Inline', 'Inline', 'Common'); - $tt->formatting = true; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Proprietary.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Proprietary.php deleted file mode 100644 index dd36a3de0..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Proprietary.php +++ /dev/null @@ -1,33 +0,0 @@ -addElement('marquee', 'Inline', 'Flow', 'Common', - array( - 'direction' => 'Enum#left,right,up,down', - 'behavior' => 'Enum#alternate', - 'width' => 'Length', - 'height' => 'Length', - 'scrolldelay' => 'Number', - 'scrollamount' => 'Number', - 'loop' => 'Number', - 'bgcolor' => 'Color', - 'hspace' => 'Pixels', - 'vspace' => 'Pixels', - ) - ); - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Ruby.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Ruby.php deleted file mode 100644 index b26a0a30a..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Ruby.php +++ /dev/null @@ -1,27 +0,0 @@ -addElement('ruby', 'Inline', - 'Custom: ((rb, (rt | (rp, rt, rp))) | (rbc, rtc, rtc?))', - 'Common'); - $this->addElement('rbc', false, 'Required: rb', 'Common'); - $this->addElement('rtc', false, 'Required: rt', 'Common'); - $rb = $this->addElement('rb', false, 'Inline', 'Common'); - $rb->excludes = array('ruby' => true); - $rt = $this->addElement('rt', false, 'Inline', 'Common', array('rbspan' => 'Number')); - $rt->excludes = array('ruby' => true); - $this->addElement('rp', false, 'Optional: #PCDATA', 'Common'); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/SafeEmbed.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/SafeEmbed.php deleted file mode 100644 index 9f3758a32..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/SafeEmbed.php +++ /dev/null @@ -1,34 +0,0 @@ -get('HTML.MaxImgLength'); - $embed = $this->addElement( - 'embed', 'Inline', 'Empty', 'Common', - array( - 'src*' => 'URI#embedded', - 'type' => 'Enum#application/x-shockwave-flash', - 'width' => 'Pixels#' . $max, - 'height' => 'Pixels#' . $max, - 'allowscriptaccess' => 'Enum#never', - 'allownetworking' => 'Enum#internal', - 'flashvars' => 'Text', - 'wmode' => 'Enum#window,transparent,opaque', - 'name' => 'ID', - ) - ); - $embed->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeEmbed(); - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/SafeObject.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/SafeObject.php deleted file mode 100644 index 00da342ef..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/SafeObject.php +++ /dev/null @@ -1,52 +0,0 @@ -get('HTML.MaxImgLength'); - $object = $this->addElement( - 'object', - 'Inline', - 'Optional: param | Flow | #PCDATA', - 'Common', - array( - // While technically not required by the spec, we're forcing - // it to this value. - 'type' => 'Enum#application/x-shockwave-flash', - 'width' => 'Pixels#' . $max, - 'height' => 'Pixels#' . $max, - 'data' => 'URI#embedded', - 'codebase' => new HTMLPurifier_AttrDef_Enum(array( - 'http://download.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=6,0,40,0')), - ) - ); - $object->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeObject(); - - $param = $this->addElement('param', false, 'Empty', false, - array( - 'id' => 'ID', - 'name*' => 'Text', - 'value' => 'Text' - ) - ); - $param->attr_transform_post[] = new HTMLPurifier_AttrTransform_SafeParam(); - $this->info_injector[] = 'SafeObject'; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/SafeScripting.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/SafeScripting.php deleted file mode 100644 index e32a6b6c5..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/SafeScripting.php +++ /dev/null @@ -1,37 +0,0 @@ -get('HTML.SafeScripting'); - $script = $this->addElement( - 'script', - 'Inline', - 'Empty', - null, - array( - // While technically not required by the spec, we're forcing - // it to this value. - 'type' => 'Enum#text/javascript', - 'src*' => new HTMLPurifier_AttrDef_Enum(array_keys($allowed)) - ) - ); - $script->attr_transform_pre[] = - $script->attr_transform_post[] = new HTMLPurifier_AttrTransform_ScriptRequired(); - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Scripting.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Scripting.php deleted file mode 100644 index 2ac0d8021..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Scripting.php +++ /dev/null @@ -1,54 +0,0 @@ - 'script | noscript', 'Inline' => 'script | noscript'); - public $safe = false; - - public function setup($config) { - // TODO: create custom child-definition for noscript that - // auto-wraps stray #PCDATA in a similar manner to - // blockquote's custom definition (we would use it but - // blockquote's contents are optional while noscript's contents - // are required) - - // TODO: convert this to new syntax, main problem is getting - // both content sets working - - // In theory, this could be safe, but I don't see any reason to - // allow it. - $this->info['noscript'] = new HTMLPurifier_ElementDef(); - $this->info['noscript']->attr = array( 0 => array('Common') ); - $this->info['noscript']->content_model = 'Heading | List | Block'; - $this->info['noscript']->content_model_type = 'required'; - - $this->info['script'] = new HTMLPurifier_ElementDef(); - $this->info['script']->attr = array( - 'defer' => new HTMLPurifier_AttrDef_Enum(array('defer')), - 'src' => new HTMLPurifier_AttrDef_URI(true), - 'type' => new HTMLPurifier_AttrDef_Enum(array('text/javascript')) - ); - $this->info['script']->content_model = '#PCDATA'; - $this->info['script']->content_model_type = 'optional'; - $this->info['script']->attr_transform_pre[] = - $this->info['script']->attr_transform_post[] = - new HTMLPurifier_AttrTransform_ScriptRequired(); - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/StyleAttribute.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/StyleAttribute.php deleted file mode 100644 index eb78464cc..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/StyleAttribute.php +++ /dev/null @@ -1,24 +0,0 @@ - array('style' => false), // see constructor - 'Core' => array(0 => array('Style')) - ); - - public function setup($config) { - $this->attr_collections['Style']['style'] = new HTMLPurifier_AttrDef_CSS(); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Tables.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Tables.php deleted file mode 100644 index 45c42bb3e..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Tables.php +++ /dev/null @@ -1,69 +0,0 @@ -addElement('caption', false, 'Inline', 'Common'); - - $this->addElement('table', 'Block', - new HTMLPurifier_ChildDef_Table(), 'Common', - array( - 'border' => 'Pixels', - 'cellpadding' => 'Length', - 'cellspacing' => 'Length', - 'frame' => 'Enum#void,above,below,hsides,lhs,rhs,vsides,box,border', - 'rules' => 'Enum#none,groups,rows,cols,all', - 'summary' => 'Text', - 'width' => 'Length' - ) - ); - - // common attributes - $cell_align = array( - 'align' => 'Enum#left,center,right,justify,char', - 'charoff' => 'Length', - 'valign' => 'Enum#top,middle,bottom,baseline', - ); - - $cell_t = array_merge( - array( - 'abbr' => 'Text', - 'colspan' => 'Number', - 'rowspan' => 'Number', - // Apparently, as of HTML5 this attribute only applies - // to 'th' elements. - 'scope' => 'Enum#row,col,rowgroup,colgroup', - ), - $cell_align - ); - $this->addElement('td', false, 'Flow', 'Common', $cell_t); - $this->addElement('th', false, 'Flow', 'Common', $cell_t); - - $this->addElement('tr', false, 'Required: td | th', 'Common', $cell_align); - - $cell_col = array_merge( - array( - 'span' => 'Number', - 'width' => 'MultiLength', - ), - $cell_align - ); - $this->addElement('col', false, 'Empty', 'Common', $cell_col); - $this->addElement('colgroup', false, 'Optional: col', 'Common', $cell_col); - - $this->addElement('tbody', false, 'Required: tr', 'Common', $cell_align); - $this->addElement('thead', false, 'Required: tr', 'Common', $cell_align); - $this->addElement('tfoot', false, 'Required: tr', 'Common', $cell_align); - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Target.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Target.php deleted file mode 100644 index 2b844ecc4..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Target.php +++ /dev/null @@ -1,23 +0,0 @@ -addBlankElement($name); - $e->attr = array( - 'target' => new HTMLPurifier_AttrDef_HTML_FrameTarget() - ); - } - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/TargetBlank.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/TargetBlank.php deleted file mode 100644 index e1305ec5d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/TargetBlank.php +++ /dev/null @@ -1,19 +0,0 @@ -addBlankElement('a'); - $a->attr_transform_post[] = new HTMLPurifier_AttrTransform_TargetBlank(); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Text.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Text.php deleted file mode 100644 index ae77c7188..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Text.php +++ /dev/null @@ -1,71 +0,0 @@ - 'Heading | Block | Inline' - ); - - public function setup($config) { - - // Inline Phrasal ------------------------------------------------- - $this->addElement('abbr', 'Inline', 'Inline', 'Common'); - $this->addElement('acronym', 'Inline', 'Inline', 'Common'); - $this->addElement('cite', 'Inline', 'Inline', 'Common'); - $this->addElement('dfn', 'Inline', 'Inline', 'Common'); - $this->addElement('kbd', 'Inline', 'Inline', 'Common'); - $this->addElement('q', 'Inline', 'Inline', 'Common', array('cite' => 'URI')); - $this->addElement('samp', 'Inline', 'Inline', 'Common'); - $this->addElement('var', 'Inline', 'Inline', 'Common'); - - $em = $this->addElement('em', 'Inline', 'Inline', 'Common'); - $em->formatting = true; - - $strong = $this->addElement('strong', 'Inline', 'Inline', 'Common'); - $strong->formatting = true; - - $code = $this->addElement('code', 'Inline', 'Inline', 'Common'); - $code->formatting = true; - - // Inline Structural ---------------------------------------------- - $this->addElement('span', 'Inline', 'Inline', 'Common'); - $this->addElement('br', 'Inline', 'Empty', 'Core'); - - // Block Phrasal -------------------------------------------------- - $this->addElement('address', 'Block', 'Inline', 'Common'); - $this->addElement('blockquote', 'Block', 'Optional: Heading | Block | List', 'Common', array('cite' => 'URI') ); - $pre = $this->addElement('pre', 'Block', 'Inline', 'Common'); - $pre->excludes = $this->makeLookup( - 'img', 'big', 'small', 'object', 'applet', 'font', 'basefont' ); - $this->addElement('h1', 'Heading', 'Inline', 'Common'); - $this->addElement('h2', 'Heading', 'Inline', 'Common'); - $this->addElement('h3', 'Heading', 'Inline', 'Common'); - $this->addElement('h4', 'Heading', 'Inline', 'Common'); - $this->addElement('h5', 'Heading', 'Inline', 'Common'); - $this->addElement('h6', 'Heading', 'Inline', 'Common'); - - // Block Structural ----------------------------------------------- - $p = $this->addElement('p', 'Block', 'Inline', 'Common'); - $p->autoclose = array_flip(array("address", "blockquote", "center", "dir", "div", "dl", "fieldset", "ol", "p", "ul")); - - $this->addElement('div', 'Block', 'Flow', 'Common'); - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Tidy.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Tidy.php deleted file mode 100644 index 21783f18e..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Tidy.php +++ /dev/null @@ -1,207 +0,0 @@ - 'none', 'light', 'medium', 'heavy'); - - /** - * Default level to place all fixes in. Disabled by default - */ - public $defaultLevel = null; - - /** - * Lists of fixes used by getFixesForLevel(). Format is: - * HTMLModule_Tidy->fixesForLevel[$level] = array('fix-1', 'fix-2'); - */ - public $fixesForLevel = array( - 'light' => array(), - 'medium' => array(), - 'heavy' => array() - ); - - /** - * Lazy load constructs the module by determining the necessary - * fixes to create and then delegating to the populate() function. - * @todo Wildcard matching and error reporting when an added or - * subtracted fix has no effect. - */ - public function setup($config) { - - // create fixes, initialize fixesForLevel - $fixes = $this->makeFixes(); - $this->makeFixesForLevel($fixes); - - // figure out which fixes to use - $level = $config->get('HTML.TidyLevel'); - $fixes_lookup = $this->getFixesForLevel($level); - - // get custom fix declarations: these need namespace processing - $add_fixes = $config->get('HTML.TidyAdd'); - $remove_fixes = $config->get('HTML.TidyRemove'); - - foreach ($fixes as $name => $fix) { - // needs to be refactored a little to implement globbing - if ( - isset($remove_fixes[$name]) || - (!isset($add_fixes[$name]) && !isset($fixes_lookup[$name])) - ) { - unset($fixes[$name]); - } - } - - // populate this module with necessary fixes - $this->populate($fixes); - - } - - /** - * Retrieves all fixes per a level, returning fixes for that specific - * level as well as all levels below it. - * @param $level String level identifier, see $levels for valid values - * @return Lookup up table of fixes - */ - public function getFixesForLevel($level) { - if ($level == $this->levels[0]) { - return array(); - } - $activated_levels = array(); - for ($i = 1, $c = count($this->levels); $i < $c; $i++) { - $activated_levels[] = $this->levels[$i]; - if ($this->levels[$i] == $level) break; - } - if ($i == $c) { - trigger_error( - 'Tidy level ' . htmlspecialchars($level) . ' not recognized', - E_USER_WARNING - ); - return array(); - } - $ret = array(); - foreach ($activated_levels as $level) { - foreach ($this->fixesForLevel[$level] as $fix) { - $ret[$fix] = true; - } - } - return $ret; - } - - /** - * Dynamically populates the $fixesForLevel member variable using - * the fixes array. It may be custom overloaded, used in conjunction - * with $defaultLevel, or not used at all. - */ - public function makeFixesForLevel($fixes) { - if (!isset($this->defaultLevel)) return; - if (!isset($this->fixesForLevel[$this->defaultLevel])) { - trigger_error( - 'Default level ' . $this->defaultLevel . ' does not exist', - E_USER_ERROR - ); - return; - } - $this->fixesForLevel[$this->defaultLevel] = array_keys($fixes); - } - - /** - * Populates the module with transforms and other special-case code - * based on a list of fixes passed to it - * @param $lookup Lookup table of fixes to activate - */ - public function populate($fixes) { - foreach ($fixes as $name => $fix) { - // determine what the fix is for - list($type, $params) = $this->getFixType($name); - switch ($type) { - case 'attr_transform_pre': - case 'attr_transform_post': - $attr = $params['attr']; - if (isset($params['element'])) { - $element = $params['element']; - if (empty($this->info[$element])) { - $e = $this->addBlankElement($element); - } else { - $e = $this->info[$element]; - } - } else { - $type = "info_$type"; - $e = $this; - } - // PHP does some weird parsing when I do - // $e->$type[$attr], so I have to assign a ref. - $f =& $e->$type; - $f[$attr] = $fix; - break; - case 'tag_transform': - $this->info_tag_transform[$params['element']] = $fix; - break; - case 'child': - case 'content_model_type': - $element = $params['element']; - if (empty($this->info[$element])) { - $e = $this->addBlankElement($element); - } else { - $e = $this->info[$element]; - } - $e->$type = $fix; - break; - default: - trigger_error("Fix type $type not supported", E_USER_ERROR); - break; - } - } - } - - /** - * Parses a fix name and determines what kind of fix it is, as well - * as other information defined by the fix - * @param $name String name of fix - * @return array(string $fix_type, array $fix_parameters) - * @note $fix_parameters is type dependant, see populate() for usage - * of these parameters - */ - public function getFixType($name) { - // parse it - $property = $attr = null; - if (strpos($name, '#') !== false) list($name, $property) = explode('#', $name); - if (strpos($name, '@') !== false) list($name, $attr) = explode('@', $name); - - // figure out the parameters - $params = array(); - if ($name !== '') $params['element'] = $name; - if (!is_null($attr)) $params['attr'] = $attr; - - // special case: attribute transform - if (!is_null($attr)) { - if (is_null($property)) $property = 'pre'; - $type = 'attr_transform_' . $property; - return array($type, $params); - } - - // special case: tag transform - if (is_null($property)) { - return array('tag_transform', $params); - } - - return array($property, $params); - - } - - /** - * Defines all fixes the module will perform in a compact - * associative array of fix name to fix implementation. - */ - public function makeFixes() {} - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Tidy/Name.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Tidy/Name.php deleted file mode 100644 index 61ff85ce2..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Tidy/Name.php +++ /dev/null @@ -1,24 +0,0 @@ -content_model_type != 'strictblockquote') return parent::getChildDef($def); - return new HTMLPurifier_ChildDef_StrictBlockquote($def->content_model); - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Tidy/Transitional.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Tidy/Transitional.php deleted file mode 100644 index 9960b1dd1..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/Tidy/Transitional.php +++ /dev/null @@ -1,9 +0,0 @@ - 'text-align:left;', - 'right' => 'text-align:right;', - 'top' => 'caption-side:top;', - 'bottom' => 'caption-side:bottom;' // not supported by IE - )); - - // @align for img ------------------------------------------------- - $r['img@align'] = - new HTMLPurifier_AttrTransform_EnumToCSS('align', array( - 'left' => 'float:left;', - 'right' => 'float:right;', - 'top' => 'vertical-align:top;', - 'middle' => 'vertical-align:middle;', - 'bottom' => 'vertical-align:baseline;', - )); - - // @align for table ----------------------------------------------- - $r['table@align'] = - new HTMLPurifier_AttrTransform_EnumToCSS('align', array( - 'left' => 'float:left;', - 'center' => 'margin-left:auto;margin-right:auto;', - 'right' => 'float:right;' - )); - - // @align for hr ----------------------------------------------- - $r['hr@align'] = - new HTMLPurifier_AttrTransform_EnumToCSS('align', array( - // we use both text-align and margin because these work - // for different browsers (IE and Firefox, respectively) - // and the melange makes for a pretty cross-compatible - // solution - 'left' => 'margin-left:0;margin-right:auto;text-align:left;', - 'center' => 'margin-left:auto;margin-right:auto;text-align:center;', - 'right' => 'margin-left:auto;margin-right:0;text-align:right;' - )); - - // @align for h1, h2, h3, h4, h5, h6, p, div ---------------------- - // {{{ - $align_lookup = array(); - $align_values = array('left', 'right', 'center', 'justify'); - foreach ($align_values as $v) $align_lookup[$v] = "text-align:$v;"; - // }}} - $r['h1@align'] = - $r['h2@align'] = - $r['h3@align'] = - $r['h4@align'] = - $r['h5@align'] = - $r['h6@align'] = - $r['p@align'] = - $r['div@align'] = - new HTMLPurifier_AttrTransform_EnumToCSS('align', $align_lookup); - - // @bgcolor for table, tr, td, th --------------------------------- - $r['table@bgcolor'] = - $r['td@bgcolor'] = - $r['th@bgcolor'] = - new HTMLPurifier_AttrTransform_BgColor(); - - // @border for img ------------------------------------------------ - $r['img@border'] = new HTMLPurifier_AttrTransform_Border(); - - // @clear for br -------------------------------------------------- - $r['br@clear'] = - new HTMLPurifier_AttrTransform_EnumToCSS('clear', array( - 'left' => 'clear:left;', - 'right' => 'clear:right;', - 'all' => 'clear:both;', - 'none' => 'clear:none;', - )); - - // @height for td, th --------------------------------------------- - $r['td@height'] = - $r['th@height'] = - new HTMLPurifier_AttrTransform_Length('height'); - - // @hspace for img ------------------------------------------------ - $r['img@hspace'] = new HTMLPurifier_AttrTransform_ImgSpace('hspace'); - - // @noshade for hr ------------------------------------------------ - // this transformation is not precise but often good enough. - // different browsers use different styles to designate noshade - $r['hr@noshade'] = - new HTMLPurifier_AttrTransform_BoolToCSS( - 'noshade', - 'color:#808080;background-color:#808080;border:0;' - ); - - // @nowrap for td, th --------------------------------------------- - $r['td@nowrap'] = - $r['th@nowrap'] = - new HTMLPurifier_AttrTransform_BoolToCSS( - 'nowrap', - 'white-space:nowrap;' - ); - - // @size for hr -------------------------------------------------- - $r['hr@size'] = new HTMLPurifier_AttrTransform_Length('size', 'height'); - - // @type for li, ol, ul ------------------------------------------- - // {{{ - $ul_types = array( - 'disc' => 'list-style-type:disc;', - 'square' => 'list-style-type:square;', - 'circle' => 'list-style-type:circle;' - ); - $ol_types = array( - '1' => 'list-style-type:decimal;', - 'i' => 'list-style-type:lower-roman;', - 'I' => 'list-style-type:upper-roman;', - 'a' => 'list-style-type:lower-alpha;', - 'A' => 'list-style-type:upper-alpha;' - ); - $li_types = $ul_types + $ol_types; - // }}} - - $r['ul@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $ul_types); - $r['ol@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $ol_types, true); - $r['li@type'] = new HTMLPurifier_AttrTransform_EnumToCSS('type', $li_types, true); - - // @vspace for img ------------------------------------------------ - $r['img@vspace'] = new HTMLPurifier_AttrTransform_ImgSpace('vspace'); - - // @width for hr, td, th ------------------------------------------ - $r['td@width'] = - $r['th@width'] = - $r['hr@width'] = new HTMLPurifier_AttrTransform_Length('width'); - - return $r; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/XMLCommonAttributes.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/XMLCommonAttributes.php deleted file mode 100644 index 9c0e03198..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModule/XMLCommonAttributes.php +++ /dev/null @@ -1,14 +0,0 @@ - array( - 'xml:lang' => 'LanguageCode', - ) - ); -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModuleManager.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModuleManager.php deleted file mode 100644 index 215308683..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/HTMLModuleManager.php +++ /dev/null @@ -1,418 +0,0 @@ -attrTypes = new HTMLPurifier_AttrTypes(); - $this->doctypes = new HTMLPurifier_DoctypeRegistry(); - - // setup basic modules - $common = array( - 'CommonAttributes', 'Text', 'Hypertext', 'List', - 'Presentation', 'Edit', 'Bdo', 'Tables', 'Image', - 'StyleAttribute', - // Unsafe: - 'Scripting', 'Object', 'Forms', - // Sorta legacy, but present in strict: - 'Name', - ); - $transitional = array('Legacy', 'Target', 'Iframe'); - $xml = array('XMLCommonAttributes'); - $non_xml = array('NonXMLCommonAttributes'); - - // setup basic doctypes - $this->doctypes->register( - 'HTML 4.01 Transitional', false, - array_merge($common, $transitional, $non_xml), - array('Tidy_Transitional', 'Tidy_Proprietary'), - array(), - '-//W3C//DTD HTML 4.01 Transitional//EN', - 'http://www.w3.org/TR/html4/loose.dtd' - ); - - $this->doctypes->register( - 'HTML 4.01 Strict', false, - array_merge($common, $non_xml), - array('Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'), - array(), - '-//W3C//DTD HTML 4.01//EN', - 'http://www.w3.org/TR/html4/strict.dtd' - ); - - $this->doctypes->register( - 'XHTML 1.0 Transitional', true, - array_merge($common, $transitional, $xml, $non_xml), - array('Tidy_Transitional', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Name'), - array(), - '-//W3C//DTD XHTML 1.0 Transitional//EN', - 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd' - ); - - $this->doctypes->register( - 'XHTML 1.0 Strict', true, - array_merge($common, $xml, $non_xml), - array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Strict', 'Tidy_Proprietary', 'Tidy_Name'), - array(), - '-//W3C//DTD XHTML 1.0 Strict//EN', - 'http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd' - ); - - $this->doctypes->register( - 'XHTML 1.1', true, - // Iframe is a real XHTML 1.1 module, despite being - // "transitional"! - array_merge($common, $xml, array('Ruby', 'Iframe')), - array('Tidy_Strict', 'Tidy_XHTML', 'Tidy_Proprietary', 'Tidy_Strict', 'Tidy_Name'), // Tidy_XHTML1_1 - array(), - '-//W3C//DTD XHTML 1.1//EN', - 'http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd' - ); - - } - - /** - * Registers a module to the recognized module list, useful for - * overloading pre-existing modules. - * @param $module Mixed: string module name, with or without - * HTMLPurifier_HTMLModule prefix, or instance of - * subclass of HTMLPurifier_HTMLModule. - * @param $overload Boolean whether or not to overload previous modules. - * If this is not set, and you do overload a module, - * HTML Purifier will complain with a warning. - * @note This function will not call autoload, you must instantiate - * (and thus invoke) autoload outside the method. - * @note If a string is passed as a module name, different variants - * will be tested in this order: - * - Check for HTMLPurifier_HTMLModule_$name - * - Check all prefixes with $name in order they were added - * - Check for literal object name - * - Throw fatal error - * If your object name collides with an internal class, specify - * your module manually. All modules must have been included - * externally: registerModule will not perform inclusions for you! - */ - public function registerModule($module, $overload = false) { - if (is_string($module)) { - // attempt to load the module - $original_module = $module; - $ok = false; - foreach ($this->prefixes as $prefix) { - $module = $prefix . $original_module; - if (class_exists($module)) { - $ok = true; - break; - } - } - if (!$ok) { - $module = $original_module; - if (!class_exists($module)) { - trigger_error($original_module . ' module does not exist', - E_USER_ERROR); - return; - } - } - $module = new $module(); - } - if (empty($module->name)) { - trigger_error('Module instance of ' . get_class($module) . ' must have name'); - return; - } - if (!$overload && isset($this->registeredModules[$module->name])) { - trigger_error('Overloading ' . $module->name . ' without explicit overload parameter', E_USER_WARNING); - } - $this->registeredModules[$module->name] = $module; - } - - /** - * Adds a module to the current doctype by first registering it, - * and then tacking it on to the active doctype - */ - public function addModule($module) { - $this->registerModule($module); - if (is_object($module)) $module = $module->name; - $this->userModules[] = $module; - } - - /** - * Adds a class prefix that registerModule() will use to resolve a - * string name to a concrete class - */ - public function addPrefix($prefix) { - $this->prefixes[] = $prefix; - } - - /** - * Performs processing on modules, after being called you may - * use getElement() and getElements() - * @param $config Instance of HTMLPurifier_Config - */ - public function setup($config) { - - $this->trusted = $config->get('HTML.Trusted'); - - // generate - $this->doctype = $this->doctypes->make($config); - $modules = $this->doctype->modules; - - // take out the default modules that aren't allowed - $lookup = $config->get('HTML.AllowedModules'); - $special_cases = $config->get('HTML.CoreModules'); - - if (is_array($lookup)) { - foreach ($modules as $k => $m) { - if (isset($special_cases[$m])) continue; - if (!isset($lookup[$m])) unset($modules[$k]); - } - } - - // custom modules - if ($config->get('HTML.Proprietary')) { - $modules[] = 'Proprietary'; - } - if ($config->get('HTML.SafeObject')) { - $modules[] = 'SafeObject'; - } - if ($config->get('HTML.SafeEmbed')) { - $modules[] = 'SafeEmbed'; - } - if ($config->get('HTML.SafeScripting') !== array()) { - $modules[] = 'SafeScripting'; - } - if ($config->get('HTML.Nofollow')) { - $modules[] = 'Nofollow'; - } - if ($config->get('HTML.TargetBlank')) { - $modules[] = 'TargetBlank'; - } - - // merge in custom modules - $modules = array_merge($modules, $this->userModules); - - foreach ($modules as $module) { - $this->processModule($module); - $this->modules[$module]->setup($config); - } - - foreach ($this->doctype->tidyModules as $module) { - $this->processModule($module); - $this->modules[$module]->setup($config); - } - - // prepare any injectors - foreach ($this->modules as $module) { - $n = array(); - foreach ($module->info_injector as $i => $injector) { - if (!is_object($injector)) { - $class = "HTMLPurifier_Injector_$injector"; - $injector = new $class; - } - $n[$injector->name] = $injector; - } - $module->info_injector = $n; - } - - // setup lookup table based on all valid modules - foreach ($this->modules as $module) { - foreach ($module->info as $name => $def) { - if (!isset($this->elementLookup[$name])) { - $this->elementLookup[$name] = array(); - } - $this->elementLookup[$name][] = $module->name; - } - } - - // note the different choice - $this->contentSets = new HTMLPurifier_ContentSets( - // content set assembly deals with all possible modules, - // not just ones deemed to be "safe" - $this->modules - ); - $this->attrCollections = new HTMLPurifier_AttrCollections( - $this->attrTypes, - // there is no way to directly disable a global attribute, - // but using AllowedAttributes or simply not including - // the module in your custom doctype should be sufficient - $this->modules - ); - } - - /** - * Takes a module and adds it to the active module collection, - * registering it if necessary. - */ - public function processModule($module) { - if (!isset($this->registeredModules[$module]) || is_object($module)) { - $this->registerModule($module); - } - $this->modules[$module] = $this->registeredModules[$module]; - } - - /** - * Retrieves merged element definitions. - * @return Array of HTMLPurifier_ElementDef - */ - public function getElements() { - - $elements = array(); - foreach ($this->modules as $module) { - if (!$this->trusted && !$module->safe) continue; - foreach ($module->info as $name => $v) { - if (isset($elements[$name])) continue; - $elements[$name] = $this->getElement($name); - } - } - - // remove dud elements, this happens when an element that - // appeared to be safe actually wasn't - foreach ($elements as $n => $v) { - if ($v === false) unset($elements[$n]); - } - - return $elements; - - } - - /** - * Retrieves a single merged element definition - * @param $name Name of element - * @param $trusted Boolean trusted overriding parameter: set to true - * if you want the full version of an element - * @return Merged HTMLPurifier_ElementDef - * @note You may notice that modules are getting iterated over twice (once - * in getElements() and once here). This - * is because - */ - public function getElement($name, $trusted = null) { - - if (!isset($this->elementLookup[$name])) { - return false; - } - - // setup global state variables - $def = false; - if ($trusted === null) $trusted = $this->trusted; - - // iterate through each module that has registered itself to this - // element - foreach($this->elementLookup[$name] as $module_name) { - - $module = $this->modules[$module_name]; - - // refuse to create/merge from a module that is deemed unsafe-- - // pretend the module doesn't exist--when trusted mode is not on. - if (!$trusted && !$module->safe) { - continue; - } - - // clone is used because, ideally speaking, the original - // definition should not be modified. Usually, this will - // make no difference, but for consistency's sake - $new_def = clone $module->info[$name]; - - if (!$def && $new_def->standalone) { - $def = $new_def; - } elseif ($def) { - // This will occur even if $new_def is standalone. In practice, - // this will usually result in a full replacement. - $def->mergeIn($new_def); - } else { - // :TODO: - // non-standalone definitions that don't have a standalone - // to merge into could be deferred to the end - // HOWEVER, it is perfectly valid for a non-standalone - // definition to lack a standalone definition, even - // after all processing: this allows us to safely - // specify extra attributes for elements that may not be - // enabled all in one place. In particular, this might - // be the case for trusted elements. WARNING: care must - // be taken that the /extra/ definitions are all safe. - continue; - } - - // attribute value expansions - $this->attrCollections->performInclusions($def->attr); - $this->attrCollections->expandIdentifiers($def->attr, $this->attrTypes); - - // descendants_are_inline, for ChildDef_Chameleon - if (is_string($def->content_model) && - strpos($def->content_model, 'Inline') !== false) { - if ($name != 'del' && $name != 'ins') { - // this is for you, ins/del - $def->descendants_are_inline = true; - } - } - - $this->contentSets->generateChildDef($def, $module); - } - - // This can occur if there is a blank definition, but no base to - // mix it in with - if (!$def) return false; - - // add information on required attributes - foreach ($def->attr as $attr_name => $attr_def) { - if ($attr_def->required) { - $def->required_attr[] = $attr_name; - } - } - - return $def; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/IDAccumulator.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/IDAccumulator.php deleted file mode 100644 index 73215295a..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/IDAccumulator.php +++ /dev/null @@ -1,53 +0,0 @@ -load($config->get('Attr.IDBlacklist')); - return $id_accumulator; - } - - /** - * Add an ID to the lookup table. - * @param $id ID to be added. - * @return Bool status, true if success, false if there's a dupe - */ - public function add($id) { - if (isset($this->ids[$id])) return false; - return $this->ids[$id] = true; - } - - /** - * Load a list of IDs into the lookup table - * @param $array_of_ids Array of IDs to load - * @note This function doesn't care about duplicates - */ - public function load($array_of_ids) { - foreach ($array_of_ids as $id) { - $this->ids[$id] = true; - } - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector.php deleted file mode 100644 index 5922f8130..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector.php +++ /dev/null @@ -1,239 +0,0 @@ -processToken() - * documentation. - * - * @todo Allow injectors to request a re-run on their output. This - * would help if an operation is recursive. - */ -abstract class HTMLPurifier_Injector -{ - - /** - * Advisory name of injector, this is for friendly error messages - */ - public $name; - - /** - * Instance of HTMLPurifier_HTMLDefinition - */ - protected $htmlDefinition; - - /** - * Reference to CurrentNesting variable in Context. This is an array - * list of tokens that we are currently "inside" - */ - protected $currentNesting; - - /** - * Reference to InputTokens variable in Context. This is an array - * list of the input tokens that are being processed. - */ - protected $inputTokens; - - /** - * Reference to InputIndex variable in Context. This is an integer - * array index for $this->inputTokens that indicates what token - * is currently being processed. - */ - protected $inputIndex; - - /** - * Array of elements and attributes this injector creates and therefore - * need to be allowed by the definition. Takes form of - * array('element' => array('attr', 'attr2'), 'element2') - */ - public $needed = array(); - - /** - * Index of inputTokens to rewind to. - */ - protected $rewind = false; - - /** - * Rewind to a spot to re-perform processing. This is useful if you - * deleted a node, and now need to see if this change affected any - * earlier nodes. Rewinding does not affect other injectors, and can - * result in infinite loops if not used carefully. - * @warning HTML Purifier will prevent you from fast-forwarding with this - * function. - */ - public function rewind($index) { - $this->rewind = $index; - } - - /** - * Retrieves rewind, and then unsets it. - */ - public function getRewind() { - $r = $this->rewind; - $this->rewind = false; - return $r; - } - - /** - * Prepares the injector by giving it the config and context objects: - * this allows references to important variables to be made within - * the injector. This function also checks if the HTML environment - * will work with the Injector (see checkNeeded()). - * @param $config Instance of HTMLPurifier_Config - * @param $context Instance of HTMLPurifier_Context - * @return Boolean false if success, string of missing needed element/attribute if failure - */ - public function prepare($config, $context) { - $this->htmlDefinition = $config->getHTMLDefinition(); - // Even though this might fail, some unit tests ignore this and - // still test checkNeeded, so be careful. Maybe get rid of that - // dependency. - $result = $this->checkNeeded($config); - if ($result !== false) return $result; - $this->currentNesting =& $context->get('CurrentNesting'); - $this->inputTokens =& $context->get('InputTokens'); - $this->inputIndex =& $context->get('InputIndex'); - return false; - } - - /** - * This function checks if the HTML environment - * will work with the Injector: if p tags are not allowed, the - * Auto-Paragraphing injector should not be enabled. - * @param $config Instance of HTMLPurifier_Config - * @param $context Instance of HTMLPurifier_Context - * @return Boolean false if success, string of missing needed element/attribute if failure - */ - public function checkNeeded($config) { - $def = $config->getHTMLDefinition(); - foreach ($this->needed as $element => $attributes) { - if (is_int($element)) $element = $attributes; - if (!isset($def->info[$element])) return $element; - if (!is_array($attributes)) continue; - foreach ($attributes as $name) { - if (!isset($def->info[$element]->attr[$name])) return "$element.$name"; - } - } - return false; - } - - /** - * Tests if the context node allows a certain element - * @param $name Name of element to test for - * @return True if element is allowed, false if it is not - */ - public function allowsElement($name) { - if (!empty($this->currentNesting)) { - $parent_token = array_pop($this->currentNesting); - $this->currentNesting[] = $parent_token; - $parent = $this->htmlDefinition->info[$parent_token->name]; - } else { - $parent = $this->htmlDefinition->info_parent_def; - } - if (!isset($parent->child->elements[$name]) || isset($parent->excludes[$name])) { - return false; - } - // check for exclusion - for ($i = count($this->currentNesting) - 2; $i >= 0; $i--) { - $node = $this->currentNesting[$i]; - $def = $this->htmlDefinition->info[$node->name]; - if (isset($def->excludes[$name])) return false; - } - return true; - } - - /** - * Iterator function, which starts with the next token and continues until - * you reach the end of the input tokens. - * @warning Please prevent previous references from interfering with this - * functions by setting $i = null beforehand! - * @param &$i Current integer index variable for inputTokens - * @param &$current Current token variable. Do NOT use $token, as that variable is also a reference - */ - protected function forward(&$i, &$current) { - if ($i === null) $i = $this->inputIndex + 1; - else $i++; - if (!isset($this->inputTokens[$i])) return false; - $current = $this->inputTokens[$i]; - return true; - } - - /** - * Similar to _forward, but accepts a third parameter $nesting (which - * should be initialized at 0) and stops when we hit the end tag - * for the node $this->inputIndex starts in. - */ - protected function forwardUntilEndToken(&$i, &$current, &$nesting) { - $result = $this->forward($i, $current); - if (!$result) return false; - if ($nesting === null) $nesting = 0; - if ($current instanceof HTMLPurifier_Token_Start) $nesting++; - elseif ($current instanceof HTMLPurifier_Token_End) { - if ($nesting <= 0) return false; - $nesting--; - } - return true; - } - - /** - * Iterator function, starts with the previous token and continues until - * you reach the beginning of input tokens. - * @warning Please prevent previous references from interfering with this - * functions by setting $i = null beforehand! - * @param &$i Current integer index variable for inputTokens - * @param &$current Current token variable. Do NOT use $token, as that variable is also a reference - */ - protected function backward(&$i, &$current) { - if ($i === null) $i = $this->inputIndex - 1; - else $i--; - if ($i < 0) return false; - $current = $this->inputTokens[$i]; - return true; - } - - /** - * Initializes the iterator at the current position. Use in a do {} while; - * loop to force the _forward and _backward functions to start at the - * current location. - * @warning Please prevent previous references from interfering with this - * functions by setting $i = null beforehand! - * @param &$i Current integer index variable for inputTokens - * @param &$current Current token variable. Do NOT use $token, as that variable is also a reference - */ - protected function current(&$i, &$current) { - if ($i === null) $i = $this->inputIndex; - $current = $this->inputTokens[$i]; - } - - /** - * Handler that is called when a text token is processed - */ - public function handleText(&$token) {} - - /** - * Handler that is called when a start or empty token is processed - */ - public function handleElement(&$token) {} - - /** - * Handler that is called when an end token is processed - */ - public function handleEnd(&$token) { - $this->notifyEnd($token); - } - - /** - * Notifier that is called when an end token is processed - * @note This differs from handlers in that the token is read-only - * @deprecated - */ - public function notifyEnd($token) {} - - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/AutoParagraph.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/AutoParagraph.php deleted file mode 100644 index afa760892..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/AutoParagraph.php +++ /dev/null @@ -1,345 +0,0 @@ -armor['MakeWellFormed_TagClosedError'] = true; - return $par; - } - - public function handleText(&$token) { - $text = $token->data; - // Does the current parent allow

    tags? - if ($this->allowsElement('p')) { - if (empty($this->currentNesting) || strpos($text, "\n\n") !== false) { - // Note that we have differing behavior when dealing with text - // in the anonymous root node, or a node inside the document. - // If the text as a double-newline, the treatment is the same; - // if it doesn't, see the next if-block if you're in the document. - - $i = $nesting = null; - if (!$this->forwardUntilEndToken($i, $current, $nesting) && $token->is_whitespace) { - // State 1.1: ... ^ (whitespace, then document end) - // ---- - // This is a degenerate case - } else { - if (!$token->is_whitespace || $this->_isInline($current)) { - // State 1.2: PAR1 - // ---- - - // State 1.3: PAR1\n\nPAR2 - // ------------ - - // State 1.4:

    PAR1\n\nPAR2 (see State 2) - // ------------ - $token = array($this->_pStart()); - $this->_splitText($text, $token); - } else { - // State 1.5: \n
    - // -- - } - } - } else { - // State 2:
    PAR1... (similar to 1.4) - // ---- - - // We're in an element that allows paragraph tags, but we're not - // sure if we're going to need them. - if ($this->_pLookAhead()) { - // State 2.1:
    PAR1PAR1\n\nPAR2 - // ---- - // Note: This will always be the first child, since any - // previous inline element would have triggered this very - // same routine, and found the double newline. One possible - // exception would be a comment. - $token = array($this->_pStart(), $token); - } else { - // State 2.2.1:
    PAR1
    - // ---- - - // State 2.2.2:
    PAR1PAR1
    - // ---- - } - } - // Is the current parent a

    tag? - } elseif ( - !empty($this->currentNesting) && - $this->currentNesting[count($this->currentNesting)-1]->name == 'p' - ) { - // State 3.1: ...

    PAR1 - // ---- - - // State 3.2: ...

    PAR1\n\nPAR2 - // ------------ - $token = array(); - $this->_splitText($text, $token); - // Abort! - } else { - // State 4.1: ...PAR1 - // ---- - - // State 4.2: ...PAR1\n\nPAR2 - // ------------ - } - } - - public function handleElement(&$token) { - // We don't have to check if we're already in a

    tag for block - // tokens, because the tag would have been autoclosed by MakeWellFormed. - if ($this->allowsElement('p')) { - if (!empty($this->currentNesting)) { - if ($this->_isInline($token)) { - // State 1:

    ... - // --- - - // Check if this token is adjacent to the parent token - // (seek backwards until token isn't whitespace) - $i = null; - $this->backward($i, $prev); - - if (!$prev instanceof HTMLPurifier_Token_Start) { - // Token wasn't adjacent - - if ( - $prev instanceof HTMLPurifier_Token_Text && - substr($prev->data, -2) === "\n\n" - ) { - // State 1.1.4:

    PAR1

    \n\n - // --- - - // Quite frankly, this should be handled by splitText - $token = array($this->_pStart(), $token); - } else { - // State 1.1.1:

    PAR1

    - // --- - - // State 1.1.2:

    - // --- - - // State 1.1.3:
    PAR - // --- - } - - } else { - // State 1.2.1:
    - // --- - - // Lookahead to see if

    is needed. - if ($this->_pLookAhead()) { - // State 1.3.1:

    PAR1\n\nPAR2 - // --- - $token = array($this->_pStart(), $token); - } else { - // State 1.3.2:
    PAR1
    - // --- - - // State 1.3.3:
    PAR1
    \n\n
    - // --- - } - } - } else { - // State 2.3: ...
    - // ----- - } - } else { - if ($this->_isInline($token)) { - // State 3.1: - // --- - // This is where the {p} tag is inserted, not reflected in - // inputTokens yet, however. - $token = array($this->_pStart(), $token); - } else { - // State 3.2:
    - // ----- - } - - $i = null; - if ($this->backward($i, $prev)) { - if ( - !$prev instanceof HTMLPurifier_Token_Text - ) { - // State 3.1.1: ...

    {p} - // --- - - // State 3.2.1: ...

    - // ----- - - if (!is_array($token)) $token = array($token); - array_unshift($token, new HTMLPurifier_Token_Text("\n\n")); - } else { - // State 3.1.2: ...

    \n\n{p} - // --- - - // State 3.2.2: ...

    \n\n
    - // ----- - - // Note: PAR cannot occur because PAR would have been - // wrapped in

    tags. - } - } - } - } else { - // State 2.2:

    • - // ---- - - // State 2.4:

      - // --- - } - } - - /** - * Splits up a text in paragraph tokens and appends them - * to the result stream that will replace the original - * @param $data String text data that will be processed - * into paragraphs - * @param $result Reference to array of tokens that the - * tags will be appended onto - * @param $config Instance of HTMLPurifier_Config - * @param $context Instance of HTMLPurifier_Context - */ - private function _splitText($data, &$result) { - $raw_paragraphs = explode("\n\n", $data); - $paragraphs = array(); // without empty paragraphs - $needs_start = false; - $needs_end = false; - - $c = count($raw_paragraphs); - if ($c == 1) { - // There were no double-newlines, abort quickly. In theory this - // should never happen. - $result[] = new HTMLPurifier_Token_Text($data); - return; - } - for ($i = 0; $i < $c; $i++) { - $par = $raw_paragraphs[$i]; - if (trim($par) !== '') { - $paragraphs[] = $par; - } else { - if ($i == 0) { - // Double newline at the front - if (empty($result)) { - // The empty result indicates that the AutoParagraph - // injector did not add any start paragraph tokens. - // This means that we have been in a paragraph for - // a while, and the newline means we should start a new one. - $result[] = new HTMLPurifier_Token_End('p'); - $result[] = new HTMLPurifier_Token_Text("\n\n"); - // However, the start token should only be added if - // there is more processing to be done (i.e. there are - // real paragraphs in here). If there are none, the - // next start paragraph tag will be handled by the - // next call to the injector - $needs_start = true; - } else { - // We just started a new paragraph! - // Reinstate a double-newline for presentation's sake, since - // it was in the source code. - array_unshift($result, new HTMLPurifier_Token_Text("\n\n")); - } - } elseif ($i + 1 == $c) { - // Double newline at the end - // There should be a trailing

      when we're finally done. - $needs_end = true; - } - } - } - - // Check if this was just a giant blob of whitespace. Move this earlier, - // perhaps? - if (empty($paragraphs)) { - return; - } - - // Add the start tag indicated by \n\n at the beginning of $data - if ($needs_start) { - $result[] = $this->_pStart(); - } - - // Append the paragraphs onto the result - foreach ($paragraphs as $par) { - $result[] = new HTMLPurifier_Token_Text($par); - $result[] = new HTMLPurifier_Token_End('p'); - $result[] = new HTMLPurifier_Token_Text("\n\n"); - $result[] = $this->_pStart(); - } - - // Remove trailing start token; Injector will handle this later if - // it was indeed needed. This prevents from needing to do a lookahead, - // at the cost of a lookbehind later. - array_pop($result); - - // If there is no need for an end tag, remove all of it and let - // MakeWellFormed close it later. - if (!$needs_end) { - array_pop($result); // removes \n\n - array_pop($result); // removes

      - } - - } - - /** - * Returns true if passed token is inline (and, ergo, allowed in - * paragraph tags) - */ - private function _isInline($token) { - return isset($this->htmlDefinition->info['p']->child->elements[$token->name]); - } - - /** - * Looks ahead in the token list and determines whether or not we need - * to insert a

      tag. - */ - private function _pLookAhead() { - $this->current($i, $current); - if ($current instanceof HTMLPurifier_Token_Start) $nesting = 1; - else $nesting = 0; - $ok = false; - while ($this->forwardUntilEndToken($i, $current, $nesting)) { - $result = $this->_checkNeedsP($current); - if ($result !== null) { - $ok = $result; - break; - } - } - return $ok; - } - - /** - * Determines if a particular token requires an earlier inline token - * to get a paragraph. This should be used with _forwardUntilEndToken - */ - private function _checkNeedsP($current) { - if ($current instanceof HTMLPurifier_Token_Start){ - if (!$this->_isInline($current)) { - //

      PAR1
      - // ---- - // Terminate early, since we hit a block element - return false; - } - } elseif ($current instanceof HTMLPurifier_Token_Text) { - if (strpos($current->data, "\n\n") !== false) { - //
      PAR1PAR1\n\nPAR2 - // ---- - return true; - } else { - //
      PAR1PAR1... - // ---- - } - } - return null; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/DisplayLinkURI.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/DisplayLinkURI.php deleted file mode 100644 index 9dce9bd08..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/DisplayLinkURI.php +++ /dev/null @@ -1,26 +0,0 @@ -start->attr['href'])){ - $url = $token->start->attr['href']; - unset($token->start->attr['href']); - $token = array($token, new HTMLPurifier_Token_Text(" ($url)")); - } else { - // nothing to display - } - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/Linkify.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/Linkify.php deleted file mode 100644 index 296dac282..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/Linkify.php +++ /dev/null @@ -1,46 +0,0 @@ - array('href')); - - public function handleText(&$token) { - if (!$this->allowsElement('a')) return; - - if (strpos($token->data, '://') === false) { - // our really quick heuristic failed, abort - // this may not work so well if we want to match things like - // "google.com", but then again, most people don't - return; - } - - // there is/are URL(s). Let's split the string: - // Note: this regex is extremely permissive - $bits = preg_split('#((?:https?|ftp)://[^\s\'"<>()]+)#S', $token->data, -1, PREG_SPLIT_DELIM_CAPTURE); - - $token = array(); - - // $i = index - // $c = count - // $l = is link - for ($i = 0, $c = count($bits), $l = false; $i < $c; $i++, $l = !$l) { - if (!$l) { - if ($bits[$i] === '') continue; - $token[] = new HTMLPurifier_Token_Text($bits[$i]); - } else { - $token[] = new HTMLPurifier_Token_Start('a', array('href' => $bits[$i])); - $token[] = new HTMLPurifier_Token_Text($bits[$i]); - $token[] = new HTMLPurifier_Token_End('a'); - } - } - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/PurifierLinkify.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/PurifierLinkify.php deleted file mode 100644 index ad2455a91..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/PurifierLinkify.php +++ /dev/null @@ -1,45 +0,0 @@ - array('href')); - - public function prepare($config, $context) { - $this->docURL = $config->get('AutoFormat.PurifierLinkify.DocURL'); - return parent::prepare($config, $context); - } - - public function handleText(&$token) { - if (!$this->allowsElement('a')) return; - if (strpos($token->data, '%') === false) return; - - $bits = preg_split('#%([a-z0-9]+\.[a-z0-9]+)#Si', $token->data, -1, PREG_SPLIT_DELIM_CAPTURE); - $token = array(); - - // $i = index - // $c = count - // $l = is link - for ($i = 0, $c = count($bits), $l = false; $i < $c; $i++, $l = !$l) { - if (!$l) { - if ($bits[$i] === '') continue; - $token[] = new HTMLPurifier_Token_Text($bits[$i]); - } else { - $token[] = new HTMLPurifier_Token_Start('a', - array('href' => str_replace('%s', $bits[$i], $this->docURL))); - $token[] = new HTMLPurifier_Token_Text('%' . $bits[$i]); - $token[] = new HTMLPurifier_Token_End('a'); - } - } - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/RemoveEmpty.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/RemoveEmpty.php deleted file mode 100644 index 423f079eb..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/RemoveEmpty.php +++ /dev/null @@ -1,54 +0,0 @@ - 1, 'th' => 1, 'td' => 1, 'iframe' => 1); - - public function prepare($config, $context) { - parent::prepare($config, $context); - $this->config = $config; - $this->context = $context; - $this->removeNbsp = $config->get('AutoFormat.RemoveEmpty.RemoveNbsp'); - $this->removeNbspExceptions = $config->get('AutoFormat.RemoveEmpty.RemoveNbsp.Exceptions'); - $this->attrValidator = new HTMLPurifier_AttrValidator(); - } - - public function handleElement(&$token) { - if (!$token instanceof HTMLPurifier_Token_Start) return; - $next = false; - for ($i = $this->inputIndex + 1, $c = count($this->inputTokens); $i < $c; $i++) { - $next = $this->inputTokens[$i]; - if ($next instanceof HTMLPurifier_Token_Text) { - if ($next->is_whitespace) continue; - if ($this->removeNbsp && !isset($this->removeNbspExceptions[$token->name])) { - $plain = str_replace("\xC2\xA0", "", $next->data); - $isWsOrNbsp = $plain === '' || ctype_space($plain); - if ($isWsOrNbsp) continue; - } - } - break; - } - if (!$next || ($next instanceof HTMLPurifier_Token_End && $next->name == $token->name)) { - if (isset($this->_exclude[$token->name])) return; - $this->attrValidator->validateToken($token, $this->config, $this->context); - $token->armor['ValidateAttributes'] = true; - if (isset($token->attr['id']) || isset($token->attr['name'])) return; - $token = $i - $this->inputIndex + 1; - for ($b = $this->inputIndex - 1; $b > 0; $b--) { - $prev = $this->inputTokens[$b]; - if ($prev instanceof HTMLPurifier_Token_Text && $prev->is_whitespace) continue; - break; - } - // This is safe because we removed the token that triggered this. - $this->rewind($b - 1); - return; - } - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/RemoveSpansWithoutAttributes.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/RemoveSpansWithoutAttributes.php deleted file mode 100644 index b21313470..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/RemoveSpansWithoutAttributes.php +++ /dev/null @@ -1,60 +0,0 @@ -attrValidator = new HTMLPurifier_AttrValidator(); - $this->config = $config; - $this->context = $context; - return parent::prepare($config, $context); - } - - public function handleElement(&$token) { - if ($token->name !== 'span' || !$token instanceof HTMLPurifier_Token_Start) { - return; - } - - // We need to validate the attributes now since this doesn't normally - // happen until after MakeWellFormed. If all the attributes are removed - // the span needs to be removed too. - $this->attrValidator->validateToken($token, $this->config, $this->context); - $token->armor['ValidateAttributes'] = true; - - if (!empty($token->attr)) { - return; - } - - $nesting = 0; - $spanContentTokens = array(); - while ($this->forwardUntilEndToken($i, $current, $nesting)) {} - - if ($current instanceof HTMLPurifier_Token_End && $current->name === 'span') { - // Mark closing span tag for deletion - $current->markForDeletion = true; - // Delete open span tag - $token = false; - } - } - - public function handleEnd(&$token) { - if ($token->markForDeletion) { - $token = false; - } - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/SafeObject.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/SafeObject.php deleted file mode 100644 index c1d8b0412..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Injector/SafeObject.php +++ /dev/null @@ -1,91 +0,0 @@ - 'never', - 'allowNetworking' => 'internal', - ); - protected $allowedParam = array( - 'wmode' => true, - 'movie' => true, - 'flashvars' => true, - 'src' => true, - 'allowFullScreen' => true, // if omitted, assume to be 'false' - ); - - public function prepare($config, $context) { - parent::prepare($config, $context); - } - - public function handleElement(&$token) { - if ($token->name == 'object') { - $this->objectStack[] = $token; - $this->paramStack[] = array(); - $new = array($token); - foreach ($this->addParam as $name => $value) { - $new[] = new HTMLPurifier_Token_Empty('param', array('name' => $name, 'value' => $value)); - } - $token = $new; - } elseif ($token->name == 'param') { - $nest = count($this->currentNesting) - 1; - if ($nest >= 0 && $this->currentNesting[$nest]->name === 'object') { - $i = count($this->objectStack) - 1; - if (!isset($token->attr['name'])) { - $token = false; - return; - } - $n = $token->attr['name']; - // We need this fix because YouTube doesn't supply a data - // attribute, which we need if a type is specified. This is - // *very* Flash specific. - if (!isset($this->objectStack[$i]->attr['data']) && - ($token->attr['name'] == 'movie' || $token->attr['name'] == 'src')) { - $this->objectStack[$i]->attr['data'] = $token->attr['value']; - } - // Check if the parameter is the correct value but has not - // already been added - if ( - !isset($this->paramStack[$i][$n]) && - isset($this->addParam[$n]) && - $token->attr['name'] === $this->addParam[$n] - ) { - // keep token, and add to param stack - $this->paramStack[$i][$n] = true; - } elseif (isset($this->allowedParam[$n])) { - // keep token, don't do anything to it - // (could possibly check for duplicates here) - } else { - $token = false; - } - } else { - // not directly inside an object, DENY! - $token = false; - } - } - } - - public function handleEnd(&$token) { - // This is the WRONG way of handling the object and param stacks; - // we should be inserting them directly on the relevant object tokens - // so that the global stack handling handles it. - if ($token->name == 'object') { - array_pop($this->objectStack); - array_pop($this->paramStack); - } - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Language.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Language.php deleted file mode 100644 index 3e2be03b5..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Language.php +++ /dev/null @@ -1,163 +0,0 @@ -config = $config; - $this->context = $context; - } - - /** - * Loads language object with necessary info from factory cache - * @note This is a lazy loader - */ - public function load() { - if ($this->_loaded) return; - $factory = HTMLPurifier_LanguageFactory::instance(); - $factory->loadLanguage($this->code); - foreach ($factory->keys as $key) { - $this->$key = $factory->cache[$this->code][$key]; - } - $this->_loaded = true; - } - - /** - * Retrieves a localised message. - * @param $key string identifier of message - * @return string localised message - */ - public function getMessage($key) { - if (!$this->_loaded) $this->load(); - if (!isset($this->messages[$key])) return "[$key]"; - return $this->messages[$key]; - } - - /** - * Retrieves a localised error name. - * @param $int integer error number, corresponding to PHP's error - * reporting - * @return string localised message - */ - public function getErrorName($int) { - if (!$this->_loaded) $this->load(); - if (!isset($this->errorNames[$int])) return "[Error: $int]"; - return $this->errorNames[$int]; - } - - /** - * Converts an array list into a string readable representation - */ - public function listify($array) { - $sep = $this->getMessage('Item separator'); - $sep_last = $this->getMessage('Item separator last'); - $ret = ''; - for ($i = 0, $c = count($array); $i < $c; $i++) { - if ($i == 0) { - } elseif ($i + 1 < $c) { - $ret .= $sep; - } else { - $ret .= $sep_last; - } - $ret .= $array[$i]; - } - return $ret; - } - - /** - * Formats a localised message with passed parameters - * @param $key string identifier of message - * @param $args Parameters to substitute in - * @return string localised message - * @todo Implement conditionals? Right now, some messages make - * reference to line numbers, but those aren't always available - */ - public function formatMessage($key, $args = array()) { - if (!$this->_loaded) $this->load(); - if (!isset($this->messages[$key])) return "[$key]"; - $raw = $this->messages[$key]; - $subst = array(); - $generator = false; - foreach ($args as $i => $value) { - if (is_object($value)) { - if ($value instanceof HTMLPurifier_Token) { - // factor this out some time - if (!$generator) $generator = $this->context->get('Generator'); - if (isset($value->name)) $subst['$'.$i.'.Name'] = $value->name; - if (isset($value->data)) $subst['$'.$i.'.Data'] = $value->data; - $subst['$'.$i.'.Compact'] = - $subst['$'.$i.'.Serialized'] = $generator->generateFromToken($value); - // a more complex algorithm for compact representation - // could be introduced for all types of tokens. This - // may need to be factored out into a dedicated class - if (!empty($value->attr)) { - $stripped_token = clone $value; - $stripped_token->attr = array(); - $subst['$'.$i.'.Compact'] = $generator->generateFromToken($stripped_token); - } - $subst['$'.$i.'.Line'] = $value->line ? $value->line : 'unknown'; - } - continue; - } elseif (is_array($value)) { - $keys = array_keys($value); - if (array_keys($keys) === $keys) { - // list - $subst['$'.$i] = $this->listify($value); - } else { - // associative array - // no $i implementation yet, sorry - $subst['$'.$i.'.Keys'] = $this->listify($keys); - $subst['$'.$i.'.Values'] = $this->listify(array_values($value)); - } - continue; - } - $subst['$' . $i] = $value; - } - return strtr($raw, $subst); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Language/classes/en-x-test.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Language/classes/en-x-test.php deleted file mode 100644 index d52fcb7ac..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Language/classes/en-x-test.php +++ /dev/null @@ -1,12 +0,0 @@ - 'HTML Purifier X' -); - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Language/messages/en-x-testmini.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Language/messages/en-x-testmini.php deleted file mode 100644 index 806c83fbf..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Language/messages/en-x-testmini.php +++ /dev/null @@ -1,12 +0,0 @@ - 'HTML Purifier XNone' -); - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Language/messages/en.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Language/messages/en.php deleted file mode 100644 index 8d7b5736b..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Language/messages/en.php +++ /dev/null @@ -1,63 +0,0 @@ - 'HTML Purifier', - -// for unit testing purposes -'LanguageFactoryTest: Pizza' => 'Pizza', -'LanguageTest: List' => '$1', -'LanguageTest: Hash' => '$1.Keys; $1.Values', - -'Item separator' => ', ', -'Item separator last' => ' and ', // non-Harvard style - -'ErrorCollector: No errors' => 'No errors detected. However, because error reporting is still incomplete, there may have been errors that the error collector was not notified of; please inspect the output HTML carefully.', -'ErrorCollector: At line' => ' at line $line', -'ErrorCollector: Incidental errors' => 'Incidental errors', - -'Lexer: Unclosed comment' => 'Unclosed comment', -'Lexer: Unescaped lt' => 'Unescaped less-than sign (<) should be <', -'Lexer: Missing gt' => 'Missing greater-than sign (>), previous less-than sign (<) should be escaped', -'Lexer: Missing attribute key' => 'Attribute declaration has no key', -'Lexer: Missing end quote' => 'Attribute declaration has no end quote', -'Lexer: Extracted body' => 'Removed document metadata tags', - -'Strategy_RemoveForeignElements: Tag transform' => '<$1> element transformed into $CurrentToken.Serialized', -'Strategy_RemoveForeignElements: Missing required attribute' => '$CurrentToken.Compact element missing required attribute $1', -'Strategy_RemoveForeignElements: Foreign element to text' => 'Unrecognized $CurrentToken.Serialized tag converted to text', -'Strategy_RemoveForeignElements: Foreign element removed' => 'Unrecognized $CurrentToken.Serialized tag removed', -'Strategy_RemoveForeignElements: Comment removed' => 'Comment containing "$CurrentToken.Data" removed', -'Strategy_RemoveForeignElements: Foreign meta element removed' => 'Unrecognized $CurrentToken.Serialized meta tag and all descendants removed', -'Strategy_RemoveForeignElements: Token removed to end' => 'Tags and text starting from $1 element where removed to end', -'Strategy_RemoveForeignElements: Trailing hyphen in comment removed' => 'Trailing hyphen(s) in comment removed', -'Strategy_RemoveForeignElements: Hyphens in comment collapsed' => 'Double hyphens in comments are not allowed, and were collapsed into single hyphens', - -'Strategy_MakeWellFormed: Unnecessary end tag removed' => 'Unnecessary $CurrentToken.Serialized tag removed', -'Strategy_MakeWellFormed: Unnecessary end tag to text' => 'Unnecessary $CurrentToken.Serialized tag converted to text', -'Strategy_MakeWellFormed: Tag auto closed' => '$1.Compact started on line $1.Line auto-closed by $CurrentToken.Compact', -'Strategy_MakeWellFormed: Tag carryover' => '$1.Compact started on line $1.Line auto-continued into $CurrentToken.Compact', -'Strategy_MakeWellFormed: Stray end tag removed' => 'Stray $CurrentToken.Serialized tag removed', -'Strategy_MakeWellFormed: Stray end tag to text' => 'Stray $CurrentToken.Serialized tag converted to text', -'Strategy_MakeWellFormed: Tag closed by element end' => '$1.Compact tag started on line $1.Line closed by end of $CurrentToken.Serialized', -'Strategy_MakeWellFormed: Tag closed by document end' => '$1.Compact tag started on line $1.Line closed by end of document', - -'Strategy_FixNesting: Node removed' => '$CurrentToken.Compact node removed', -'Strategy_FixNesting: Node excluded' => '$CurrentToken.Compact node removed due to descendant exclusion by ancestor element', -'Strategy_FixNesting: Node reorganized' => 'Contents of $CurrentToken.Compact node reorganized to enforce its content model', -'Strategy_FixNesting: Node contents removed' => 'Contents of $CurrentToken.Compact node removed', - -'AttrValidator: Attributes transformed' => 'Attributes on $CurrentToken.Compact transformed from $1.Keys to $2.Keys', -'AttrValidator: Attribute removed' => '$CurrentAttr.Name attribute on $CurrentToken.Compact removed', - -); - -$errorNames = array( - E_ERROR => 'Error', - E_WARNING => 'Warning', - E_NOTICE => 'Notice' -); - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/LanguageFactory.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/LanguageFactory.php deleted file mode 100644 index 134ef8c74..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/LanguageFactory.php +++ /dev/null @@ -1,198 +0,0 @@ -cache[$language_code][$key] = $value - * @value array map - */ - public $cache; - - /** - * Valid keys in the HTMLPurifier_Language object. Designates which - * variables to slurp out of a message file. - * @value array list - */ - public $keys = array('fallback', 'messages', 'errorNames'); - - /** - * Instance of HTMLPurifier_AttrDef_Lang to validate language codes - * @value object HTMLPurifier_AttrDef_Lang - */ - protected $validator; - - /** - * Cached copy of dirname(__FILE__), directory of current file without - * trailing slash - * @value string filename - */ - protected $dir; - - /** - * Keys whose contents are a hash map and can be merged - * @value array lookup - */ - protected $mergeable_keys_map = array('messages' => true, 'errorNames' => true); - - /** - * Keys whose contents are a list and can be merged - * @value array lookup - */ - protected $mergeable_keys_list = array(); - - /** - * Retrieve sole instance of the factory. - * @param $prototype Optional prototype to overload sole instance with, - * or bool true to reset to default factory. - */ - public static function instance($prototype = null) { - static $instance = null; - if ($prototype !== null) { - $instance = $prototype; - } elseif ($instance === null || $prototype == true) { - $instance = new HTMLPurifier_LanguageFactory(); - $instance->setup(); - } - return $instance; - } - - /** - * Sets up the singleton, much like a constructor - * @note Prevents people from getting this outside of the singleton - */ - public function setup() { - $this->validator = new HTMLPurifier_AttrDef_Lang(); - $this->dir = HTMLPURIFIER_PREFIX . '/HTMLPurifier'; - } - - /** - * Creates a language object, handles class fallbacks - * @param $config Instance of HTMLPurifier_Config - * @param $context Instance of HTMLPurifier_Context - * @param $code Code to override configuration with. Private parameter. - */ - public function create($config, $context, $code = false) { - - // validate language code - if ($code === false) { - $code = $this->validator->validate( - $config->get('Core.Language'), $config, $context - ); - } else { - $code = $this->validator->validate($code, $config, $context); - } - if ($code === false) $code = 'en'; // malformed code becomes English - - $pcode = str_replace('-', '_', $code); // make valid PHP classname - static $depth = 0; // recursion protection - - if ($code == 'en') { - $lang = new HTMLPurifier_Language($config, $context); - } else { - $class = 'HTMLPurifier_Language_' . $pcode; - $file = $this->dir . '/Language/classes/' . $code . '.php'; - if (file_exists($file) || class_exists($class, false)) { - $lang = new $class($config, $context); - } else { - // Go fallback - $raw_fallback = $this->getFallbackFor($code); - $fallback = $raw_fallback ? $raw_fallback : 'en'; - $depth++; - $lang = $this->create($config, $context, $fallback); - if (!$raw_fallback) { - $lang->error = true; - } - $depth--; - } - } - - $lang->code = $code; - - return $lang; - - } - - /** - * Returns the fallback language for language - * @note Loads the original language into cache - * @param $code string language code - */ - public function getFallbackFor($code) { - $this->loadLanguage($code); - return $this->cache[$code]['fallback']; - } - - /** - * Loads language into the cache, handles message file and fallbacks - * @param $code string language code - */ - public function loadLanguage($code) { - static $languages_seen = array(); // recursion guard - - // abort if we've already loaded it - if (isset($this->cache[$code])) return; - - // generate filename - $filename = $this->dir . '/Language/messages/' . $code . '.php'; - - // default fallback : may be overwritten by the ensuing include - $fallback = ($code != 'en') ? 'en' : false; - - // load primary localisation - if (!file_exists($filename)) { - // skip the include: will rely solely on fallback - $filename = $this->dir . '/Language/messages/en.php'; - $cache = array(); - } else { - include $filename; - $cache = compact($this->keys); - } - - // load fallback localisation - if (!empty($fallback)) { - - // infinite recursion guard - if (isset($languages_seen[$code])) { - trigger_error('Circular fallback reference in language ' . - $code, E_USER_ERROR); - $fallback = 'en'; - } - $language_seen[$code] = true; - - // load the fallback recursively - $this->loadLanguage($fallback); - $fallback_cache = $this->cache[$fallback]; - - // merge fallback with current language - foreach ( $this->keys as $key ) { - if (isset($cache[$key]) && isset($fallback_cache[$key])) { - if (isset($this->mergeable_keys_map[$key])) { - $cache[$key] = $cache[$key] + $fallback_cache[$key]; - } elseif (isset($this->mergeable_keys_list[$key])) { - $cache[$key] = array_merge( $fallback_cache[$key], $cache[$key] ); - } - } else { - $cache[$key] = $fallback_cache[$key]; - } - } - - } - - // save to cache for later retrieval - $this->cache[$code] = $cache; - - return; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Length.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Length.php deleted file mode 100644 index 8d2a46b7d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Length.php +++ /dev/null @@ -1,115 +0,0 @@ - true, 'ex' => true, 'px' => true, 'in' => true, - 'cm' => true, 'mm' => true, 'pt' => true, 'pc' => true - ); - - /** - * @param number $n Magnitude - * @param string $u Unit - */ - public function __construct($n = '0', $u = false) { - $this->n = (string) $n; - $this->unit = $u !== false ? (string) $u : false; - } - - /** - * @param string $s Unit string, like '2em' or '3.4in' - * @warning Does not perform validation. - */ - static public function make($s) { - if ($s instanceof HTMLPurifier_Length) return $s; - $n_length = strspn($s, '1234567890.+-'); - $n = substr($s, 0, $n_length); - $unit = substr($s, $n_length); - if ($unit === '') $unit = false; - return new HTMLPurifier_Length($n, $unit); - } - - /** - * Validates the number and unit. - */ - protected function validate() { - // Special case: - if ($this->n === '+0' || $this->n === '-0') $this->n = '0'; - if ($this->n === '0' && $this->unit === false) return true; - if (!ctype_lower($this->unit)) $this->unit = strtolower($this->unit); - if (!isset(HTMLPurifier_Length::$allowedUnits[$this->unit])) return false; - // Hack: - $def = new HTMLPurifier_AttrDef_CSS_Number(); - $result = $def->validate($this->n, false, false); - if ($result === false) return false; - $this->n = $result; - return true; - } - - /** - * Returns string representation of number. - */ - public function toString() { - if (!$this->isValid()) return false; - return $this->n . $this->unit; - } - - /** - * Retrieves string numeric magnitude. - */ - public function getN() {return $this->n;} - - /** - * Retrieves string unit. - */ - public function getUnit() {return $this->unit;} - - /** - * Returns true if this length unit is valid. - */ - public function isValid() { - if ($this->isValid === null) $this->isValid = $this->validate(); - return $this->isValid; - } - - /** - * Compares two lengths, and returns 1 if greater, -1 if less and 0 if equal. - * @warning If both values are too large or small, this calculation will - * not work properly - */ - public function compareTo($l) { - if ($l === false) return false; - if ($l->unit !== $this->unit) { - $converter = new HTMLPurifier_UnitConverter(); - $l = $converter->convert($l, $this->unit); - if ($l === false) return false; - } - return $this->n - $l->n; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Lexer.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Lexer.php deleted file mode 100644 index 9bdbbbb25..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Lexer.php +++ /dev/null @@ -1,326 +0,0 @@ -get('Core.LexerImpl'); - } - - $needs_tracking = - $config->get('Core.MaintainLineNumbers') || - $config->get('Core.CollectErrors'); - - $inst = null; - if (is_object($lexer)) { - $inst = $lexer; - } else { - - if (is_null($lexer)) { do { - // auto-detection algorithm - - if ($needs_tracking) { - $lexer = 'DirectLex'; - break; - } - - if ( - class_exists('DOMDocument') && - method_exists('DOMDocument', 'loadHTML') && - !extension_loaded('domxml') - ) { - // check for DOM support, because while it's part of the - // core, it can be disabled compile time. Also, the PECL - // domxml extension overrides the default DOM, and is evil - // and nasty and we shan't bother to support it - $lexer = 'DOMLex'; - } else { - $lexer = 'DirectLex'; - } - - } while(0); } // do..while so we can break - - // instantiate recognized string names - switch ($lexer) { - case 'DOMLex': - $inst = new HTMLPurifier_Lexer_DOMLex(); - break; - case 'DirectLex': - $inst = new HTMLPurifier_Lexer_DirectLex(); - break; - case 'PH5P': - $inst = new HTMLPurifier_Lexer_PH5P(); - break; - default: - throw new HTMLPurifier_Exception("Cannot instantiate unrecognized Lexer type " . htmlspecialchars($lexer)); - } - } - - if (!$inst) throw new HTMLPurifier_Exception('No lexer was instantiated'); - - // once PHP DOM implements native line numbers, or we - // hack out something using XSLT, remove this stipulation - if ($needs_tracking && !$inst->tracksLineNumbers) { - throw new HTMLPurifier_Exception('Cannot use lexer that does not support line numbers with Core.MaintainLineNumbers or Core.CollectErrors (use DirectLex instead)'); - } - - return $inst; - - } - - // -- CONVENIENCE MEMBERS --------------------------------------------- - - public function __construct() { - $this->_entity_parser = new HTMLPurifier_EntityParser(); - } - - /** - * Most common entity to raw value conversion table for special entities. - */ - protected $_special_entity2str = - array( - '"' => '"', - '&' => '&', - '<' => '<', - '>' => '>', - ''' => "'", - ''' => "'", - ''' => "'" - ); - - /** - * Parses special entities into the proper characters. - * - * This string will translate escaped versions of the special characters - * into the correct ones. - * - * @warning - * You should be able to treat the output of this function as - * completely parsed, but that's only because all other entities should - * have been handled previously in substituteNonSpecialEntities() - * - * @param $string String character data to be parsed. - * @returns Parsed character data. - */ - public function parseData($string) { - - // following functions require at least one character - if ($string === '') return ''; - - // subtracts amps that cannot possibly be escaped - $num_amp = substr_count($string, '&') - substr_count($string, '& ') - - ($string[strlen($string)-1] === '&' ? 1 : 0); - - if (!$num_amp) return $string; // abort if no entities - $num_esc_amp = substr_count($string, '&'); - $string = strtr($string, $this->_special_entity2str); - - // code duplication for sake of optimization, see above - $num_amp_2 = substr_count($string, '&') - substr_count($string, '& ') - - ($string[strlen($string)-1] === '&' ? 1 : 0); - - if ($num_amp_2 <= $num_esc_amp) return $string; - - // hmm... now we have some uncommon entities. Use the callback. - $string = $this->_entity_parser->substituteSpecialEntities($string); - return $string; - } - - /** - * Lexes an HTML string into tokens. - * - * @param $string String HTML. - * @return HTMLPurifier_Token array representation of HTML. - */ - public function tokenizeHTML($string, $config, $context) { - trigger_error('Call to abstract class', E_USER_ERROR); - } - - /** - * Translates CDATA sections into regular sections (through escaping). - * - * @param $string HTML string to process. - * @returns HTML with CDATA sections escaped. - */ - protected static function escapeCDATA($string) { - return preg_replace_callback( - '//s', - array('HTMLPurifier_Lexer', 'CDATACallback'), - $string - ); - } - - /** - * Special CDATA case that is especially convoluted for )#si', - array($this, 'scriptCallback'), $html); - } - - $html = $this->normalize($html, $config, $context); - - $cursor = 0; // our location in the text - $inside_tag = false; // whether or not we're parsing the inside of a tag - $array = array(); // result array - - // This is also treated to mean maintain *column* numbers too - $maintain_line_numbers = $config->get('Core.MaintainLineNumbers'); - - if ($maintain_line_numbers === null) { - // automatically determine line numbering by checking - // if error collection is on - $maintain_line_numbers = $config->get('Core.CollectErrors'); - } - - if ($maintain_line_numbers) { - $current_line = 1; - $current_col = 0; - $length = strlen($html); - } else { - $current_line = false; - $current_col = false; - $length = false; - } - $context->register('CurrentLine', $current_line); - $context->register('CurrentCol', $current_col); - $nl = "\n"; - // how often to manually recalculate. This will ALWAYS be right, - // but it's pretty wasteful. Set to 0 to turn off - $synchronize_interval = $config->get('Core.DirectLexLineNumberSyncInterval'); - - $e = false; - if ($config->get('Core.CollectErrors')) { - $e =& $context->get('ErrorCollector'); - } - - // for testing synchronization - $loops = 0; - - while(++$loops) { - - // $cursor is either at the start of a token, or inside of - // a tag (i.e. there was a < immediately before it), as indicated - // by $inside_tag - - if ($maintain_line_numbers) { - - // $rcursor, however, is always at the start of a token. - $rcursor = $cursor - (int) $inside_tag; - - // Column number is cheap, so we calculate it every round. - // We're interested at the *end* of the newline string, so - // we need to add strlen($nl) == 1 to $nl_pos before subtracting it - // from our "rcursor" position. - $nl_pos = strrpos($html, $nl, $rcursor - $length); - $current_col = $rcursor - (is_bool($nl_pos) ? 0 : $nl_pos + 1); - - // recalculate lines - if ( - $synchronize_interval && // synchronization is on - $cursor > 0 && // cursor is further than zero - $loops % $synchronize_interval === 0 // time to synchronize! - ) { - $current_line = 1 + $this->substrCount($html, $nl, 0, $cursor); - } - - } - - $position_next_lt = strpos($html, '<', $cursor); - $position_next_gt = strpos($html, '>', $cursor); - - // triggers on "asdf" but not "asdf " - // special case to set up context - if ($position_next_lt === $cursor) { - $inside_tag = true; - $cursor++; - } - - if (!$inside_tag && $position_next_lt !== false) { - // We are not inside tag and there still is another tag to parse - $token = new - HTMLPurifier_Token_Text( - $this->parseData( - substr( - $html, $cursor, $position_next_lt - $cursor - ) - ) - ); - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $position_next_lt - $cursor); - } - $array[] = $token; - $cursor = $position_next_lt + 1; - $inside_tag = true; - continue; - } elseif (!$inside_tag) { - // We are not inside tag but there are no more tags - // If we're already at the end, break - if ($cursor === strlen($html)) break; - // Create Text of rest of string - $token = new - HTMLPurifier_Token_Text( - $this->parseData( - substr( - $html, $cursor - ) - ) - ); - if ($maintain_line_numbers) $token->rawPosition($current_line, $current_col); - $array[] = $token; - break; - } elseif ($inside_tag && $position_next_gt !== false) { - // We are in tag and it is well formed - // Grab the internals of the tag - $strlen_segment = $position_next_gt - $cursor; - - if ($strlen_segment < 1) { - // there's nothing to process! - $token = new HTMLPurifier_Token_Text('<'); - $cursor++; - continue; - } - - $segment = substr($html, $cursor, $strlen_segment); - - if ($segment === false) { - // somehow, we attempted to access beyond the end of - // the string, defense-in-depth, reported by Nate Abele - break; - } - - // Check if it's a comment - if ( - substr($segment, 0, 3) === '!--' - ) { - // re-determine segment length, looking for --> - $position_comment_end = strpos($html, '-->', $cursor); - if ($position_comment_end === false) { - // uh oh, we have a comment that extends to - // infinity. Can't be helped: set comment - // end position to end of string - if ($e) $e->send(E_WARNING, 'Lexer: Unclosed comment'); - $position_comment_end = strlen($html); - $end = true; - } else { - $end = false; - } - $strlen_segment = $position_comment_end - $cursor; - $segment = substr($html, $cursor, $strlen_segment); - $token = new - HTMLPurifier_Token_Comment( - substr( - $segment, 3, $strlen_segment - 3 - ) - ); - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $strlen_segment); - } - $array[] = $token; - $cursor = $end ? $position_comment_end : $position_comment_end + 3; - $inside_tag = false; - continue; - } - - // Check if it's an end tag - $is_end_tag = (strpos($segment,'/') === 0); - if ($is_end_tag) { - $type = substr($segment, 1); - $token = new HTMLPurifier_Token_End($type); - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); - } - $array[] = $token; - $inside_tag = false; - $cursor = $position_next_gt + 1; - continue; - } - - // Check leading character is alnum, if not, we may - // have accidently grabbed an emoticon. Translate into - // text and go our merry way - if (!ctype_alpha($segment[0])) { - // XML: $segment[0] !== '_' && $segment[0] !== ':' - if ($e) $e->send(E_NOTICE, 'Lexer: Unescaped lt'); - $token = new HTMLPurifier_Token_Text('<'); - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); - } - $array[] = $token; - $inside_tag = false; - continue; - } - - // Check if it is explicitly self closing, if so, remove - // trailing slash. Remember, we could have a tag like
      , so - // any later token processing scripts must convert improperly - // classified EmptyTags from StartTags. - $is_self_closing = (strrpos($segment,'/') === $strlen_segment-1); - if ($is_self_closing) { - $strlen_segment--; - $segment = substr($segment, 0, $strlen_segment); - } - - // Check if there are any attributes - $position_first_space = strcspn($segment, $this->_whitespace); - - if ($position_first_space >= $strlen_segment) { - if ($is_self_closing) { - $token = new HTMLPurifier_Token_Empty($segment); - } else { - $token = new HTMLPurifier_Token_Start($segment); - } - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); - } - $array[] = $token; - $inside_tag = false; - $cursor = $position_next_gt + 1; - continue; - } - - // Grab out all the data - $type = substr($segment, 0, $position_first_space); - $attribute_string = - trim( - substr( - $segment, $position_first_space - ) - ); - if ($attribute_string) { - $attr = $this->parseAttributeString( - $attribute_string - , $config, $context - ); - } else { - $attr = array(); - } - - if ($is_self_closing) { - $token = new HTMLPurifier_Token_Empty($type, $attr); - } else { - $token = new HTMLPurifier_Token_Start($type, $attr); - } - if ($maintain_line_numbers) { - $token->rawPosition($current_line, $current_col); - $current_line += $this->substrCount($html, $nl, $cursor, $position_next_gt - $cursor); - } - $array[] = $token; - $cursor = $position_next_gt + 1; - $inside_tag = false; - continue; - } else { - // inside tag, but there's no ending > sign - if ($e) $e->send(E_WARNING, 'Lexer: Missing gt'); - $token = new - HTMLPurifier_Token_Text( - '<' . - $this->parseData( - substr($html, $cursor) - ) - ); - if ($maintain_line_numbers) $token->rawPosition($current_line, $current_col); - // no cursor scroll? Hmm... - $array[] = $token; - break; - } - break; - } - - $context->destroy('CurrentLine'); - $context->destroy('CurrentCol'); - return $array; - } - - /** - * PHP 5.0.x compatible substr_count that implements offset and length - */ - protected function substrCount($haystack, $needle, $offset, $length) { - static $oldVersion; - if ($oldVersion === null) { - $oldVersion = version_compare(PHP_VERSION, '5.1', '<'); - } - if ($oldVersion) { - $haystack = substr($haystack, $offset, $length); - return substr_count($haystack, $needle); - } else { - return substr_count($haystack, $needle, $offset, $length); - } - } - - /** - * Takes the inside of an HTML tag and makes an assoc array of attributes. - * - * @param $string Inside of tag excluding name. - * @returns Assoc array of attributes. - */ - public function parseAttributeString($string, $config, $context) { - $string = (string) $string; // quick typecast - - if ($string == '') return array(); // no attributes - - $e = false; - if ($config->get('Core.CollectErrors')) { - $e =& $context->get('ErrorCollector'); - } - - // let's see if we can abort as quickly as possible - // one equal sign, no spaces => one attribute - $num_equal = substr_count($string, '='); - $has_space = strpos($string, ' '); - if ($num_equal === 0 && !$has_space) { - // bool attribute - return array($string => $string); - } elseif ($num_equal === 1 && !$has_space) { - // only one attribute - list($key, $quoted_value) = explode('=', $string); - $quoted_value = trim($quoted_value); - if (!$key) { - if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key'); - return array(); - } - if (!$quoted_value) return array($key => ''); - $first_char = @$quoted_value[0]; - $last_char = @$quoted_value[strlen($quoted_value)-1]; - - $same_quote = ($first_char == $last_char); - $open_quote = ($first_char == '"' || $first_char == "'"); - - if ( $same_quote && $open_quote) { - // well behaved - $value = substr($quoted_value, 1, strlen($quoted_value) - 2); - } else { - // not well behaved - if ($open_quote) { - if ($e) $e->send(E_ERROR, 'Lexer: Missing end quote'); - $value = substr($quoted_value, 1); - } else { - $value = $quoted_value; - } - } - if ($value === false) $value = ''; - return array($key => $this->parseData($value)); - } - - // setup loop environment - $array = array(); // return assoc array of attributes - $cursor = 0; // current position in string (moves forward) - $size = strlen($string); // size of the string (stays the same) - - // if we have unquoted attributes, the parser expects a terminating - // space, so let's guarantee that there's always a terminating space. - $string .= ' '; - - while(true) { - - if ($cursor >= $size) { - break; - } - - $cursor += ($value = strspn($string, $this->_whitespace, $cursor)); - // grab the key - - $key_begin = $cursor; //we're currently at the start of the key - - // scroll past all characters that are the key (not whitespace or =) - $cursor += strcspn($string, $this->_whitespace . '=', $cursor); - - $key_end = $cursor; // now at the end of the key - - $key = substr($string, $key_begin, $key_end - $key_begin); - - if (!$key) { - if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key'); - $cursor += strcspn($string, $this->_whitespace, $cursor + 1); // prevent infinite loop - continue; // empty key - } - - // scroll past all whitespace - $cursor += strspn($string, $this->_whitespace, $cursor); - - if ($cursor >= $size) { - $array[$key] = $key; - break; - } - - // if the next character is an equal sign, we've got a regular - // pair, otherwise, it's a bool attribute - $first_char = @$string[$cursor]; - - if ($first_char == '=') { - // key="value" - - $cursor++; - $cursor += strspn($string, $this->_whitespace, $cursor); - - if ($cursor === false) { - $array[$key] = ''; - break; - } - - // we might be in front of a quote right now - - $char = @$string[$cursor]; - - if ($char == '"' || $char == "'") { - // it's quoted, end bound is $char - $cursor++; - $value_begin = $cursor; - $cursor = strpos($string, $char, $cursor); - $value_end = $cursor; - } else { - // it's not quoted, end bound is whitespace - $value_begin = $cursor; - $cursor += strcspn($string, $this->_whitespace, $cursor); - $value_end = $cursor; - } - - // we reached a premature end - if ($cursor === false) { - $cursor = $size; - $value_end = $cursor; - } - - $value = substr($string, $value_begin, $value_end - $value_begin); - if ($value === false) $value = ''; - $array[$key] = $this->parseData($value); - $cursor++; - - } else { - // boolattr - if ($key !== '') { - $array[$key] = $key; - } else { - // purely theoretical - if ($e) $e->send(E_ERROR, 'Lexer: Missing attribute key'); - } - - } - } - return $array; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Lexer/PH5P.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Lexer/PH5P.php deleted file mode 100644 index faf00b829..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Lexer/PH5P.php +++ /dev/null @@ -1,3904 +0,0 @@ -normalize($html, $config, $context); - $new_html = $this->wrapHTML($new_html, $config, $context); - try { - $parser = new HTML5($new_html); - $doc = $parser->save(); - } catch (DOMException $e) { - // Uh oh, it failed. Punt to DirectLex. - $lexer = new HTMLPurifier_Lexer_DirectLex(); - $context->register('PH5PError', $e); // save the error, so we can detect it - return $lexer->tokenizeHTML($html, $config, $context); // use original HTML - } - $tokens = array(); - $this->tokenizeDOM( - $doc->getElementsByTagName('html')->item(0)-> // - getElementsByTagName('body')->item(0)-> // - getElementsByTagName('div')->item(0) //
      - , $tokens); - return $tokens; - } - -} - -/* - -Copyright 2007 Jeroen van der Meer - -Permission is hereby granted, free of charge, to any person obtaining a -copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be included -in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS -OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. -IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, -TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE -SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - -*/ - -class HTML5 { - private $data; - private $char; - private $EOF; - private $state; - private $tree; - private $token; - private $content_model; - private $escape = false; - private $entities = array('AElig;','AElig','AMP;','AMP','Aacute;','Aacute', - 'Acirc;','Acirc','Agrave;','Agrave','Alpha;','Aring;','Aring','Atilde;', - 'Atilde','Auml;','Auml','Beta;','COPY;','COPY','Ccedil;','Ccedil','Chi;', - 'Dagger;','Delta;','ETH;','ETH','Eacute;','Eacute','Ecirc;','Ecirc','Egrave;', - 'Egrave','Epsilon;','Eta;','Euml;','Euml','GT;','GT','Gamma;','Iacute;', - 'Iacute','Icirc;','Icirc','Igrave;','Igrave','Iota;','Iuml;','Iuml','Kappa;', - 'LT;','LT','Lambda;','Mu;','Ntilde;','Ntilde','Nu;','OElig;','Oacute;', - 'Oacute','Ocirc;','Ocirc','Ograve;','Ograve','Omega;','Omicron;','Oslash;', - 'Oslash','Otilde;','Otilde','Ouml;','Ouml','Phi;','Pi;','Prime;','Psi;', - 'QUOT;','QUOT','REG;','REG','Rho;','Scaron;','Sigma;','THORN;','THORN', - 'TRADE;','Tau;','Theta;','Uacute;','Uacute','Ucirc;','Ucirc','Ugrave;', - 'Ugrave','Upsilon;','Uuml;','Uuml','Xi;','Yacute;','Yacute','Yuml;','Zeta;', - 'aacute;','aacute','acirc;','acirc','acute;','acute','aelig;','aelig', - 'agrave;','agrave','alefsym;','alpha;','amp;','amp','and;','ang;','apos;', - 'aring;','aring','asymp;','atilde;','atilde','auml;','auml','bdquo;','beta;', - 'brvbar;','brvbar','bull;','cap;','ccedil;','ccedil','cedil;','cedil', - 'cent;','cent','chi;','circ;','clubs;','cong;','copy;','copy','crarr;', - 'cup;','curren;','curren','dArr;','dagger;','darr;','deg;','deg','delta;', - 'diams;','divide;','divide','eacute;','eacute','ecirc;','ecirc','egrave;', - 'egrave','empty;','emsp;','ensp;','epsilon;','equiv;','eta;','eth;','eth', - 'euml;','euml','euro;','exist;','fnof;','forall;','frac12;','frac12', - 'frac14;','frac14','frac34;','frac34','frasl;','gamma;','ge;','gt;','gt', - 'hArr;','harr;','hearts;','hellip;','iacute;','iacute','icirc;','icirc', - 'iexcl;','iexcl','igrave;','igrave','image;','infin;','int;','iota;', - 'iquest;','iquest','isin;','iuml;','iuml','kappa;','lArr;','lambda;','lang;', - 'laquo;','laquo','larr;','lceil;','ldquo;','le;','lfloor;','lowast;','loz;', - 'lrm;','lsaquo;','lsquo;','lt;','lt','macr;','macr','mdash;','micro;','micro', - 'middot;','middot','minus;','mu;','nabla;','nbsp;','nbsp','ndash;','ne;', - 'ni;','not;','not','notin;','nsub;','ntilde;','ntilde','nu;','oacute;', - 'oacute','ocirc;','ocirc','oelig;','ograve;','ograve','oline;','omega;', - 'omicron;','oplus;','or;','ordf;','ordf','ordm;','ordm','oslash;','oslash', - 'otilde;','otilde','otimes;','ouml;','ouml','para;','para','part;','permil;', - 'perp;','phi;','pi;','piv;','plusmn;','plusmn','pound;','pound','prime;', - 'prod;','prop;','psi;','quot;','quot','rArr;','radic;','rang;','raquo;', - 'raquo','rarr;','rceil;','rdquo;','real;','reg;','reg','rfloor;','rho;', - 'rlm;','rsaquo;','rsquo;','sbquo;','scaron;','sdot;','sect;','sect','shy;', - 'shy','sigma;','sigmaf;','sim;','spades;','sub;','sube;','sum;','sup1;', - 'sup1','sup2;','sup2','sup3;','sup3','sup;','supe;','szlig;','szlig','tau;', - 'there4;','theta;','thetasym;','thinsp;','thorn;','thorn','tilde;','times;', - 'times','trade;','uArr;','uacute;','uacute','uarr;','ucirc;','ucirc', - 'ugrave;','ugrave','uml;','uml','upsih;','upsilon;','uuml;','uuml','weierp;', - 'xi;','yacute;','yacute','yen;','yen','yuml;','yuml','zeta;','zwj;','zwnj;'); - - const PCDATA = 0; - const RCDATA = 1; - const CDATA = 2; - const PLAINTEXT = 3; - - const DOCTYPE = 0; - const STARTTAG = 1; - const ENDTAG = 2; - const COMMENT = 3; - const CHARACTR = 4; - const EOF = 5; - - public function __construct($data) { - - $this->data = $data; - $this->char = -1; - $this->EOF = strlen($data); - $this->tree = new HTML5TreeConstructer; - $this->content_model = self::PCDATA; - - $this->state = 'data'; - - while($this->state !== null) { - $this->{$this->state.'State'}(); - } - } - - public function save() { - return $this->tree->save(); - } - - private function char() { - return ($this->char < $this->EOF) - ? $this->data[$this->char] - : false; - } - - private function character($s, $l = 0) { - if($s + $l < $this->EOF) { - if($l === 0) { - return $this->data[$s]; - } else { - return substr($this->data, $s, $l); - } - } - } - - private function characters($char_class, $start) { - return preg_replace('#^(['.$char_class.']+).*#s', '\\1', substr($this->data, $start)); - } - - private function dataState() { - // Consume the next input character - $this->char++; - $char = $this->char(); - - if($char === '&' && ($this->content_model === self::PCDATA || $this->content_model === self::RCDATA)) { - /* U+0026 AMPERSAND (&) - When the content model flag is set to one of the PCDATA or RCDATA - states: switch to the entity data state. Otherwise: treat it as per - the "anything else" entry below. */ - $this->state = 'entityData'; - - } elseif($char === '-') { - /* If the content model flag is set to either the RCDATA state or - the CDATA state, and the escape flag is false, and there are at - least three characters before this one in the input stream, and the - last four characters in the input stream, including this one, are - U+003C LESS-THAN SIGN, U+0021 EXCLAMATION MARK, U+002D HYPHEN-MINUS, - and U+002D HYPHEN-MINUS (""), - set the escape flag to false. */ - if(($this->content_model === self::RCDATA || - $this->content_model === self::CDATA) && $this->escape === true && - $this->character($this->char, 3) === '-->') { - $this->escape = false; - } - - /* In any case, emit the input character as a character token. - Stay in the data state. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => $char - )); - - } elseif($this->char === $this->EOF) { - /* EOF - Emit an end-of-file token. */ - $this->EOF(); - - } elseif($this->content_model === self::PLAINTEXT) { - /* When the content model flag is set to the PLAINTEXT state - THIS DIFFERS GREATLY FROM THE SPEC: Get the remaining characters of - the text and emit it as a character token. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => substr($this->data, $this->char) - )); - - $this->EOF(); - - } else { - /* Anything else - THIS DIFFERS GREATLY FROM THE SPEC: Get as many character that - otherwise would also be treated as a character token and emit it - as a single character token. Stay in the data state. */ - $len = strcspn($this->data, '<&', $this->char); - $char = substr($this->data, $this->char, $len); - $this->char += $len - 1; - - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => $char - )); - - $this->state = 'data'; - } - } - - private function entityDataState() { - // Attempt to consume an entity. - $entity = $this->entity(); - - // If nothing is returned, emit a U+0026 AMPERSAND character token. - // Otherwise, emit the character token that was returned. - $char = (!$entity) ? '&' : $entity; - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => $char - )); - - // Finally, switch to the data state. - $this->state = 'data'; - } - - private function tagOpenState() { - switch($this->content_model) { - case self::RCDATA: - case self::CDATA: - /* If the next input character is a U+002F SOLIDUS (/) character, - consume it and switch to the close tag open state. If the next - input character is not a U+002F SOLIDUS (/) character, emit a - U+003C LESS-THAN SIGN character token and switch to the data - state to process the next input character. */ - if($this->character($this->char + 1) === '/') { - $this->char++; - $this->state = 'closeTagOpen'; - - } else { - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => '<' - )); - - $this->state = 'data'; - } - break; - - case self::PCDATA: - // If the content model flag is set to the PCDATA state - // Consume the next input character: - $this->char++; - $char = $this->char(); - - if($char === '!') { - /* U+0021 EXCLAMATION MARK (!) - Switch to the markup declaration open state. */ - $this->state = 'markupDeclarationOpen'; - - } elseif($char === '/') { - /* U+002F SOLIDUS (/) - Switch to the close tag open state. */ - $this->state = 'closeTagOpen'; - - } elseif(preg_match('/^[A-Za-z]$/', $char)) { - /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z - Create a new start tag token, set its tag name to the lowercase - version of the input character (add 0x0020 to the character's code - point), then switch to the tag name state. (Don't emit the token - yet; further details will be filled in before it is emitted.) */ - $this->token = array( - 'name' => strtolower($char), - 'type' => self::STARTTAG, - 'attr' => array() - ); - - $this->state = 'tagName'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Parse error. Emit a U+003C LESS-THAN SIGN character token and a - U+003E GREATER-THAN SIGN character token. Switch to the data state. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => '<>' - )); - - $this->state = 'data'; - - } elseif($char === '?') { - /* U+003F QUESTION MARK (?) - Parse error. Switch to the bogus comment state. */ - $this->state = 'bogusComment'; - - } else { - /* Anything else - Parse error. Emit a U+003C LESS-THAN SIGN character token and - reconsume the current input character in the data state. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => '<' - )); - - $this->char--; - $this->state = 'data'; - } - break; - } - } - - private function closeTagOpenState() { - $next_node = strtolower($this->characters('A-Za-z', $this->char + 1)); - $the_same = count($this->tree->stack) > 0 && $next_node === end($this->tree->stack)->nodeName; - - if(($this->content_model === self::RCDATA || $this->content_model === self::CDATA) && - (!$the_same || ($the_same && (!preg_match('/[\t\n\x0b\x0c >\/]/', - $this->character($this->char + 1 + strlen($next_node))) || $this->EOF === $this->char)))) { - /* If the content model flag is set to the RCDATA or CDATA states then - examine the next few characters. If they do not match the tag name of - the last start tag token emitted (case insensitively), or if they do but - they are not immediately followed by one of the following characters: - * U+0009 CHARACTER TABULATION - * U+000A LINE FEED (LF) - * U+000B LINE TABULATION - * U+000C FORM FEED (FF) - * U+0020 SPACE - * U+003E GREATER-THAN SIGN (>) - * U+002F SOLIDUS (/) - * EOF - ...then there is a parse error. Emit a U+003C LESS-THAN SIGN character - token, a U+002F SOLIDUS character token, and switch to the data state - to process the next input character. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => 'state = 'data'; - - } else { - /* Otherwise, if the content model flag is set to the PCDATA state, - or if the next few characters do match that tag name, consume the - next input character: */ - $this->char++; - $char = $this->char(); - - if(preg_match('/^[A-Za-z]$/', $char)) { - /* U+0041 LATIN LETTER A through to U+005A LATIN LETTER Z - Create a new end tag token, set its tag name to the lowercase version - of the input character (add 0x0020 to the character's code point), then - switch to the tag name state. (Don't emit the token yet; further details - will be filled in before it is emitted.) */ - $this->token = array( - 'name' => strtolower($char), - 'type' => self::ENDTAG - ); - - $this->state = 'tagName'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Parse error. Switch to the data state. */ - $this->state = 'data'; - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit a U+003C LESS-THAN SIGN character token and a U+002F - SOLIDUS character token. Reconsume the EOF character in the data state. */ - $this->emitToken(array( - 'type' => self::CHARACTR, - 'data' => 'char--; - $this->state = 'data'; - - } else { - /* Parse error. Switch to the bogus comment state. */ - $this->state = 'bogusComment'; - } - } - } - - private function tagNameState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Switch to the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the EOF - character in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } elseif($char === '/') { - /* U+002F SOLIDUS (/) - Parse error unless this is a permitted slash. Switch to the before - attribute name state. */ - $this->state = 'beforeAttributeName'; - - } else { - /* Anything else - Append the current input character to the current tag token's tag name. - Stay in the tag name state. */ - $this->token['name'] .= strtolower($char); - $this->state = 'tagName'; - } - } - - private function beforeAttributeNameState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Stay in the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($char === '/') { - /* U+002F SOLIDUS (/) - Parse error unless this is a permitted slash. Stay in the before - attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the EOF - character in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Start a new attribute in the current tag token. Set that attribute's - name to the current input character, and its value to the empty string. - Switch to the attribute name state. */ - $this->token['attr'][] = array( - 'name' => strtolower($char), - 'value' => null - ); - - $this->state = 'attributeName'; - } - } - - private function attributeNameState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Stay in the before attribute name state. */ - $this->state = 'afterAttributeName'; - - } elseif($char === '=') { - /* U+003D EQUALS SIGN (=) - Switch to the before attribute value state. */ - $this->state = 'beforeAttributeValue'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($char === '/' && $this->character($this->char + 1) !== '>') { - /* U+002F SOLIDUS (/) - Parse error unless this is a permitted slash. Switch to the before - attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the EOF - character in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's name. - Stay in the attribute name state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['name'] .= strtolower($char); - - $this->state = 'attributeName'; - } - } - - private function afterAttributeNameState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Stay in the after attribute name state. */ - $this->state = 'afterAttributeName'; - - } elseif($char === '=') { - /* U+003D EQUALS SIGN (=) - Switch to the before attribute value state. */ - $this->state = 'beforeAttributeValue'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($char === '/' && $this->character($this->char + 1) !== '>') { - /* U+002F SOLIDUS (/) - Parse error unless this is a permitted slash. Switch to the - before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the EOF - character in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Start a new attribute in the current tag token. Set that attribute's - name to the current input character, and its value to the empty string. - Switch to the attribute name state. */ - $this->token['attr'][] = array( - 'name' => strtolower($char), - 'value' => null - ); - - $this->state = 'attributeName'; - } - } - - private function beforeAttributeValueState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Stay in the before attribute value state. */ - $this->state = 'beforeAttributeValue'; - - } elseif($char === '"') { - /* U+0022 QUOTATION MARK (") - Switch to the attribute value (double-quoted) state. */ - $this->state = 'attributeValueDoubleQuoted'; - - } elseif($char === '&') { - /* U+0026 AMPERSAND (&) - Switch to the attribute value (unquoted) state and reconsume - this input character. */ - $this->char--; - $this->state = 'attributeValueUnquoted'; - - } elseif($char === '\'') { - /* U+0027 APOSTROPHE (') - Switch to the attribute value (single-quoted) state. */ - $this->state = 'attributeValueSingleQuoted'; - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's value. - Switch to the attribute value (unquoted) state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - - $this->state = 'attributeValueUnquoted'; - } - } - - private function attributeValueDoubleQuotedState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if($char === '"') { - /* U+0022 QUOTATION MARK (") - Switch to the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($char === '&') { - /* U+0026 AMPERSAND (&) - Switch to the entity in attribute value state. */ - $this->entityInAttributeValueState('double'); - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the character - in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's value. - Stay in the attribute value (double-quoted) state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - - $this->state = 'attributeValueDoubleQuoted'; - } - } - - private function attributeValueSingleQuotedState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if($char === '\'') { - /* U+0022 QUOTATION MARK (') - Switch to the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($char === '&') { - /* U+0026 AMPERSAND (&) - Switch to the entity in attribute value state. */ - $this->entityInAttributeValueState('single'); - - } elseif($this->char === $this->EOF) { - /* EOF - Parse error. Emit the current tag token. Reconsume the character - in the data state. */ - $this->emitToken($this->token); - - $this->char--; - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's value. - Stay in the attribute value (single-quoted) state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - - $this->state = 'attributeValueSingleQuoted'; - } - } - - private function attributeValueUnquotedState() { - // Consume the next input character: - $this->char++; - $char = $this->character($this->char); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - /* U+0009 CHARACTER TABULATION - U+000A LINE FEED (LF) - U+000B LINE TABULATION - U+000C FORM FEED (FF) - U+0020 SPACE - Switch to the before attribute name state. */ - $this->state = 'beforeAttributeName'; - - } elseif($char === '&') { - /* U+0026 AMPERSAND (&) - Switch to the entity in attribute value state. */ - $this->entityInAttributeValueState(); - - } elseif($char === '>') { - /* U+003E GREATER-THAN SIGN (>) - Emit the current tag token. Switch to the data state. */ - $this->emitToken($this->token); - $this->state = 'data'; - - } else { - /* Anything else - Append the current input character to the current attribute's value. - Stay in the attribute value (unquoted) state. */ - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - - $this->state = 'attributeValueUnquoted'; - } - } - - private function entityInAttributeValueState() { - // Attempt to consume an entity. - $entity = $this->entity(); - - // If nothing is returned, append a U+0026 AMPERSAND character to the - // current attribute's value. Otherwise, emit the character token that - // was returned. - $char = (!$entity) - ? '&' - : $entity; - - $last = count($this->token['attr']) - 1; - $this->token['attr'][$last]['value'] .= $char; - } - - private function bogusCommentState() { - /* Consume every character up to the first U+003E GREATER-THAN SIGN - character (>) or the end of the file (EOF), whichever comes first. Emit - a comment token whose data is the concatenation of all the characters - starting from and including the character that caused the state machine - to switch into the bogus comment state, up to and including the last - consumed character before the U+003E character, if any, or up to the - end of the file otherwise. (If the comment was started by the end of - the file (EOF), the token is empty.) */ - $data = $this->characters('^>', $this->char); - $this->emitToken(array( - 'data' => $data, - 'type' => self::COMMENT - )); - - $this->char += strlen($data); - - /* Switch to the data state. */ - $this->state = 'data'; - - /* If the end of the file was reached, reconsume the EOF character. */ - if($this->char === $this->EOF) { - $this->char = $this->EOF - 1; - } - } - - private function markupDeclarationOpenState() { - /* If the next two characters are both U+002D HYPHEN-MINUS (-) - characters, consume those two characters, create a comment token whose - data is the empty string, and switch to the comment state. */ - if($this->character($this->char + 1, 2) === '--') { - $this->char += 2; - $this->state = 'comment'; - $this->token = array( - 'data' => null, - 'type' => self::COMMENT - ); - - /* Otherwise if the next seven chacacters are a case-insensitive match - for the word "DOCTYPE", then consume those characters and switch to the - DOCTYPE state. */ - } elseif(strtolower($this->character($this->char + 1, 7)) === 'doctype') { - $this->char += 7; - $this->state = 'doctype'; - - /* Otherwise, is is a parse error. Switch to the bogus comment state. - The next character that is consumed, if any, is the first character - that will be in the comment. */ - } else { - $this->char++; - $this->state = 'bogusComment'; - } - } - - private function commentState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - /* U+002D HYPHEN-MINUS (-) */ - if($char === '-') { - /* Switch to the comment dash state */ - $this->state = 'commentDash'; - - /* EOF */ - } elseif($this->char === $this->EOF) { - /* Parse error. Emit the comment token. Reconsume the EOF character - in the data state. */ - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - /* Anything else */ - } else { - /* Append the input character to the comment token's data. Stay in - the comment state. */ - $this->token['data'] .= $char; - } - } - - private function commentDashState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - /* U+002D HYPHEN-MINUS (-) */ - if($char === '-') { - /* Switch to the comment end state */ - $this->state = 'commentEnd'; - - /* EOF */ - } elseif($this->char === $this->EOF) { - /* Parse error. Emit the comment token. Reconsume the EOF character - in the data state. */ - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - /* Anything else */ - } else { - /* Append a U+002D HYPHEN-MINUS (-) character and the input - character to the comment token's data. Switch to the comment state. */ - $this->token['data'] .= '-'.$char; - $this->state = 'comment'; - } - } - - private function commentEndState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if($char === '>') { - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($char === '-') { - $this->token['data'] .= '-'; - - } elseif($this->char === $this->EOF) { - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - } else { - $this->token['data'] .= '--'.$char; - $this->state = 'comment'; - } - } - - private function doctypeState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - $this->state = 'beforeDoctypeName'; - - } else { - $this->char--; - $this->state = 'beforeDoctypeName'; - } - } - - private function beforeDoctypeNameState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - // Stay in the before DOCTYPE name state. - - } elseif(preg_match('/^[a-z]$/', $char)) { - $this->token = array( - 'name' => strtoupper($char), - 'type' => self::DOCTYPE, - 'error' => true - ); - - $this->state = 'doctypeName'; - - } elseif($char === '>') { - $this->emitToken(array( - 'name' => null, - 'type' => self::DOCTYPE, - 'error' => true - )); - - $this->state = 'data'; - - } elseif($this->char === $this->EOF) { - $this->emitToken(array( - 'name' => null, - 'type' => self::DOCTYPE, - 'error' => true - )); - - $this->char--; - $this->state = 'data'; - - } else { - $this->token = array( - 'name' => $char, - 'type' => self::DOCTYPE, - 'error' => true - ); - - $this->state = 'doctypeName'; - } - } - - private function doctypeNameState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - $this->state = 'AfterDoctypeName'; - - } elseif($char === '>') { - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif(preg_match('/^[a-z]$/', $char)) { - $this->token['name'] .= strtoupper($char); - - } elseif($this->char === $this->EOF) { - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - } else { - $this->token['name'] .= $char; - } - - $this->token['error'] = ($this->token['name'] === 'HTML') - ? false - : true; - } - - private function afterDoctypeNameState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if(preg_match('/^[\t\n\x0b\x0c ]$/', $char)) { - // Stay in the DOCTYPE name state. - - } elseif($char === '>') { - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($this->char === $this->EOF) { - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - } else { - $this->token['error'] = true; - $this->state = 'bogusDoctype'; - } - } - - private function bogusDoctypeState() { - /* Consume the next input character: */ - $this->char++; - $char = $this->char(); - - if($char === '>') { - $this->emitToken($this->token); - $this->state = 'data'; - - } elseif($this->char === $this->EOF) { - $this->emitToken($this->token); - $this->char--; - $this->state = 'data'; - - } else { - // Stay in the bogus DOCTYPE state. - } - } - - private function entity() { - $start = $this->char; - - // This section defines how to consume an entity. This definition is - // used when parsing entities in text and in attributes. - - // The behaviour depends on the identity of the next character (the - // one immediately after the U+0026 AMPERSAND character): - - switch($this->character($this->char + 1)) { - // U+0023 NUMBER SIGN (#) - case '#': - - // The behaviour further depends on the character after the - // U+0023 NUMBER SIGN: - switch($this->character($this->char + 1)) { - // U+0078 LATIN SMALL LETTER X - // U+0058 LATIN CAPITAL LETTER X - case 'x': - case 'X': - // Follow the steps below, but using the range of - // characters U+0030 DIGIT ZERO through to U+0039 DIGIT - // NINE, U+0061 LATIN SMALL LETTER A through to U+0066 - // LATIN SMALL LETTER F, and U+0041 LATIN CAPITAL LETTER - // A, through to U+0046 LATIN CAPITAL LETTER F (in other - // words, 0-9, A-F, a-f). - $char = 1; - $char_class = '0-9A-Fa-f'; - break; - - // Anything else - default: - // Follow the steps below, but using the range of - // characters U+0030 DIGIT ZERO through to U+0039 DIGIT - // NINE (i.e. just 0-9). - $char = 0; - $char_class = '0-9'; - break; - } - - // Consume as many characters as match the range of characters - // given above. - $this->char++; - $e_name = $this->characters($char_class, $this->char + $char + 1); - $entity = $this->character($start, $this->char); - $cond = strlen($e_name) > 0; - - // The rest of the parsing happens bellow. - break; - - // Anything else - default: - // Consume the maximum number of characters possible, with the - // consumed characters case-sensitively matching one of the - // identifiers in the first column of the entities table. - $e_name = $this->characters('0-9A-Za-z;', $this->char + 1); - $len = strlen($e_name); - - for($c = 1; $c <= $len; $c++) { - $id = substr($e_name, 0, $c); - $this->char++; - - if(in_array($id, $this->entities)) { - if ($e_name[$c-1] !== ';') { - if ($c < $len && $e_name[$c] == ';') { - $this->char++; // consume extra semicolon - } - } - $entity = $id; - break; - } - } - - $cond = isset($entity); - // The rest of the parsing happens bellow. - break; - } - - if(!$cond) { - // If no match can be made, then this is a parse error. No - // characters are consumed, and nothing is returned. - $this->char = $start; - return false; - } - - // Return a character token for the character corresponding to the - // entity name (as given by the second column of the entities table). - return html_entity_decode('&'.$entity.';', ENT_QUOTES, 'UTF-8'); - } - - private function emitToken($token) { - $emit = $this->tree->emitToken($token); - - if(is_int($emit)) { - $this->content_model = $emit; - - } elseif($token['type'] === self::ENDTAG) { - $this->content_model = self::PCDATA; - } - } - - private function EOF() { - $this->state = null; - $this->tree->emitToken(array( - 'type' => self::EOF - )); - } -} - -class HTML5TreeConstructer { - public $stack = array(); - - private $phase; - private $mode; - private $dom; - private $foster_parent = null; - private $a_formatting = array(); - - private $head_pointer = null; - private $form_pointer = null; - - private $scoping = array('button','caption','html','marquee','object','table','td','th'); - private $formatting = array('a','b','big','em','font','i','nobr','s','small','strike','strong','tt','u'); - private $special = array('address','area','base','basefont','bgsound', - 'blockquote','body','br','center','col','colgroup','dd','dir','div','dl', - 'dt','embed','fieldset','form','frame','frameset','h1','h2','h3','h4','h5', - 'h6','head','hr','iframe','image','img','input','isindex','li','link', - 'listing','menu','meta','noembed','noframes','noscript','ol','optgroup', - 'option','p','param','plaintext','pre','script','select','spacer','style', - 'tbody','textarea','tfoot','thead','title','tr','ul','wbr'); - - // The different phases. - const INIT_PHASE = 0; - const ROOT_PHASE = 1; - const MAIN_PHASE = 2; - const END_PHASE = 3; - - // The different insertion modes for the main phase. - const BEFOR_HEAD = 0; - const IN_HEAD = 1; - const AFTER_HEAD = 2; - const IN_BODY = 3; - const IN_TABLE = 4; - const IN_CAPTION = 5; - const IN_CGROUP = 6; - const IN_TBODY = 7; - const IN_ROW = 8; - const IN_CELL = 9; - const IN_SELECT = 10; - const AFTER_BODY = 11; - const IN_FRAME = 12; - const AFTR_FRAME = 13; - - // The different types of elements. - const SPECIAL = 0; - const SCOPING = 1; - const FORMATTING = 2; - const PHRASING = 3; - - const MARKER = 0; - - public function __construct() { - $this->phase = self::INIT_PHASE; - $this->mode = self::BEFOR_HEAD; - $this->dom = new DOMDocument; - - $this->dom->encoding = 'UTF-8'; - $this->dom->preserveWhiteSpace = true; - $this->dom->substituteEntities = true; - $this->dom->strictErrorChecking = false; - } - - // Process tag tokens - public function emitToken($token) { - switch($this->phase) { - case self::INIT_PHASE: return $this->initPhase($token); break; - case self::ROOT_PHASE: return $this->rootElementPhase($token); break; - case self::MAIN_PHASE: return $this->mainPhase($token); break; - case self::END_PHASE : return $this->trailingEndPhase($token); break; - } - } - - private function initPhase($token) { - /* Initially, the tree construction stage must handle each token - emitted from the tokenisation stage as follows: */ - - /* A DOCTYPE token that is marked as being in error - A comment token - A start tag token - An end tag token - A character token that is not one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE - An end-of-file token */ - if((isset($token['error']) && $token['error']) || - $token['type'] === HTML5::COMMENT || - $token['type'] === HTML5::STARTTAG || - $token['type'] === HTML5::ENDTAG || - $token['type'] === HTML5::EOF || - ($token['type'] === HTML5::CHARACTR && isset($token['data']) && - !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data']))) { - /* This specification does not define how to handle this case. In - particular, user agents may ignore the entirety of this specification - altogether for such documents, and instead invoke special parse modes - with a greater emphasis on backwards compatibility. */ - - $this->phase = self::ROOT_PHASE; - return $this->rootElementPhase($token); - - /* A DOCTYPE token marked as being correct */ - } elseif(isset($token['error']) && !$token['error']) { - /* Append a DocumentType node to the Document node, with the name - attribute set to the name given in the DOCTYPE token (which will be - "HTML"), and the other attributes specific to DocumentType objects - set to null, empty lists, or the empty string as appropriate. */ - $doctype = new DOMDocumentType(null, null, 'HTML'); - - /* Then, switch to the root element phase of the tree construction - stage. */ - $this->phase = self::ROOT_PHASE; - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - } elseif(isset($token['data']) && preg_match('/^[\t\n\x0b\x0c ]+$/', - $token['data'])) { - /* Append that character to the Document node. */ - $text = $this->dom->createTextNode($token['data']); - $this->dom->appendChild($text); - } - } - - private function rootElementPhase($token) { - /* After the initial phase, as each token is emitted from the tokenisation - stage, it must be processed as described in this section. */ - - /* A DOCTYPE token */ - if($token['type'] === HTML5::DOCTYPE) { - // Parse error. Ignore the token. - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the Document object with the data - attribute set to the data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - $this->dom->appendChild($comment); - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - } elseif($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append that character to the Document node. */ - $text = $this->dom->createTextNode($token['data']); - $this->dom->appendChild($text); - - /* A character token that is not one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED - (FF), or U+0020 SPACE - A start tag token - An end tag token - An end-of-file token */ - } elseif(($token['type'] === HTML5::CHARACTR && - !preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || - $token['type'] === HTML5::STARTTAG || - $token['type'] === HTML5::ENDTAG || - $token['type'] === HTML5::EOF) { - /* Create an HTMLElement node with the tag name html, in the HTML - namespace. Append it to the Document object. Switch to the main - phase and reprocess the current token. */ - $html = $this->dom->createElement('html'); - $this->dom->appendChild($html); - $this->stack[] = $html; - - $this->phase = self::MAIN_PHASE; - return $this->mainPhase($token); - } - } - - private function mainPhase($token) { - /* Tokens in the main phase must be handled as follows: */ - - /* A DOCTYPE token */ - if($token['type'] === HTML5::DOCTYPE) { - // Parse error. Ignore the token. - - /* A start tag token with the tag name "html" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'html') { - /* If this start tag token was not the first start tag token, then - it is a parse error. */ - - /* For each attribute on the token, check to see if the attribute - is already present on the top element of the stack of open elements. - If it is not, add the attribute and its corresponding value to that - element. */ - foreach($token['attr'] as $attr) { - if(!$this->stack[0]->hasAttribute($attr['name'])) { - $this->stack[0]->setAttribute($attr['name'], $attr['value']); - } - } - - /* An end-of-file token */ - } elseif($token['type'] === HTML5::EOF) { - /* Generate implied end tags. */ - $this->generateImpliedEndTags(); - - /* Anything else. */ - } else { - /* Depends on the insertion mode: */ - switch($this->mode) { - case self::BEFOR_HEAD: return $this->beforeHead($token); break; - case self::IN_HEAD: return $this->inHead($token); break; - case self::AFTER_HEAD: return $this->afterHead($token); break; - case self::IN_BODY: return $this->inBody($token); break; - case self::IN_TABLE: return $this->inTable($token); break; - case self::IN_CAPTION: return $this->inCaption($token); break; - case self::IN_CGROUP: return $this->inColumnGroup($token); break; - case self::IN_TBODY: return $this->inTableBody($token); break; - case self::IN_ROW: return $this->inRow($token); break; - case self::IN_CELL: return $this->inCell($token); break; - case self::IN_SELECT: return $this->inSelect($token); break; - case self::AFTER_BODY: return $this->afterBody($token); break; - case self::IN_FRAME: return $this->inFrameset($token); break; - case self::AFTR_FRAME: return $this->afterFrameset($token); break; - case self::END_PHASE: return $this->trailingEndPhase($token); break; - } - } - } - - private function beforeHead($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data attribute - set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* A start tag token with the tag name "head" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') { - /* Create an element for the token, append the new element to the - current node and push it onto the stack of open elements. */ - $element = $this->insertElement($token); - - /* Set the head element pointer to this new element node. */ - $this->head_pointer = $element; - - /* Change the insertion mode to "in head". */ - $this->mode = self::IN_HEAD; - - /* A start tag token whose tag name is one of: "base", "link", "meta", - "script", "style", "title". Or an end tag with the tag name "html". - Or a character token that is not one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE. Or any other start tag token */ - } elseif($token['type'] === HTML5::STARTTAG || - ($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') || - ($token['type'] === HTML5::CHARACTR && !preg_match('/^[\t\n\x0b\x0c ]$/', - $token['data']))) { - /* Act as if a start tag token with the tag name "head" and no - attributes had been seen, then reprocess the current token. */ - $this->beforeHead(array( - 'name' => 'head', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - return $this->inHead($token); - - /* Any other end tag */ - } elseif($token['type'] === HTML5::ENDTAG) { - /* Parse error. Ignore the token. */ - } - } - - private function inHead($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE. - - THIS DIFFERS FROM THE SPEC: If the current node is either a title, style - or script element, append the character to the current node regardless - of its content. */ - if(($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || ( - $token['type'] === HTML5::CHARACTR && in_array(end($this->stack)->nodeName, - array('title', 'style', 'script')))) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data attribute - set to the data given in the comment token. */ - $this->insertComment($token['data']); - - } elseif($token['type'] === HTML5::ENDTAG && - in_array($token['name'], array('title', 'style', 'script'))) { - array_pop($this->stack); - return HTML5::PCDATA; - - /* A start tag with the tag name "title" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'title') { - /* Create an element for the token and append the new element to the - node pointed to by the head element pointer, or, if that is null - (innerHTML case), to the current node. */ - if($this->head_pointer !== null) { - $element = $this->insertElement($token, false); - $this->head_pointer->appendChild($element); - - } else { - $element = $this->insertElement($token); - } - - /* Switch the tokeniser's content model flag to the RCDATA state. */ - return HTML5::RCDATA; - - /* A start tag with the tag name "style" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'style') { - /* Create an element for the token and append the new element to the - node pointed to by the head element pointer, or, if that is null - (innerHTML case), to the current node. */ - if($this->head_pointer !== null) { - $element = $this->insertElement($token, false); - $this->head_pointer->appendChild($element); - - } else { - $this->insertElement($token); - } - - /* Switch the tokeniser's content model flag to the CDATA state. */ - return HTML5::CDATA; - - /* A start tag with the tag name "script" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'script') { - /* Create an element for the token. */ - $element = $this->insertElement($token, false); - $this->head_pointer->appendChild($element); - - /* Switch the tokeniser's content model flag to the CDATA state. */ - return HTML5::CDATA; - - /* A start tag with the tag name "base", "link", or "meta" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('base', 'link', 'meta'))) { - /* Create an element for the token and append the new element to the - node pointed to by the head element pointer, or, if that is null - (innerHTML case), to the current node. */ - if($this->head_pointer !== null) { - $element = $this->insertElement($token, false); - $this->head_pointer->appendChild($element); - array_pop($this->stack); - - } else { - $this->insertElement($token); - } - - /* An end tag with the tag name "head" */ - } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'head') { - /* If the current node is a head element, pop the current node off - the stack of open elements. */ - if($this->head_pointer->isSameNode(end($this->stack))) { - array_pop($this->stack); - - /* Otherwise, this is a parse error. */ - } else { - // k - } - - /* Change the insertion mode to "after head". */ - $this->mode = self::AFTER_HEAD; - - /* A start tag with the tag name "head" or an end tag except "html". */ - } elseif(($token['type'] === HTML5::STARTTAG && $token['name'] === 'head') || - ($token['type'] === HTML5::ENDTAG && $token['name'] !== 'html')) { - // Parse error. Ignore the token. - - /* Anything else */ - } else { - /* If the current node is a head element, act as if an end tag - token with the tag name "head" had been seen. */ - if($this->head_pointer->isSameNode(end($this->stack))) { - $this->inHead(array( - 'name' => 'head', - 'type' => HTML5::ENDTAG - )); - - /* Otherwise, change the insertion mode to "after head". */ - } else { - $this->mode = self::AFTER_HEAD; - } - - /* Then, reprocess the current token. */ - return $this->afterHead($token); - } - } - - private function afterHead($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data attribute - set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* A start tag token with the tag name "body" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'body') { - /* Insert a body element for the token. */ - $this->insertElement($token); - - /* Change the insertion mode to "in body". */ - $this->mode = self::IN_BODY; - - /* A start tag token with the tag name "frameset" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'frameset') { - /* Insert a frameset element for the token. */ - $this->insertElement($token); - - /* Change the insertion mode to "in frameset". */ - $this->mode = self::IN_FRAME; - - /* A start tag token whose tag name is one of: "base", "link", "meta", - "script", "style", "title" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('base', 'link', 'meta', 'script', 'style', 'title'))) { - /* Parse error. Switch the insertion mode back to "in head" and - reprocess the token. */ - $this->mode = self::IN_HEAD; - return $this->inHead($token); - - /* Anything else */ - } else { - /* Act as if a start tag token with the tag name "body" and no - attributes had been seen, and then reprocess the current token. */ - $this->afterHead(array( - 'name' => 'body', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - return $this->inBody($token); - } - } - - private function inBody($token) { - /* Handle the token as follows: */ - - switch($token['type']) { - /* A character token */ - case HTML5::CHARACTR: - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Append the token's character to the current node. */ - $this->insertText($token['data']); - break; - - /* A comment token */ - case HTML5::COMMENT: - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $this->insertComment($token['data']); - break; - - case HTML5::STARTTAG: - switch($token['name']) { - /* A start tag token whose tag name is one of: "script", - "style" */ - case 'script': case 'style': - /* Process the token as if the insertion mode had been "in - head". */ - return $this->inHead($token); - break; - - /* A start tag token whose tag name is one of: "base", "link", - "meta", "title" */ - case 'base': case 'link': case 'meta': case 'title': - /* Parse error. Process the token as if the insertion mode - had been "in head". */ - return $this->inHead($token); - break; - - /* A start tag token with the tag name "body" */ - case 'body': - /* Parse error. If the second element on the stack of open - elements is not a body element, or, if the stack of open - elements has only one node on it, then ignore the token. - (innerHTML case) */ - if(count($this->stack) === 1 || $this->stack[1]->nodeName !== 'body') { - // Ignore - - /* Otherwise, for each attribute on the token, check to see - if the attribute is already present on the body element (the - second element) on the stack of open elements. If it is not, - add the attribute and its corresponding value to that - element. */ - } else { - foreach($token['attr'] as $attr) { - if(!$this->stack[1]->hasAttribute($attr['name'])) { - $this->stack[1]->setAttribute($attr['name'], $attr['value']); - } - } - } - break; - - /* A start tag whose tag name is one of: "address", - "blockquote", "center", "dir", "div", "dl", "fieldset", - "listing", "menu", "ol", "p", "ul" */ - case 'address': case 'blockquote': case 'center': case 'dir': - case 'div': case 'dl': case 'fieldset': case 'listing': - case 'menu': case 'ol': case 'p': case 'ul': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been - seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - break; - - /* A start tag whose tag name is "form" */ - case 'form': - /* If the form element pointer is not null, ignore the - token with a parse error. */ - if($this->form_pointer !== null) { - // Ignore. - - /* Otherwise: */ - } else { - /* If the stack of open elements has a p element in - scope, then act as if an end tag with the tag name p - had been seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token, and set the - form element pointer to point to the element created. */ - $element = $this->insertElement($token); - $this->form_pointer = $element; - } - break; - - /* A start tag whose tag name is "li", "dd" or "dt" */ - case 'li': case 'dd': case 'dt': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been - seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - $stack_length = count($this->stack) - 1; - - for($n = $stack_length; 0 <= $n; $n--) { - /* 1. Initialise node to be the current node (the - bottommost node of the stack). */ - $stop = false; - $node = $this->stack[$n]; - $cat = $this->getElementCategory($node->tagName); - - /* 2. If node is an li, dd or dt element, then pop all - the nodes from the current node up to node, including - node, then stop this algorithm. */ - if($token['name'] === $node->tagName || ($token['name'] !== 'li' - && ($node->tagName === 'dd' || $node->tagName === 'dt'))) { - for($x = $stack_length; $x >= $n ; $x--) { - array_pop($this->stack); - } - - break; - } - - /* 3. If node is not in the formatting category, and is - not in the phrasing category, and is not an address or - div element, then stop this algorithm. */ - if($cat !== self::FORMATTING && $cat !== self::PHRASING && - $node->tagName !== 'address' && $node->tagName !== 'div') { - break; - } - } - - /* Finally, insert an HTML element with the same tag - name as the token's. */ - $this->insertElement($token); - break; - - /* A start tag token whose tag name is "plaintext" */ - case 'plaintext': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been - seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - return HTML5::PLAINTEXT; - break; - - /* A start tag whose tag name is one of: "h1", "h2", "h3", "h4", - "h5", "h6" */ - case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* If the stack of open elements has in scope an element whose - tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then - this is a parse error; pop elements from the stack until an - element with one of those tag names has been popped from the - stack. */ - while($this->elementInScope(array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'))) { - array_pop($this->stack); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - break; - - /* A start tag whose tag name is "a" */ - case 'a': - /* If the list of active formatting elements contains - an element whose tag name is "a" between the end of the - list and the last marker on the list (or the start of - the list if there is no marker on the list), then this - is a parse error; act as if an end tag with the tag name - "a" had been seen, then remove that element from the list - of active formatting elements and the stack of open - elements if the end tag didn't already remove it (it - might not have if the element is not in table scope). */ - $leng = count($this->a_formatting); - - for($n = $leng - 1; $n >= 0; $n--) { - if($this->a_formatting[$n] === self::MARKER) { - break; - - } elseif($this->a_formatting[$n]->nodeName === 'a') { - $this->emitToken(array( - 'name' => 'a', - 'type' => HTML5::ENDTAG - )); - break; - } - } - - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $el = $this->insertElement($token); - - /* Add that element to the list of active formatting - elements. */ - $this->a_formatting[] = $el; - break; - - /* A start tag whose tag name is one of: "b", "big", "em", "font", - "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ - case 'b': case 'big': case 'em': case 'font': case 'i': - case 'nobr': case 's': case 'small': case 'strike': - case 'strong': case 'tt': case 'u': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $el = $this->insertElement($token); - - /* Add that element to the list of active formatting - elements. */ - $this->a_formatting[] = $el; - break; - - /* A start tag token whose tag name is "button" */ - case 'button': - /* If the stack of open elements has a button element in scope, - then this is a parse error; act as if an end tag with the tag - name "button" had been seen, then reprocess the token. (We don't - do that. Unnecessary.) */ - if($this->elementInScope('button')) { - $this->inBody(array( - 'name' => 'button', - 'type' => HTML5::ENDTAG - )); - } - - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Insert a marker at the end of the list of active - formatting elements. */ - $this->a_formatting[] = self::MARKER; - break; - - /* A start tag token whose tag name is one of: "marquee", "object" */ - case 'marquee': case 'object': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Insert a marker at the end of the list of active - formatting elements. */ - $this->a_formatting[] = self::MARKER; - break; - - /* A start tag token whose tag name is "xmp" */ - case 'xmp': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Switch the content model flag to the CDATA state. */ - return HTML5::CDATA; - break; - - /* A start tag whose tag name is "table" */ - case 'table': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Change the insertion mode to "in table". */ - $this->mode = self::IN_TABLE; - break; - - /* A start tag whose tag name is one of: "area", "basefont", - "bgsound", "br", "embed", "img", "param", "spacer", "wbr" */ - case 'area': case 'basefont': case 'bgsound': case 'br': - case 'embed': case 'img': case 'param': case 'spacer': - case 'wbr': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Immediately pop the current node off the stack of open elements. */ - array_pop($this->stack); - break; - - /* A start tag whose tag name is "hr" */ - case 'hr': - /* If the stack of open elements has a p element in scope, - then act as if an end tag with the tag name p had been seen. */ - if($this->elementInScope('p')) { - $this->emitToken(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Immediately pop the current node off the stack of open elements. */ - array_pop($this->stack); - break; - - /* A start tag whose tag name is "image" */ - case 'image': - /* Parse error. Change the token's tag name to "img" and - reprocess it. (Don't ask.) */ - $token['name'] = 'img'; - return $this->inBody($token); - break; - - /* A start tag whose tag name is "input" */ - case 'input': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an input element for the token. */ - $element = $this->insertElement($token, false); - - /* If the form element pointer is not null, then associate the - input element with the form element pointed to by the form - element pointer. */ - $this->form_pointer !== null - ? $this->form_pointer->appendChild($element) - : end($this->stack)->appendChild($element); - - /* Pop that input element off the stack of open elements. */ - array_pop($this->stack); - break; - - /* A start tag whose tag name is "isindex" */ - case 'isindex': - /* Parse error. */ - // w/e - - /* If the form element pointer is not null, - then ignore the token. */ - if($this->form_pointer === null) { - /* Act as if a start tag token with the tag name "form" had - been seen. */ - $this->inBody(array( - 'name' => 'body', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - /* Act as if a start tag token with the tag name "hr" had - been seen. */ - $this->inBody(array( - 'name' => 'hr', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - /* Act as if a start tag token with the tag name "p" had - been seen. */ - $this->inBody(array( - 'name' => 'p', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - /* Act as if a start tag token with the tag name "label" - had been seen. */ - $this->inBody(array( - 'name' => 'label', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - /* Act as if a stream of character tokens had been seen. */ - $this->insertText('This is a searchable index. '. - 'Insert your search keywords here: '); - - /* Act as if a start tag token with the tag name "input" - had been seen, with all the attributes from the "isindex" - token, except with the "name" attribute set to the value - "isindex" (ignoring any explicit "name" attribute). */ - $attr = $token['attr']; - $attr[] = array('name' => 'name', 'value' => 'isindex'); - - $this->inBody(array( - 'name' => 'input', - 'type' => HTML5::STARTTAG, - 'attr' => $attr - )); - - /* Act as if a stream of character tokens had been seen - (see below for what they should say). */ - $this->insertText('This is a searchable index. '. - 'Insert your search keywords here: '); - - /* Act as if an end tag token with the tag name "label" - had been seen. */ - $this->inBody(array( - 'name' => 'label', - 'type' => HTML5::ENDTAG - )); - - /* Act as if an end tag token with the tag name "p" had - been seen. */ - $this->inBody(array( - 'name' => 'p', - 'type' => HTML5::ENDTAG - )); - - /* Act as if a start tag token with the tag name "hr" had - been seen. */ - $this->inBody(array( - 'name' => 'hr', - 'type' => HTML5::ENDTAG - )); - - /* Act as if an end tag token with the tag name "form" had - been seen. */ - $this->inBody(array( - 'name' => 'form', - 'type' => HTML5::ENDTAG - )); - } - break; - - /* A start tag whose tag name is "textarea" */ - case 'textarea': - $this->insertElement($token); - - /* Switch the tokeniser's content model flag to the - RCDATA state. */ - return HTML5::RCDATA; - break; - - /* A start tag whose tag name is one of: "iframe", "noembed", - "noframes" */ - case 'iframe': case 'noembed': case 'noframes': - $this->insertElement($token); - - /* Switch the tokeniser's content model flag to the CDATA state. */ - return HTML5::CDATA; - break; - - /* A start tag whose tag name is "select" */ - case 'select': - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Change the insertion mode to "in select". */ - $this->mode = self::IN_SELECT; - break; - - /* A start or end tag whose tag name is one of: "caption", "col", - "colgroup", "frame", "frameset", "head", "option", "optgroup", - "tbody", "td", "tfoot", "th", "thead", "tr". */ - case 'caption': case 'col': case 'colgroup': case 'frame': - case 'frameset': case 'head': case 'option': case 'optgroup': - case 'tbody': case 'td': case 'tfoot': case 'th': case 'thead': - case 'tr': - // Parse error. Ignore the token. - break; - - /* A start or end tag whose tag name is one of: "event-source", - "section", "nav", "article", "aside", "header", "footer", - "datagrid", "command" */ - case 'event-source': case 'section': case 'nav': case 'article': - case 'aside': case 'header': case 'footer': case 'datagrid': - case 'command': - // Work in progress! - break; - - /* A start tag token not covered by the previous entries */ - default: - /* Reconstruct the active formatting elements, if any. */ - $this->reconstructActiveFormattingElements(); - - $this->insertElement($token, true, true); - break; - } - break; - - case HTML5::ENDTAG: - switch($token['name']) { - /* An end tag with the tag name "body" */ - case 'body': - /* If the second element in the stack of open elements is - not a body element, this is a parse error. Ignore the token. - (innerHTML case) */ - if(count($this->stack) < 2 || $this->stack[1]->nodeName !== 'body') { - // Ignore. - - /* If the current node is not the body element, then this - is a parse error. */ - } elseif(end($this->stack)->nodeName !== 'body') { - // Parse error. - } - - /* Change the insertion mode to "after body". */ - $this->mode = self::AFTER_BODY; - break; - - /* An end tag with the tag name "html" */ - case 'html': - /* Act as if an end tag with tag name "body" had been seen, - then, if that token wasn't ignored, reprocess the current - token. */ - $this->inBody(array( - 'name' => 'body', - 'type' => HTML5::ENDTAG - )); - - return $this->afterBody($token); - break; - - /* An end tag whose tag name is one of: "address", "blockquote", - "center", "dir", "div", "dl", "fieldset", "listing", "menu", - "ol", "pre", "ul" */ - case 'address': case 'blockquote': case 'center': case 'dir': - case 'div': case 'dl': case 'fieldset': case 'listing': - case 'menu': case 'ol': case 'pre': case 'ul': - /* If the stack of open elements has an element in scope - with the same tag name as that of the token, then generate - implied end tags. */ - if($this->elementInScope($token['name'])) { - $this->generateImpliedEndTags(); - - /* Now, if the current node is not an element with - the same tag name as that of the token, then this - is a parse error. */ - // w/e - - /* If the stack of open elements has an element in - scope with the same tag name as that of the token, - then pop elements from this stack until an element - with that tag name has been popped from the stack. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->stack[$n]->nodeName === $token['name']) { - $n = -1; - } - - array_pop($this->stack); - } - } - break; - - /* An end tag whose tag name is "form" */ - case 'form': - /* If the stack of open elements has an element in scope - with the same tag name as that of the token, then generate - implied end tags. */ - if($this->elementInScope($token['name'])) { - $this->generateImpliedEndTags(); - - } - - if(end($this->stack)->nodeName !== $token['name']) { - /* Now, if the current node is not an element with the - same tag name as that of the token, then this is a parse - error. */ - // w/e - - } else { - /* Otherwise, if the current node is an element with - the same tag name as that of the token pop that element - from the stack. */ - array_pop($this->stack); - } - - /* In any case, set the form element pointer to null. */ - $this->form_pointer = null; - break; - - /* An end tag whose tag name is "p" */ - case 'p': - /* If the stack of open elements has a p element in scope, - then generate implied end tags, except for p elements. */ - if($this->elementInScope('p')) { - $this->generateImpliedEndTags(array('p')); - - /* If the current node is not a p element, then this is - a parse error. */ - // k - - /* If the stack of open elements has a p element in - scope, then pop elements from this stack until the stack - no longer has a p element in scope. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->elementInScope('p')) { - array_pop($this->stack); - - } else { - break; - } - } - } - break; - - /* An end tag whose tag name is "dd", "dt", or "li" */ - case 'dd': case 'dt': case 'li': - /* If the stack of open elements has an element in scope - whose tag name matches the tag name of the token, then - generate implied end tags, except for elements with the - same tag name as the token. */ - if($this->elementInScope($token['name'])) { - $this->generateImpliedEndTags(array($token['name'])); - - /* If the current node is not an element with the same - tag name as the token, then this is a parse error. */ - // w/e - - /* If the stack of open elements has an element in scope - whose tag name matches the tag name of the token, then - pop elements from this stack until an element with that - tag name has been popped from the stack. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->stack[$n]->nodeName === $token['name']) { - $n = -1; - } - - array_pop($this->stack); - } - } - break; - - /* An end tag whose tag name is one of: "h1", "h2", "h3", "h4", - "h5", "h6" */ - case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': - $elements = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6'); - - /* If the stack of open elements has in scope an element whose - tag name is one of "h1", "h2", "h3", "h4", "h5", or "h6", then - generate implied end tags. */ - if($this->elementInScope($elements)) { - $this->generateImpliedEndTags(); - - /* Now, if the current node is not an element with the same - tag name as that of the token, then this is a parse error. */ - // w/e - - /* If the stack of open elements has in scope an element - whose tag name is one of "h1", "h2", "h3", "h4", "h5", or - "h6", then pop elements from the stack until an element - with one of those tag names has been popped from the stack. */ - while($this->elementInScope($elements)) { - array_pop($this->stack); - } - } - break; - - /* An end tag whose tag name is one of: "a", "b", "big", "em", - "font", "i", "nobr", "s", "small", "strike", "strong", "tt", "u" */ - case 'a': case 'b': case 'big': case 'em': case 'font': - case 'i': case 'nobr': case 's': case 'small': case 'strike': - case 'strong': case 'tt': case 'u': - /* 1. Let the formatting element be the last element in - the list of active formatting elements that: - * is between the end of the list and the last scope - marker in the list, if any, or the start of the list - otherwise, and - * has the same tag name as the token. - */ - while(true) { - for($a = count($this->a_formatting) - 1; $a >= 0; $a--) { - if($this->a_formatting[$a] === self::MARKER) { - break; - - } elseif($this->a_formatting[$a]->tagName === $token['name']) { - $formatting_element = $this->a_formatting[$a]; - $in_stack = in_array($formatting_element, $this->stack, true); - $fe_af_pos = $a; - break; - } - } - - /* If there is no such node, or, if that node is - also in the stack of open elements but the element - is not in scope, then this is a parse error. Abort - these steps. The token is ignored. */ - if(!isset($formatting_element) || ($in_stack && - !$this->elementInScope($token['name']))) { - break; - - /* Otherwise, if there is such a node, but that node - is not in the stack of open elements, then this is a - parse error; remove the element from the list, and - abort these steps. */ - } elseif(isset($formatting_element) && !$in_stack) { - unset($this->a_formatting[$fe_af_pos]); - $this->a_formatting = array_merge($this->a_formatting); - break; - } - - /* 2. Let the furthest block be the topmost node in the - stack of open elements that is lower in the stack - than the formatting element, and is not an element in - the phrasing or formatting categories. There might - not be one. */ - $fe_s_pos = array_search($formatting_element, $this->stack, true); - $length = count($this->stack); - - for($s = $fe_s_pos + 1; $s < $length; $s++) { - $category = $this->getElementCategory($this->stack[$s]->nodeName); - - if($category !== self::PHRASING && $category !== self::FORMATTING) { - $furthest_block = $this->stack[$s]; - } - } - - /* 3. If there is no furthest block, then the UA must - skip the subsequent steps and instead just pop all - the nodes from the bottom of the stack of open - elements, from the current node up to the formatting - element, and remove the formatting element from the - list of active formatting elements. */ - if(!isset($furthest_block)) { - for($n = $length - 1; $n >= $fe_s_pos; $n--) { - array_pop($this->stack); - } - - unset($this->a_formatting[$fe_af_pos]); - $this->a_formatting = array_merge($this->a_formatting); - break; - } - - /* 4. Let the common ancestor be the element - immediately above the formatting element in the stack - of open elements. */ - $common_ancestor = $this->stack[$fe_s_pos - 1]; - - /* 5. If the furthest block has a parent node, then - remove the furthest block from its parent node. */ - if($furthest_block->parentNode !== null) { - $furthest_block->parentNode->removeChild($furthest_block); - } - - /* 6. Let a bookmark note the position of the - formatting element in the list of active formatting - elements relative to the elements on either side - of it in the list. */ - $bookmark = $fe_af_pos; - - /* 7. Let node and last node be the furthest block. - Follow these steps: */ - $node = $furthest_block; - $last_node = $furthest_block; - - while(true) { - for($n = array_search($node, $this->stack, true) - 1; $n >= 0; $n--) { - /* 7.1 Let node be the element immediately - prior to node in the stack of open elements. */ - $node = $this->stack[$n]; - - /* 7.2 If node is not in the list of active - formatting elements, then remove node from - the stack of open elements and then go back - to step 1. */ - if(!in_array($node, $this->a_formatting, true)) { - unset($this->stack[$n]); - $this->stack = array_merge($this->stack); - - } else { - break; - } - } - - /* 7.3 Otherwise, if node is the formatting - element, then go to the next step in the overall - algorithm. */ - if($node === $formatting_element) { - break; - - /* 7.4 Otherwise, if last node is the furthest - block, then move the aforementioned bookmark to - be immediately after the node in the list of - active formatting elements. */ - } elseif($last_node === $furthest_block) { - $bookmark = array_search($node, $this->a_formatting, true) + 1; - } - - /* 7.5 If node has any children, perform a - shallow clone of node, replace the entry for - node in the list of active formatting elements - with an entry for the clone, replace the entry - for node in the stack of open elements with an - entry for the clone, and let node be the clone. */ - if($node->hasChildNodes()) { - $clone = $node->cloneNode(); - $s_pos = array_search($node, $this->stack, true); - $a_pos = array_search($node, $this->a_formatting, true); - - $this->stack[$s_pos] = $clone; - $this->a_formatting[$a_pos] = $clone; - $node = $clone; - } - - /* 7.6 Insert last node into node, first removing - it from its previous parent node if any. */ - if($last_node->parentNode !== null) { - $last_node->parentNode->removeChild($last_node); - } - - $node->appendChild($last_node); - - /* 7.7 Let last node be node. */ - $last_node = $node; - } - - /* 8. Insert whatever last node ended up being in - the previous step into the common ancestor node, - first removing it from its previous parent node if - any. */ - if($last_node->parentNode !== null) { - $last_node->parentNode->removeChild($last_node); - } - - $common_ancestor->appendChild($last_node); - - /* 9. Perform a shallow clone of the formatting - element. */ - $clone = $formatting_element->cloneNode(); - - /* 10. Take all of the child nodes of the furthest - block and append them to the clone created in the - last step. */ - while($furthest_block->hasChildNodes()) { - $child = $furthest_block->firstChild; - $furthest_block->removeChild($child); - $clone->appendChild($child); - } - - /* 11. Append that clone to the furthest block. */ - $furthest_block->appendChild($clone); - - /* 12. Remove the formatting element from the list - of active formatting elements, and insert the clone - into the list of active formatting elements at the - position of the aforementioned bookmark. */ - $fe_af_pos = array_search($formatting_element, $this->a_formatting, true); - unset($this->a_formatting[$fe_af_pos]); - $this->a_formatting = array_merge($this->a_formatting); - - $af_part1 = array_slice($this->a_formatting, 0, $bookmark - 1); - $af_part2 = array_slice($this->a_formatting, $bookmark, count($this->a_formatting)); - $this->a_formatting = array_merge($af_part1, array($clone), $af_part2); - - /* 13. Remove the formatting element from the stack - of open elements, and insert the clone into the stack - of open elements immediately after (i.e. in a more - deeply nested position than) the position of the - furthest block in that stack. */ - $fe_s_pos = array_search($formatting_element, $this->stack, true); - $fb_s_pos = array_search($furthest_block, $this->stack, true); - unset($this->stack[$fe_s_pos]); - - $s_part1 = array_slice($this->stack, 0, $fb_s_pos); - $s_part2 = array_slice($this->stack, $fb_s_pos + 1, count($this->stack)); - $this->stack = array_merge($s_part1, array($clone), $s_part2); - - /* 14. Jump back to step 1 in this series of steps. */ - unset($formatting_element, $fe_af_pos, $fe_s_pos, $furthest_block); - } - break; - - /* An end tag token whose tag name is one of: "button", - "marquee", "object" */ - case 'button': case 'marquee': case 'object': - /* If the stack of open elements has an element in scope whose - tag name matches the tag name of the token, then generate implied - tags. */ - if($this->elementInScope($token['name'])) { - $this->generateImpliedEndTags(); - - /* Now, if the current node is not an element with the same - tag name as the token, then this is a parse error. */ - // k - - /* Now, if the stack of open elements has an element in scope - whose tag name matches the tag name of the token, then pop - elements from the stack until that element has been popped from - the stack, and clear the list of active formatting elements up - to the last marker. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->stack[$n]->nodeName === $token['name']) { - $n = -1; - } - - array_pop($this->stack); - } - - $marker = end(array_keys($this->a_formatting, self::MARKER, true)); - - for($n = count($this->a_formatting) - 1; $n > $marker; $n--) { - array_pop($this->a_formatting); - } - } - break; - - /* Or an end tag whose tag name is one of: "area", "basefont", - "bgsound", "br", "embed", "hr", "iframe", "image", "img", - "input", "isindex", "noembed", "noframes", "param", "select", - "spacer", "table", "textarea", "wbr" */ - case 'area': case 'basefont': case 'bgsound': case 'br': - case 'embed': case 'hr': case 'iframe': case 'image': - case 'img': case 'input': case 'isindex': case 'noembed': - case 'noframes': case 'param': case 'select': case 'spacer': - case 'table': case 'textarea': case 'wbr': - // Parse error. Ignore the token. - break; - - /* An end tag token not covered by the previous entries */ - default: - for($n = count($this->stack) - 1; $n >= 0; $n--) { - /* Initialise node to be the current node (the bottommost - node of the stack). */ - $node = end($this->stack); - - /* If node has the same tag name as the end tag token, - then: */ - if($token['name'] === $node->nodeName) { - /* Generate implied end tags. */ - $this->generateImpliedEndTags(); - - /* If the tag name of the end tag token does not - match the tag name of the current node, this is a - parse error. */ - // k - - /* Pop all the nodes from the current node up to - node, including node, then stop this algorithm. */ - for($x = count($this->stack) - $n; $x >= $n; $x--) { - array_pop($this->stack); - } - - } else { - $category = $this->getElementCategory($node); - - if($category !== self::SPECIAL && $category !== self::SCOPING) { - /* Otherwise, if node is in neither the formatting - category nor the phrasing category, then this is a - parse error. Stop this algorithm. The end tag token - is ignored. */ - return false; - } - } - } - break; - } - break; - } - } - - private function inTable($token) { - $clear = array('html', 'table'); - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $text = $this->dom->createTextNode($token['data']); - end($this->stack)->appendChild($text); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - end($this->stack)->appendChild($comment); - - /* A start tag whose tag name is "caption" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'caption') { - /* Clear the stack back to a table context. */ - $this->clearStackToTableContext($clear); - - /* Insert a marker at the end of the list of active - formatting elements. */ - $this->a_formatting[] = self::MARKER; - - /* Insert an HTML element for the token, then switch the - insertion mode to "in caption". */ - $this->insertElement($token); - $this->mode = self::IN_CAPTION; - - /* A start tag whose tag name is "colgroup" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'colgroup') { - /* Clear the stack back to a table context. */ - $this->clearStackToTableContext($clear); - - /* Insert an HTML element for the token, then switch the - insertion mode to "in column group". */ - $this->insertElement($token); - $this->mode = self::IN_CGROUP; - - /* A start tag whose tag name is "col" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'col') { - $this->inTable(array( - 'name' => 'colgroup', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - $this->inColumnGroup($token); - - /* A start tag whose tag name is one of: "tbody", "tfoot", "thead" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('tbody', 'tfoot', 'thead'))) { - /* Clear the stack back to a table context. */ - $this->clearStackToTableContext($clear); - - /* Insert an HTML element for the token, then switch the insertion - mode to "in table body". */ - $this->insertElement($token); - $this->mode = self::IN_TBODY; - - /* A start tag whose tag name is one of: "td", "th", "tr" */ - } elseif($token['type'] === HTML5::STARTTAG && - in_array($token['name'], array('td', 'th', 'tr'))) { - /* Act as if a start tag token with the tag name "tbody" had been - seen, then reprocess the current token. */ - $this->inTable(array( - 'name' => 'tbody', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - return $this->inTableBody($token); - - /* A start tag whose tag name is "table" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'table') { - /* Parse error. Act as if an end tag token with the tag name "table" - had been seen, then, if that token wasn't ignored, reprocess the - current token. */ - $this->inTable(array( - 'name' => 'table', - 'type' => HTML5::ENDTAG - )); - - return $this->mainPhase($token); - - /* An end tag whose tag name is "table" */ - } elseif($token['type'] === HTML5::ENDTAG && - $token['name'] === 'table') { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. (innerHTML case) */ - if(!$this->elementInScope($token['name'], true)) { - return false; - - /* Otherwise: */ - } else { - /* Generate implied end tags. */ - $this->generateImpliedEndTags(); - - /* Now, if the current node is not a table element, then this - is a parse error. */ - // w/e - - /* Pop elements from this stack until a table element has been - popped from the stack. */ - while(true) { - $current = end($this->stack)->nodeName; - array_pop($this->stack); - - if($current === 'table') { - break; - } - } - - /* Reset the insertion mode appropriately. */ - $this->resetInsertionMode(); - } - - /* An end tag whose tag name is one of: "body", "caption", "col", - "colgroup", "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('body', 'caption', 'col', 'colgroup', 'html', 'tbody', 'td', - 'tfoot', 'th', 'thead', 'tr'))) { - // Parse error. Ignore the token. - - /* Anything else */ - } else { - /* Parse error. Process the token as if the insertion mode was "in - body", with the following exception: */ - - /* If the current node is a table, tbody, tfoot, thead, or tr - element, then, whenever a node would be inserted into the current - node, it must instead be inserted into the foster parent element. */ - if(in_array(end($this->stack)->nodeName, - array('table', 'tbody', 'tfoot', 'thead', 'tr'))) { - /* The foster parent element is the parent element of the last - table element in the stack of open elements, if there is a - table element and it has such a parent element. If there is no - table element in the stack of open elements (innerHTML case), - then the foster parent element is the first element in the - stack of open elements (the html element). Otherwise, if there - is a table element in the stack of open elements, but the last - table element in the stack of open elements has no parent, or - its parent node is not an element, then the foster parent - element is the element before the last table element in the - stack of open elements. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->stack[$n]->nodeName === 'table') { - $table = $this->stack[$n]; - break; - } - } - - if(isset($table) && $table->parentNode !== null) { - $this->foster_parent = $table->parentNode; - - } elseif(!isset($table)) { - $this->foster_parent = $this->stack[0]; - - } elseif(isset($table) && ($table->parentNode === null || - $table->parentNode->nodeType !== XML_ELEMENT_NODE)) { - $this->foster_parent = $this->stack[$n - 1]; - } - } - - $this->inBody($token); - } - } - - private function inCaption($token) { - /* An end tag whose tag name is "caption" */ - if($token['type'] === HTML5::ENDTAG && $token['name'] === 'caption') { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. (innerHTML case) */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore - - /* Otherwise: */ - } else { - /* Generate implied end tags. */ - $this->generateImpliedEndTags(); - - /* Now, if the current node is not a caption element, then this - is a parse error. */ - // w/e - - /* Pop elements from this stack until a caption element has - been popped from the stack. */ - while(true) { - $node = end($this->stack)->nodeName; - array_pop($this->stack); - - if($node === 'caption') { - break; - } - } - - /* Clear the list of active formatting elements up to the last - marker. */ - $this->clearTheActiveFormattingElementsUpToTheLastMarker(); - - /* Switch the insertion mode to "in table". */ - $this->mode = self::IN_TABLE; - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "td", "tfoot", "th", "thead", "tr", or an end tag whose tag - name is "table" */ - } elseif(($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', - 'thead', 'tr'))) || ($token['type'] === HTML5::ENDTAG && - $token['name'] === 'table')) { - /* Parse error. Act as if an end tag with the tag name "caption" - had been seen, then, if that token wasn't ignored, reprocess the - current token. */ - $this->inCaption(array( - 'name' => 'caption', - 'type' => HTML5::ENDTAG - )); - - return $this->inTable($token); - - /* An end tag whose tag name is one of: "body", "col", "colgroup", - "html", "tbody", "td", "tfoot", "th", "thead", "tr" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('body', 'col', 'colgroup', 'html', 'tbody', 'tfoot', 'th', - 'thead', 'tr'))) { - // Parse error. Ignore the token. - - /* Anything else */ - } else { - /* Process the token as if the insertion mode was "in body". */ - $this->inBody($token); - } - } - - private function inColumnGroup($token) { - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $text = $this->dom->createTextNode($token['data']); - end($this->stack)->appendChild($text); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - end($this->stack)->appendChild($comment); - - /* A start tag whose tag name is "col" */ - } elseif($token['type'] === HTML5::STARTTAG && $token['name'] === 'col') { - /* Insert a col element for the token. Immediately pop the current - node off the stack of open elements. */ - $this->insertElement($token); - array_pop($this->stack); - - /* An end tag whose tag name is "colgroup" */ - } elseif($token['type'] === HTML5::ENDTAG && - $token['name'] === 'colgroup') { - /* If the current node is the root html element, then this is a - parse error, ignore the token. (innerHTML case) */ - if(end($this->stack)->nodeName === 'html') { - // Ignore - - /* Otherwise, pop the current node (which will be a colgroup - element) from the stack of open elements. Switch the insertion - mode to "in table". */ - } else { - array_pop($this->stack); - $this->mode = self::IN_TABLE; - } - - /* An end tag whose tag name is "col" */ - } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'col') { - /* Parse error. Ignore the token. */ - - /* Anything else */ - } else { - /* Act as if an end tag with the tag name "colgroup" had been seen, - and then, if that token wasn't ignored, reprocess the current token. */ - $this->inColumnGroup(array( - 'name' => 'colgroup', - 'type' => HTML5::ENDTAG - )); - - return $this->inTable($token); - } - } - - private function inTableBody($token) { - $clear = array('tbody', 'tfoot', 'thead', 'html'); - - /* A start tag whose tag name is "tr" */ - if($token['type'] === HTML5::STARTTAG && $token['name'] === 'tr') { - /* Clear the stack back to a table body context. */ - $this->clearStackToTableContext($clear); - - /* Insert a tr element for the token, then switch the insertion - mode to "in row". */ - $this->insertElement($token); - $this->mode = self::IN_ROW; - - /* A start tag whose tag name is one of: "th", "td" */ - } elseif($token['type'] === HTML5::STARTTAG && - ($token['name'] === 'th' || $token['name'] === 'td')) { - /* Parse error. Act as if a start tag with the tag name "tr" had - been seen, then reprocess the current token. */ - $this->inTableBody(array( - 'name' => 'tr', - 'type' => HTML5::STARTTAG, - 'attr' => array() - )); - - return $this->inRow($token); - - /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ - } elseif($token['type'] === HTML5::ENDTAG && - in_array($token['name'], array('tbody', 'tfoot', 'thead'))) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore - - /* Otherwise: */ - } else { - /* Clear the stack back to a table body context. */ - $this->clearStackToTableContext($clear); - - /* Pop the current node from the stack of open elements. Switch - the insertion mode to "in table". */ - array_pop($this->stack); - $this->mode = self::IN_TABLE; - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "tfoot", "thead", or an end tag whose tag name is "table" */ - } elseif(($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('caption', 'col', 'colgroup', 'tbody', 'tfoor', 'thead'))) || - ($token['type'] === HTML5::STARTTAG && $token['name'] === 'table')) { - /* If the stack of open elements does not have a tbody, thead, or - tfoot element in table scope, this is a parse error. Ignore the - token. (innerHTML case) */ - if(!$this->elementInScope(array('tbody', 'thead', 'tfoot'), true)) { - // Ignore. - - /* Otherwise: */ - } else { - /* Clear the stack back to a table body context. */ - $this->clearStackToTableContext($clear); - - /* Act as if an end tag with the same tag name as the current - node ("tbody", "tfoot", or "thead") had been seen, then - reprocess the current token. */ - $this->inTableBody(array( - 'name' => end($this->stack)->nodeName, - 'type' => HTML5::ENDTAG - )); - - return $this->mainPhase($token); - } - - /* An end tag whose tag name is one of: "body", "caption", "col", - "colgroup", "html", "td", "th", "tr" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) { - /* Parse error. Ignore the token. */ - - /* Anything else */ - } else { - /* Process the token as if the insertion mode was "in table". */ - $this->inTable($token); - } - } - - private function inRow($token) { - $clear = array('tr', 'html'); - - /* A start tag whose tag name is one of: "th", "td" */ - if($token['type'] === HTML5::STARTTAG && - ($token['name'] === 'th' || $token['name'] === 'td')) { - /* Clear the stack back to a table row context. */ - $this->clearStackToTableContext($clear); - - /* Insert an HTML element for the token, then switch the insertion - mode to "in cell". */ - $this->insertElement($token); - $this->mode = self::IN_CELL; - - /* Insert a marker at the end of the list of active formatting - elements. */ - $this->a_formatting[] = self::MARKER; - - /* An end tag whose tag name is "tr" */ - } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'tr') { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. (innerHTML case) */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore. - - /* Otherwise: */ - } else { - /* Clear the stack back to a table row context. */ - $this->clearStackToTableContext($clear); - - /* Pop the current node (which will be a tr element) from the - stack of open elements. Switch the insertion mode to "in table - body". */ - array_pop($this->stack); - $this->mode = self::IN_TBODY; - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "tfoot", "thead", "tr" or an end tag whose tag name is "table" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('caption', 'col', 'colgroup', 'tbody', 'tfoot', 'thead', 'tr'))) { - /* Act as if an end tag with the tag name "tr" had been seen, then, - if that token wasn't ignored, reprocess the current token. */ - $this->inRow(array( - 'name' => 'tr', - 'type' => HTML5::ENDTAG - )); - - return $this->inCell($token); - - /* An end tag whose tag name is one of: "tbody", "tfoot", "thead" */ - } elseif($token['type'] === HTML5::ENDTAG && - in_array($token['name'], array('tbody', 'tfoot', 'thead'))) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore. - - /* Otherwise: */ - } else { - /* Otherwise, act as if an end tag with the tag name "tr" had - been seen, then reprocess the current token. */ - $this->inRow(array( - 'name' => 'tr', - 'type' => HTML5::ENDTAG - )); - - return $this->inCell($token); - } - - /* An end tag whose tag name is one of: "body", "caption", "col", - "colgroup", "html", "td", "th" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('body', 'caption', 'col', 'colgroup', 'html', 'td', 'th', 'tr'))) { - /* Parse error. Ignore the token. */ - - /* Anything else */ - } else { - /* Process the token as if the insertion mode was "in table". */ - $this->inTable($token); - } - } - - private function inCell($token) { - /* An end tag whose tag name is one of: "td", "th" */ - if($token['type'] === HTML5::ENDTAG && - ($token['name'] === 'td' || $token['name'] === 'th')) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as that of the token, then this is a - parse error and the token must be ignored. */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore. - - /* Otherwise: */ - } else { - /* Generate implied end tags, except for elements with the same - tag name as the token. */ - $this->generateImpliedEndTags(array($token['name'])); - - /* Now, if the current node is not an element with the same tag - name as the token, then this is a parse error. */ - // k - - /* Pop elements from this stack until an element with the same - tag name as the token has been popped from the stack. */ - while(true) { - $node = end($this->stack)->nodeName; - array_pop($this->stack); - - if($node === $token['name']) { - break; - } - } - - /* Clear the list of active formatting elements up to the last - marker. */ - $this->clearTheActiveFormattingElementsUpToTheLastMarker(); - - /* Switch the insertion mode to "in row". (The current node - will be a tr element at this point.) */ - $this->mode = self::IN_ROW; - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "td", "tfoot", "th", "thead", "tr" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', - 'thead', 'tr'))) { - /* If the stack of open elements does not have a td or th element - in table scope, then this is a parse error; ignore the token. - (innerHTML case) */ - if(!$this->elementInScope(array('td', 'th'), true)) { - // Ignore. - - /* Otherwise, close the cell (see below) and reprocess the current - token. */ - } else { - $this->closeCell(); - return $this->inRow($token); - } - - /* A start tag whose tag name is one of: "caption", "col", "colgroup", - "tbody", "td", "tfoot", "th", "thead", "tr" */ - } elseif($token['type'] === HTML5::STARTTAG && in_array($token['name'], - array('caption', 'col', 'colgroup', 'tbody', 'td', 'tfoot', 'th', - 'thead', 'tr'))) { - /* If the stack of open elements does not have a td or th element - in table scope, then this is a parse error; ignore the token. - (innerHTML case) */ - if(!$this->elementInScope(array('td', 'th'), true)) { - // Ignore. - - /* Otherwise, close the cell (see below) and reprocess the current - token. */ - } else { - $this->closeCell(); - return $this->inRow($token); - } - - /* An end tag whose tag name is one of: "body", "caption", "col", - "colgroup", "html" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('body', 'caption', 'col', 'colgroup', 'html'))) { - /* Parse error. Ignore the token. */ - - /* An end tag whose tag name is one of: "table", "tbody", "tfoot", - "thead", "tr" */ - } elseif($token['type'] === HTML5::ENDTAG && in_array($token['name'], - array('table', 'tbody', 'tfoot', 'thead', 'tr'))) { - /* If the stack of open elements does not have an element in table - scope with the same tag name as that of the token (which can only - happen for "tbody", "tfoot" and "thead", or, in the innerHTML case), - then this is a parse error and the token must be ignored. */ - if(!$this->elementInScope($token['name'], true)) { - // Ignore. - - /* Otherwise, close the cell (see below) and reprocess the current - token. */ - } else { - $this->closeCell(); - return $this->inRow($token); - } - - /* Anything else */ - } else { - /* Process the token as if the insertion mode was "in body". */ - $this->inBody($token); - } - } - - private function inSelect($token) { - /* Handle the token as follows: */ - - /* A character token */ - if($token['type'] === HTML5::CHARACTR) { - /* Append the token's character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* A start tag token whose tag name is "option" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'option') { - /* If the current node is an option element, act as if an end tag - with the tag name "option" had been seen. */ - if(end($this->stack)->nodeName === 'option') { - $this->inSelect(array( - 'name' => 'option', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* A start tag token whose tag name is "optgroup" */ - } elseif($token['type'] === HTML5::STARTTAG && - $token['name'] === 'optgroup') { - /* If the current node is an option element, act as if an end tag - with the tag name "option" had been seen. */ - if(end($this->stack)->nodeName === 'option') { - $this->inSelect(array( - 'name' => 'option', - 'type' => HTML5::ENDTAG - )); - } - - /* If the current node is an optgroup element, act as if an end tag - with the tag name "optgroup" had been seen. */ - if(end($this->stack)->nodeName === 'optgroup') { - $this->inSelect(array( - 'name' => 'optgroup', - 'type' => HTML5::ENDTAG - )); - } - - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* An end tag token whose tag name is "optgroup" */ - } elseif($token['type'] === HTML5::ENDTAG && - $token['name'] === 'optgroup') { - /* First, if the current node is an option element, and the node - immediately before it in the stack of open elements is an optgroup - element, then act as if an end tag with the tag name "option" had - been seen. */ - $elements_in_stack = count($this->stack); - - if($this->stack[$elements_in_stack - 1]->nodeName === 'option' && - $this->stack[$elements_in_stack - 2]->nodeName === 'optgroup') { - $this->inSelect(array( - 'name' => 'option', - 'type' => HTML5::ENDTAG - )); - } - - /* If the current node is an optgroup element, then pop that node - from the stack of open elements. Otherwise, this is a parse error, - ignore the token. */ - if($this->stack[$elements_in_stack - 1] === 'optgroup') { - array_pop($this->stack); - } - - /* An end tag token whose tag name is "option" */ - } elseif($token['type'] === HTML5::ENDTAG && - $token['name'] === 'option') { - /* If the current node is an option element, then pop that node - from the stack of open elements. Otherwise, this is a parse error, - ignore the token. */ - if(end($this->stack)->nodeName === 'option') { - array_pop($this->stack); - } - - /* An end tag whose tag name is "select" */ - } elseif($token['type'] === HTML5::ENDTAG && - $token['name'] === 'select') { - /* If the stack of open elements does not have an element in table - scope with the same tag name as the token, this is a parse error. - Ignore the token. (innerHTML case) */ - if(!$this->elementInScope($token['name'], true)) { - // w/e - - /* Otherwise: */ - } else { - /* Pop elements from the stack of open elements until a select - element has been popped from the stack. */ - while(true) { - $current = end($this->stack)->nodeName; - array_pop($this->stack); - - if($current === 'select') { - break; - } - } - - /* Reset the insertion mode appropriately. */ - $this->resetInsertionMode(); - } - - /* A start tag whose tag name is "select" */ - } elseif($token['name'] === 'select' && - $token['type'] === HTML5::STARTTAG) { - /* Parse error. Act as if the token had been an end tag with the - tag name "select" instead. */ - $this->inSelect(array( - 'name' => 'select', - 'type' => HTML5::ENDTAG - )); - - /* An end tag whose tag name is one of: "caption", "table", "tbody", - "tfoot", "thead", "tr", "td", "th" */ - } elseif(in_array($token['name'], array('caption', 'table', 'tbody', - 'tfoot', 'thead', 'tr', 'td', 'th')) && $token['type'] === HTML5::ENDTAG) { - /* Parse error. */ - // w/e - - /* If the stack of open elements has an element in table scope with - the same tag name as that of the token, then act as if an end tag - with the tag name "select" had been seen, and reprocess the token. - Otherwise, ignore the token. */ - if($this->elementInScope($token['name'], true)) { - $this->inSelect(array( - 'name' => 'select', - 'type' => HTML5::ENDTAG - )); - - $this->mainPhase($token); - } - - /* Anything else */ - } else { - /* Parse error. Ignore the token. */ - } - } - - private function afterBody($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Process the token as it would be processed if the insertion mode - was "in body". */ - $this->inBody($token); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the first element in the stack of open - elements (the html element), with the data attribute set to the - data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - $this->stack[0]->appendChild($comment); - - /* An end tag with the tag name "html" */ - } elseif($token['type'] === HTML5::ENDTAG && $token['name'] === 'html') { - /* If the parser was originally created in order to handle the - setting of an element's innerHTML attribute, this is a parse error; - ignore the token. (The element will be an html element in this - case.) (innerHTML case) */ - - /* Otherwise, switch to the trailing end phase. */ - $this->phase = self::END_PHASE; - - /* Anything else */ - } else { - /* Parse error. Set the insertion mode to "in body" and reprocess - the token. */ - $this->mode = self::IN_BODY; - return $this->inBody($token); - } - } - - private function inFrameset($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* A start tag with the tag name "frameset" */ - } elseif($token['name'] === 'frameset' && - $token['type'] === HTML5::STARTTAG) { - $this->insertElement($token); - - /* An end tag with the tag name "frameset" */ - } elseif($token['name'] === 'frameset' && - $token['type'] === HTML5::ENDTAG) { - /* If the current node is the root html element, then this is a - parse error; ignore the token. (innerHTML case) */ - if(end($this->stack)->nodeName === 'html') { - // Ignore - - } else { - /* Otherwise, pop the current node from the stack of open - elements. */ - array_pop($this->stack); - - /* If the parser was not originally created in order to handle - the setting of an element's innerHTML attribute (innerHTML case), - and the current node is no longer a frameset element, then change - the insertion mode to "after frameset". */ - $this->mode = self::AFTR_FRAME; - } - - /* A start tag with the tag name "frame" */ - } elseif($token['name'] === 'frame' && - $token['type'] === HTML5::STARTTAG) { - /* Insert an HTML element for the token. */ - $this->insertElement($token); - - /* Immediately pop the current node off the stack of open elements. */ - array_pop($this->stack); - - /* A start tag with the tag name "noframes" */ - } elseif($token['name'] === 'noframes' && - $token['type'] === HTML5::STARTTAG) { - /* Process the token as if the insertion mode had been "in body". */ - $this->inBody($token); - - /* Anything else */ - } else { - /* Parse error. Ignore the token. */ - } - } - - private function afterFrameset($token) { - /* Handle the token as follows: */ - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - U+000D CARRIAGE RETURN (CR), or U+0020 SPACE */ - if($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Append the character to the current node. */ - $this->insertText($token['data']); - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the current node with the data - attribute set to the data given in the comment token. */ - $this->insertComment($token['data']); - - /* An end tag with the tag name "html" */ - } elseif($token['name'] === 'html' && - $token['type'] === HTML5::ENDTAG) { - /* Switch to the trailing end phase. */ - $this->phase = self::END_PHASE; - - /* A start tag with the tag name "noframes" */ - } elseif($token['name'] === 'noframes' && - $token['type'] === HTML5::STARTTAG) { - /* Process the token as if the insertion mode had been "in body". */ - $this->inBody($token); - - /* Anything else */ - } else { - /* Parse error. Ignore the token. */ - } - } - - private function trailingEndPhase($token) { - /* After the main phase, as each token is emitted from the tokenisation - stage, it must be processed as described in this section. */ - - /* A DOCTYPE token */ - if($token['type'] === HTML5::DOCTYPE) { - // Parse error. Ignore the token. - - /* A comment token */ - } elseif($token['type'] === HTML5::COMMENT) { - /* Append a Comment node to the Document object with the data - attribute set to the data given in the comment token. */ - $comment = $this->dom->createComment($token['data']); - $this->dom->appendChild($comment); - - /* A character token that is one of one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE */ - } elseif($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) { - /* Process the token as it would be processed in the main phase. */ - $this->mainPhase($token); - - /* A character token that is not one of U+0009 CHARACTER TABULATION, - U+000A LINE FEED (LF), U+000B LINE TABULATION, U+000C FORM FEED (FF), - or U+0020 SPACE. Or a start tag token. Or an end tag token. */ - } elseif(($token['type'] === HTML5::CHARACTR && - preg_match('/^[\t\n\x0b\x0c ]+$/', $token['data'])) || - $token['type'] === HTML5::STARTTAG || $token['type'] === HTML5::ENDTAG) { - /* Parse error. Switch back to the main phase and reprocess the - token. */ - $this->phase = self::MAIN_PHASE; - return $this->mainPhase($token); - - /* An end-of-file token */ - } elseif($token['type'] === HTML5::EOF) { - /* OMG DONE!! */ - } - } - - private function insertElement($token, $append = true, $check = false) { - // Proprietary workaround for libxml2's limitations with tag names - if ($check) { - // Slightly modified HTML5 tag-name modification, - // removing anything that's not an ASCII letter, digit, or hyphen - $token['name'] = preg_replace('/[^a-z0-9-]/i', '', $token['name']); - // Remove leading hyphens and numbers - $token['name'] = ltrim($token['name'], '-0..9'); - // In theory, this should ever be needed, but just in case - if ($token['name'] === '') $token['name'] = 'span'; // arbitrary generic choice - } - - $el = $this->dom->createElement($token['name']); - - foreach($token['attr'] as $attr) { - if(!$el->hasAttribute($attr['name'])) { - $el->setAttribute($attr['name'], $attr['value']); - } - } - - $this->appendToRealParent($el); - $this->stack[] = $el; - - return $el; - } - - private function insertText($data) { - $text = $this->dom->createTextNode($data); - $this->appendToRealParent($text); - } - - private function insertComment($data) { - $comment = $this->dom->createComment($data); - $this->appendToRealParent($comment); - } - - private function appendToRealParent($node) { - if($this->foster_parent === null) { - end($this->stack)->appendChild($node); - - } elseif($this->foster_parent !== null) { - /* If the foster parent element is the parent element of the - last table element in the stack of open elements, then the new - node must be inserted immediately before the last table element - in the stack of open elements in the foster parent element; - otherwise, the new node must be appended to the foster parent - element. */ - for($n = count($this->stack) - 1; $n >= 0; $n--) { - if($this->stack[$n]->nodeName === 'table' && - $this->stack[$n]->parentNode !== null) { - $table = $this->stack[$n]; - break; - } - } - - if(isset($table) && $this->foster_parent->isSameNode($table->parentNode)) - $this->foster_parent->insertBefore($node, $table); - else - $this->foster_parent->appendChild($node); - - $this->foster_parent = null; - } - } - - private function elementInScope($el, $table = false) { - if(is_array($el)) { - foreach($el as $element) { - if($this->elementInScope($element, $table)) { - return true; - } - } - - return false; - } - - $leng = count($this->stack); - - for($n = 0; $n < $leng; $n++) { - /* 1. Initialise node to be the current node (the bottommost node of - the stack). */ - $node = $this->stack[$leng - 1 - $n]; - - if($node->tagName === $el) { - /* 2. If node is the target node, terminate in a match state. */ - return true; - - } elseif($node->tagName === 'table') { - /* 3. Otherwise, if node is a table element, terminate in a failure - state. */ - return false; - - } elseif($table === true && in_array($node->tagName, array('caption', 'td', - 'th', 'button', 'marquee', 'object'))) { - /* 4. Otherwise, if the algorithm is the "has an element in scope" - variant (rather than the "has an element in table scope" variant), - and node is one of the following, terminate in a failure state. */ - return false; - - } elseif($node === $node->ownerDocument->documentElement) { - /* 5. Otherwise, if node is an html element (root element), terminate - in a failure state. (This can only happen if the node is the topmost - node of the stack of open elements, and prevents the next step from - being invoked if there are no more elements in the stack.) */ - return false; - } - - /* Otherwise, set node to the previous entry in the stack of open - elements and return to step 2. (This will never fail, since the loop - will always terminate in the previous step if the top of the stack - is reached.) */ - } - } - - private function reconstructActiveFormattingElements() { - /* 1. If there are no entries in the list of active formatting elements, - then there is nothing to reconstruct; stop this algorithm. */ - $formatting_elements = count($this->a_formatting); - - if($formatting_elements === 0) { - return false; - } - - /* 3. Let entry be the last (most recently added) element in the list - of active formatting elements. */ - $entry = end($this->a_formatting); - - /* 2. If the last (most recently added) entry in the list of active - formatting elements is a marker, or if it is an element that is in the - stack of open elements, then there is nothing to reconstruct; stop this - algorithm. */ - if($entry === self::MARKER || in_array($entry, $this->stack, true)) { - return false; - } - - for($a = $formatting_elements - 1; $a >= 0; true) { - /* 4. If there are no entries before entry in the list of active - formatting elements, then jump to step 8. */ - if($a === 0) { - $step_seven = false; - break; - } - - /* 5. Let entry be the entry one earlier than entry in the list of - active formatting elements. */ - $a--; - $entry = $this->a_formatting[$a]; - - /* 6. If entry is neither a marker nor an element that is also in - thetack of open elements, go to step 4. */ - if($entry === self::MARKER || in_array($entry, $this->stack, true)) { - break; - } - } - - while(true) { - /* 7. Let entry be the element one later than entry in the list of - active formatting elements. */ - if(isset($step_seven) && $step_seven === true) { - $a++; - $entry = $this->a_formatting[$a]; - } - - /* 8. Perform a shallow clone of the element entry to obtain clone. */ - $clone = $entry->cloneNode(); - - /* 9. Append clone to the current node and push it onto the stack - of open elements so that it is the new current node. */ - end($this->stack)->appendChild($clone); - $this->stack[] = $clone; - - /* 10. Replace the entry for entry in the list with an entry for - clone. */ - $this->a_formatting[$a] = $clone; - - /* 11. If the entry for clone in the list of active formatting - elements is not the last entry in the list, return to step 7. */ - if(end($this->a_formatting) !== $clone) { - $step_seven = true; - } else { - break; - } - } - } - - private function clearTheActiveFormattingElementsUpToTheLastMarker() { - /* When the steps below require the UA to clear the list of active - formatting elements up to the last marker, the UA must perform the - following steps: */ - - while(true) { - /* 1. Let entry be the last (most recently added) entry in the list - of active formatting elements. */ - $entry = end($this->a_formatting); - - /* 2. Remove entry from the list of active formatting elements. */ - array_pop($this->a_formatting); - - /* 3. If entry was a marker, then stop the algorithm at this point. - The list has been cleared up to the last marker. */ - if($entry === self::MARKER) { - break; - } - } - } - - private function generateImpliedEndTags($exclude = array()) { - /* When the steps below require the UA to generate implied end tags, - then, if the current node is a dd element, a dt element, an li element, - a p element, a td element, a th element, or a tr element, the UA must - act as if an end tag with the respective tag name had been seen and - then generate implied end tags again. */ - $node = end($this->stack); - $elements = array_diff(array('dd', 'dt', 'li', 'p', 'td', 'th', 'tr'), $exclude); - - while(in_array(end($this->stack)->nodeName, $elements)) { - array_pop($this->stack); - } - } - - private function getElementCategory($node) { - $name = $node->tagName; - if(in_array($name, $this->special)) - return self::SPECIAL; - - elseif(in_array($name, $this->scoping)) - return self::SCOPING; - - elseif(in_array($name, $this->formatting)) - return self::FORMATTING; - - else - return self::PHRASING; - } - - private function clearStackToTableContext($elements) { - /* When the steps above require the UA to clear the stack back to a - table context, it means that the UA must, while the current node is not - a table element or an html element, pop elements from the stack of open - elements. If this causes any elements to be popped from the stack, then - this is a parse error. */ - while(true) { - $node = end($this->stack)->nodeName; - - if(in_array($node, $elements)) { - break; - } else { - array_pop($this->stack); - } - } - } - - private function resetInsertionMode() { - /* 1. Let last be false. */ - $last = false; - $leng = count($this->stack); - - for($n = $leng - 1; $n >= 0; $n--) { - /* 2. Let node be the last node in the stack of open elements. */ - $node = $this->stack[$n]; - - /* 3. If node is the first node in the stack of open elements, then - set last to true. If the element whose innerHTML attribute is being - set is neither a td element nor a th element, then set node to the - element whose innerHTML attribute is being set. (innerHTML case) */ - if($this->stack[0]->isSameNode($node)) { - $last = true; - } - - /* 4. If node is a select element, then switch the insertion mode to - "in select" and abort these steps. (innerHTML case) */ - if($node->nodeName === 'select') { - $this->mode = self::IN_SELECT; - break; - - /* 5. If node is a td or th element, then switch the insertion mode - to "in cell" and abort these steps. */ - } elseif($node->nodeName === 'td' || $node->nodeName === 'th') { - $this->mode = self::IN_CELL; - break; - - /* 6. If node is a tr element, then switch the insertion mode to - "in row" and abort these steps. */ - } elseif($node->nodeName === 'tr') { - $this->mode = self::IN_ROW; - break; - - /* 7. If node is a tbody, thead, or tfoot element, then switch the - insertion mode to "in table body" and abort these steps. */ - } elseif(in_array($node->nodeName, array('tbody', 'thead', 'tfoot'))) { - $this->mode = self::IN_TBODY; - break; - - /* 8. If node is a caption element, then switch the insertion mode - to "in caption" and abort these steps. */ - } elseif($node->nodeName === 'caption') { - $this->mode = self::IN_CAPTION; - break; - - /* 9. If node is a colgroup element, then switch the insertion mode - to "in column group" and abort these steps. (innerHTML case) */ - } elseif($node->nodeName === 'colgroup') { - $this->mode = self::IN_CGROUP; - break; - - /* 10. If node is a table element, then switch the insertion mode - to "in table" and abort these steps. */ - } elseif($node->nodeName === 'table') { - $this->mode = self::IN_TABLE; - break; - - /* 11. If node is a head element, then switch the insertion mode - to "in body" ("in body"! not "in head"!) and abort these steps. - (innerHTML case) */ - } elseif($node->nodeName === 'head') { - $this->mode = self::IN_BODY; - break; - - /* 12. If node is a body element, then switch the insertion mode to - "in body" and abort these steps. */ - } elseif($node->nodeName === 'body') { - $this->mode = self::IN_BODY; - break; - - /* 13. If node is a frameset element, then switch the insertion - mode to "in frameset" and abort these steps. (innerHTML case) */ - } elseif($node->nodeName === 'frameset') { - $this->mode = self::IN_FRAME; - break; - - /* 14. If node is an html element, then: if the head element - pointer is null, switch the insertion mode to "before head", - otherwise, switch the insertion mode to "after head". In either - case, abort these steps. (innerHTML case) */ - } elseif($node->nodeName === 'html') { - $this->mode = ($this->head_pointer === null) - ? self::BEFOR_HEAD - : self::AFTER_HEAD; - - break; - - /* 15. If last is true, then set the insertion mode to "in body" - and abort these steps. (innerHTML case) */ - } elseif($last) { - $this->mode = self::IN_BODY; - break; - } - } - } - - private function closeCell() { - /* If the stack of open elements has a td or th element in table scope, - then act as if an end tag token with that tag name had been seen. */ - foreach(array('td', 'th') as $cell) { - if($this->elementInScope($cell, true)) { - $this->inCell(array( - 'name' => $cell, - 'type' => HTML5::ENDTAG - )); - - break; - } - } - } - - public function save() { - return $this->dom; - } -} -?> diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/PercentEncoder.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/PercentEncoder.php deleted file mode 100644 index a43c44f4c..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/PercentEncoder.php +++ /dev/null @@ -1,98 +0,0 @@ -preserve[$i] = true; // digits - for ($i = 65; $i <= 90; $i++) $this->preserve[$i] = true; // upper-case - for ($i = 97; $i <= 122; $i++) $this->preserve[$i] = true; // lower-case - $this->preserve[45] = true; // Dash - - $this->preserve[46] = true; // Period . - $this->preserve[95] = true; // Underscore _ - $this->preserve[126]= true; // Tilde ~ - - // extra letters not to escape - if ($preserve !== false) { - for ($i = 0, $c = strlen($preserve); $i < $c; $i++) { - $this->preserve[ord($preserve[$i])] = true; - } - } - } - - /** - * Our replacement for urlencode, it encodes all non-reserved characters, - * as well as any extra characters that were instructed to be preserved. - * @note - * Assumes that the string has already been normalized, making any - * and all percent escape sequences valid. Percents will not be - * re-escaped, regardless of their status in $preserve - * @param $string String to be encoded - * @return Encoded string. - */ - public function encode($string) { - $ret = ''; - for ($i = 0, $c = strlen($string); $i < $c; $i++) { - if ($string[$i] !== '%' && !isset($this->preserve[$int = ord($string[$i])]) ) { - $ret .= '%' . sprintf('%02X', $int); - } else { - $ret .= $string[$i]; - } - } - return $ret; - } - - /** - * Fix up percent-encoding by decoding unreserved characters and normalizing. - * @warning This function is affected by $preserve, even though the - * usual desired behavior is for this not to preserve those - * characters. Be careful when reusing instances of PercentEncoder! - * @param $string String to normalize - */ - public function normalize($string) { - if ($string == '') return ''; - $parts = explode('%', $string); - $ret = array_shift($parts); - foreach ($parts as $part) { - $length = strlen($part); - if ($length < 2) { - $ret .= '%25' . $part; - continue; - } - $encoding = substr($part, 0, 2); - $text = substr($part, 2); - if (!ctype_xdigit($encoding)) { - $ret .= '%25' . $part; - continue; - } - $int = hexdec($encoding); - if (isset($this->preserve[$int])) { - $ret .= chr($int) . $text; - continue; - } - $encoding = strtoupper($encoding); - $ret .= '%' . $encoding . $text; - } - return $ret; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer.php deleted file mode 100644 index e7eb82e83..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer.php +++ /dev/null @@ -1,176 +0,0 @@ -getAll(); - $context = new HTMLPurifier_Context(); - $this->generator = new HTMLPurifier_Generator($config, $context); - } - - /** - * Main function that renders object or aspect of that object - * @note Parameters vary depending on printer - */ - // function render() {} - - /** - * Returns a start tag - * @param $tag Tag name - * @param $attr Attribute array - */ - protected function start($tag, $attr = array()) { - return $this->generator->generateFromToken( - new HTMLPurifier_Token_Start($tag, $attr ? $attr : array()) - ); - } - - /** - * Returns an end teg - * @param $tag Tag name - */ - protected function end($tag) { - return $this->generator->generateFromToken( - new HTMLPurifier_Token_End($tag) - ); - } - - /** - * Prints a complete element with content inside - * @param $tag Tag name - * @param $contents Element contents - * @param $attr Tag attributes - * @param $escape Bool whether or not to escape contents - */ - protected function element($tag, $contents, $attr = array(), $escape = true) { - return $this->start($tag, $attr) . - ($escape ? $this->escape($contents) : $contents) . - $this->end($tag); - } - - protected function elementEmpty($tag, $attr = array()) { - return $this->generator->generateFromToken( - new HTMLPurifier_Token_Empty($tag, $attr) - ); - } - - protected function text($text) { - return $this->generator->generateFromToken( - new HTMLPurifier_Token_Text($text) - ); - } - - /** - * Prints a simple key/value row in a table. - * @param $name Key - * @param $value Value - */ - protected function row($name, $value) { - if (is_bool($value)) $value = $value ? 'On' : 'Off'; - return - $this->start('tr') . "\n" . - $this->element('th', $name) . "\n" . - $this->element('td', $value) . "\n" . - $this->end('tr') - ; - } - - /** - * Escapes a string for HTML output. - * @param $string String to escape - */ - protected function escape($string) { - $string = HTMLPurifier_Encoder::cleanUTF8($string); - $string = htmlspecialchars($string, ENT_COMPAT, 'UTF-8'); - return $string; - } - - /** - * Takes a list of strings and turns them into a single list - * @param $array List of strings - * @param $polite Bool whether or not to add an end before the last - */ - protected function listify($array, $polite = false) { - if (empty($array)) return 'None'; - $ret = ''; - $i = count($array); - foreach ($array as $value) { - $i--; - $ret .= $value; - if ($i > 0 && !($polite && $i == 1)) $ret .= ', '; - if ($polite && $i == 1) $ret .= 'and '; - } - return $ret; - } - - /** - * Retrieves the class of an object without prefixes, as well as metadata - * @param $obj Object to determine class of - * @param $prefix Further prefix to remove - */ - protected function getClass($obj, $sec_prefix = '') { - static $five = null; - if ($five === null) $five = version_compare(PHP_VERSION, '5', '>='); - $prefix = 'HTMLPurifier_' . $sec_prefix; - if (!$five) $prefix = strtolower($prefix); - $class = str_replace($prefix, '', get_class($obj)); - $lclass = strtolower($class); - $class .= '('; - switch ($lclass) { - case 'enum': - $values = array(); - foreach ($obj->valid_values as $value => $bool) { - $values[] = $value; - } - $class .= implode(', ', $values); - break; - case 'css_composite': - $values = array(); - foreach ($obj->defs as $def) { - $values[] = $this->getClass($def, $sec_prefix); - } - $class .= implode(', ', $values); - break; - case 'css_multiple': - $class .= $this->getClass($obj->single, $sec_prefix) . ', '; - $class .= $obj->max; - break; - case 'css_denyelementdecorator': - $class .= $this->getClass($obj->def, $sec_prefix) . ', '; - $class .= $obj->element; - break; - case 'css_importantdecorator': - $class .= $this->getClass($obj->def, $sec_prefix); - if ($obj->allow) $class .= ', !important'; - break; - } - $class .= ')'; - return $class; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer/CSSDefinition.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer/CSSDefinition.php deleted file mode 100644 index 81f986590..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer/CSSDefinition.php +++ /dev/null @@ -1,38 +0,0 @@ -def = $config->getCSSDefinition(); - $ret = ''; - - $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); - $ret .= $this->start('table'); - - $ret .= $this->element('caption', 'Properties ($info)'); - - $ret .= $this->start('thead'); - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Property', array('class' => 'heavy')); - $ret .= $this->element('th', 'Definition', array('class' => 'heavy', 'style' => 'width:auto;')); - $ret .= $this->end('tr'); - $ret .= $this->end('thead'); - - ksort($this->def->info); - foreach ($this->def->info as $property => $obj) { - $name = $this->getClass($obj, 'AttrDef_'); - $ret .= $this->row($property, $name); - } - - $ret .= $this->end('table'); - $ret .= $this->end('div'); - - return $ret; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer/ConfigForm.css b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer/ConfigForm.css deleted file mode 100644 index 3ff1a88aa..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer/ConfigForm.css +++ /dev/null @@ -1,10 +0,0 @@ - -.hp-config {} - -.hp-config tbody th {text-align:right; padding-right:0.5em;} -.hp-config thead, .hp-config .namespace {background:#3C578C; color:#FFF;} -.hp-config .namespace th {text-align:center;} -.hp-config .verbose {display:none;} -.hp-config .controls {text-align:center;} - -/* vim: et sw=4 sts=4 */ diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer/ConfigForm.js b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer/ConfigForm.js deleted file mode 100644 index cba00c9b8..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer/ConfigForm.js +++ /dev/null @@ -1,5 +0,0 @@ -function toggleWriteability(id_of_patient, checked) { - document.getElementById(id_of_patient).disabled = checked; -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php deleted file mode 100644 index 02aa65689..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer/ConfigForm.php +++ /dev/null @@ -1,368 +0,0 @@ -docURL = $doc_url; - $this->name = $name; - $this->compress = $compress; - // initialize sub-printers - $this->fields[0] = new HTMLPurifier_Printer_ConfigForm_default(); - $this->fields[HTMLPurifier_VarParser::BOOL] = new HTMLPurifier_Printer_ConfigForm_bool(); - } - - /** - * Sets default column and row size for textareas in sub-printers - * @param $cols Integer columns of textarea, null to use default - * @param $rows Integer rows of textarea, null to use default - */ - public function setTextareaDimensions($cols = null, $rows = null) { - if ($cols) $this->fields['default']->cols = $cols; - if ($rows) $this->fields['default']->rows = $rows; - } - - /** - * Retrieves styling, in case it is not accessible by webserver - */ - public static function getCSS() { - return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.css'); - } - - /** - * Retrieves JavaScript, in case it is not accessible by webserver - */ - public static function getJavaScript() { - return file_get_contents(HTMLPURIFIER_PREFIX . '/HTMLPurifier/Printer/ConfigForm.js'); - } - - /** - * Returns HTML output for a configuration form - * @param $config Configuration object of current form state, or an array - * where [0] has an HTML namespace and [1] is being rendered. - * @param $allowed Optional namespace(s) and directives to restrict form to. - */ - public function render($config, $allowed = true, $render_controls = true) { - if (is_array($config) && isset($config[0])) { - $gen_config = $config[0]; - $config = $config[1]; - } else { - $gen_config = $config; - } - - $this->config = $config; - $this->genConfig = $gen_config; - $this->prepareGenerator($gen_config); - - $allowed = HTMLPurifier_Config::getAllowedDirectivesForForm($allowed, $config->def); - $all = array(); - foreach ($allowed as $key) { - list($ns, $directive) = $key; - $all[$ns][$directive] = $config->get($ns .'.'. $directive); - } - - $ret = ''; - $ret .= $this->start('table', array('class' => 'hp-config')); - $ret .= $this->start('thead'); - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Directive', array('class' => 'hp-directive')); - $ret .= $this->element('th', 'Value', array('class' => 'hp-value')); - $ret .= $this->end('tr'); - $ret .= $this->end('thead'); - foreach ($all as $ns => $directives) { - $ret .= $this->renderNamespace($ns, $directives); - } - if ($render_controls) { - $ret .= $this->start('tbody'); - $ret .= $this->start('tr'); - $ret .= $this->start('td', array('colspan' => 2, 'class' => 'controls')); - $ret .= $this->elementEmpty('input', array('type' => 'submit', 'value' => 'Submit')); - $ret .= '[Reset]'; - $ret .= $this->end('td'); - $ret .= $this->end('tr'); - $ret .= $this->end('tbody'); - } - $ret .= $this->end('table'); - return $ret; - } - - /** - * Renders a single namespace - * @param $ns String namespace name - * @param $directive Associative array of directives to values - */ - protected function renderNamespace($ns, $directives) { - $ret = ''; - $ret .= $this->start('tbody', array('class' => 'namespace')); - $ret .= $this->start('tr'); - $ret .= $this->element('th', $ns, array('colspan' => 2)); - $ret .= $this->end('tr'); - $ret .= $this->end('tbody'); - $ret .= $this->start('tbody'); - foreach ($directives as $directive => $value) { - $ret .= $this->start('tr'); - $ret .= $this->start('th'); - if ($this->docURL) { - $url = str_replace('%s', urlencode("$ns.$directive"), $this->docURL); - $ret .= $this->start('a', array('href' => $url)); - } - $attr = array('for' => "{$this->name}:$ns.$directive"); - - // crop directive name if it's too long - if (!$this->compress || (strlen($directive) < $this->compress)) { - $directive_disp = $directive; - } else { - $directive_disp = substr($directive, 0, $this->compress - 2) . '...'; - $attr['title'] = $directive; - } - - $ret .= $this->element( - 'label', - $directive_disp, - // component printers must create an element with this id - $attr - ); - if ($this->docURL) $ret .= $this->end('a'); - $ret .= $this->end('th'); - - $ret .= $this->start('td'); - $def = $this->config->def->info["$ns.$directive"]; - if (is_int($def)) { - $allow_null = $def < 0; - $type = abs($def); - } else { - $type = $def->type; - $allow_null = isset($def->allow_null); - } - if (!isset($this->fields[$type])) $type = 0; // default - $type_obj = $this->fields[$type]; - if ($allow_null) { - $type_obj = new HTMLPurifier_Printer_ConfigForm_NullDecorator($type_obj); - } - $ret .= $type_obj->render($ns, $directive, $value, $this->name, array($this->genConfig, $this->config)); - $ret .= $this->end('td'); - $ret .= $this->end('tr'); - } - $ret .= $this->end('tbody'); - return $ret; - } - -} - -/** - * Printer decorator for directives that accept null - */ -class HTMLPurifier_Printer_ConfigForm_NullDecorator extends HTMLPurifier_Printer { - /** - * Printer being decorated - */ - protected $obj; - /** - * @param $obj Printer to decorate - */ - public function __construct($obj) { - parent::__construct(); - $this->obj = $obj; - } - public function render($ns, $directive, $value, $name, $config) { - if (is_array($config) && isset($config[0])) { - $gen_config = $config[0]; - $config = $config[1]; - } else { - $gen_config = $config; - } - $this->prepareGenerator($gen_config); - - $ret = ''; - $ret .= $this->start('label', array('for' => "$name:Null_$ns.$directive")); - $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); - $ret .= $this->text(' Null/Disabled'); - $ret .= $this->end('label'); - $attr = array( - 'type' => 'checkbox', - 'value' => '1', - 'class' => 'null-toggle', - 'name' => "$name"."[Null_$ns.$directive]", - 'id' => "$name:Null_$ns.$directive", - 'onclick' => "toggleWriteability('$name:$ns.$directive',checked)" // INLINE JAVASCRIPT!!!! - ); - if ($this->obj instanceof HTMLPurifier_Printer_ConfigForm_bool) { - // modify inline javascript slightly - $attr['onclick'] = "toggleWriteability('$name:Yes_$ns.$directive',checked);toggleWriteability('$name:No_$ns.$directive',checked)"; - } - if ($value === null) $attr['checked'] = 'checked'; - $ret .= $this->elementEmpty('input', $attr); - $ret .= $this->text(' or '); - $ret .= $this->elementEmpty('br'); - $ret .= $this->obj->render($ns, $directive, $value, $name, array($gen_config, $config)); - return $ret; - } -} - -/** - * Swiss-army knife configuration form field printer - */ -class HTMLPurifier_Printer_ConfigForm_default extends HTMLPurifier_Printer { - public $cols = 18; - public $rows = 5; - public function render($ns, $directive, $value, $name, $config) { - if (is_array($config) && isset($config[0])) { - $gen_config = $config[0]; - $config = $config[1]; - } else { - $gen_config = $config; - } - $this->prepareGenerator($gen_config); - // this should probably be split up a little - $ret = ''; - $def = $config->def->info["$ns.$directive"]; - if (is_int($def)) { - $type = abs($def); - } else { - $type = $def->type; - } - if (is_array($value)) { - switch ($type) { - case HTMLPurifier_VarParser::LOOKUP: - $array = $value; - $value = array(); - foreach ($array as $val => $b) { - $value[] = $val; - } - case HTMLPurifier_VarParser::ALIST: - $value = implode(PHP_EOL, $value); - break; - case HTMLPurifier_VarParser::HASH: - $nvalue = ''; - foreach ($value as $i => $v) { - $nvalue .= "$i:$v" . PHP_EOL; - } - $value = $nvalue; - break; - default: - $value = ''; - } - } - if ($type === HTMLPurifier_VarParser::MIXED) { - return 'Not supported'; - $value = serialize($value); - } - $attr = array( - 'name' => "$name"."[$ns.$directive]", - 'id' => "$name:$ns.$directive" - ); - if ($value === null) $attr['disabled'] = 'disabled'; - if (isset($def->allowed)) { - $ret .= $this->start('select', $attr); - foreach ($def->allowed as $val => $b) { - $attr = array(); - if ($value == $val) $attr['selected'] = 'selected'; - $ret .= $this->element('option', $val, $attr); - } - $ret .= $this->end('select'); - } elseif ( - $type === HTMLPurifier_VarParser::TEXT || - $type === HTMLPurifier_VarParser::ITEXT || - $type === HTMLPurifier_VarParser::ALIST || - $type === HTMLPurifier_VarParser::HASH || - $type === HTMLPurifier_VarParser::LOOKUP - ) { - $attr['cols'] = $this->cols; - $attr['rows'] = $this->rows; - $ret .= $this->start('textarea', $attr); - $ret .= $this->text($value); - $ret .= $this->end('textarea'); - } else { - $attr['value'] = $value; - $attr['type'] = 'text'; - $ret .= $this->elementEmpty('input', $attr); - } - return $ret; - } -} - -/** - * Bool form field printer - */ -class HTMLPurifier_Printer_ConfigForm_bool extends HTMLPurifier_Printer { - public function render($ns, $directive, $value, $name, $config) { - if (is_array($config) && isset($config[0])) { - $gen_config = $config[0]; - $config = $config[1]; - } else { - $gen_config = $config; - } - $this->prepareGenerator($gen_config); - $ret = ''; - $ret .= $this->start('div', array('id' => "$name:$ns.$directive")); - - $ret .= $this->start('label', array('for' => "$name:Yes_$ns.$directive")); - $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); - $ret .= $this->text(' Yes'); - $ret .= $this->end('label'); - - $attr = array( - 'type' => 'radio', - 'name' => "$name"."[$ns.$directive]", - 'id' => "$name:Yes_$ns.$directive", - 'value' => '1' - ); - if ($value === true) $attr['checked'] = 'checked'; - if ($value === null) $attr['disabled'] = 'disabled'; - $ret .= $this->elementEmpty('input', $attr); - - $ret .= $this->start('label', array('for' => "$name:No_$ns.$directive")); - $ret .= $this->element('span', "$ns.$directive:", array('class' => 'verbose')); - $ret .= $this->text(' No'); - $ret .= $this->end('label'); - - $attr = array( - 'type' => 'radio', - 'name' => "$name"."[$ns.$directive]", - 'id' => "$name:No_$ns.$directive", - 'value' => '0' - ); - if ($value === false) $attr['checked'] = 'checked'; - if ($value === null) $attr['disabled'] = 'disabled'; - $ret .= $this->elementEmpty('input', $attr); - - $ret .= $this->end('div'); - - return $ret; - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer/HTMLDefinition.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer/HTMLDefinition.php deleted file mode 100644 index 8a8f126b8..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Printer/HTMLDefinition.php +++ /dev/null @@ -1,272 +0,0 @@ -config =& $config; - - $this->def = $config->getHTMLDefinition(); - - $ret .= $this->start('div', array('class' => 'HTMLPurifier_Printer')); - - $ret .= $this->renderDoctype(); - $ret .= $this->renderEnvironment(); - $ret .= $this->renderContentSets(); - $ret .= $this->renderInfo(); - - $ret .= $this->end('div'); - - return $ret; - } - - /** - * Renders the Doctype table - */ - protected function renderDoctype() { - $doctype = $this->def->doctype; - $ret = ''; - $ret .= $this->start('table'); - $ret .= $this->element('caption', 'Doctype'); - $ret .= $this->row('Name', $doctype->name); - $ret .= $this->row('XML', $doctype->xml ? 'Yes' : 'No'); - $ret .= $this->row('Default Modules', implode($doctype->modules, ', ')); - $ret .= $this->row('Default Tidy Modules', implode($doctype->tidyModules, ', ')); - $ret .= $this->end('table'); - return $ret; - } - - - /** - * Renders environment table, which is miscellaneous info - */ - protected function renderEnvironment() { - $def = $this->def; - - $ret = ''; - - $ret .= $this->start('table'); - $ret .= $this->element('caption', 'Environment'); - - $ret .= $this->row('Parent of fragment', $def->info_parent); - $ret .= $this->renderChildren($def->info_parent_def->child); - $ret .= $this->row('Block wrap name', $def->info_block_wrapper); - - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Global attributes'); - $ret .= $this->element('td', $this->listifyAttr($def->info_global_attr),0,0); - $ret .= $this->end('tr'); - - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Tag transforms'); - $list = array(); - foreach ($def->info_tag_transform as $old => $new) { - $new = $this->getClass($new, 'TagTransform_'); - $list[] = "<$old> with $new"; - } - $ret .= $this->element('td', $this->listify($list)); - $ret .= $this->end('tr'); - - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Pre-AttrTransform'); - $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_pre)); - $ret .= $this->end('tr'); - - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Post-AttrTransform'); - $ret .= $this->element('td', $this->listifyObjectList($def->info_attr_transform_post)); - $ret .= $this->end('tr'); - - $ret .= $this->end('table'); - return $ret; - } - - /** - * Renders the Content Sets table - */ - protected function renderContentSets() { - $ret = ''; - $ret .= $this->start('table'); - $ret .= $this->element('caption', 'Content Sets'); - foreach ($this->def->info_content_sets as $name => $lookup) { - $ret .= $this->heavyHeader($name); - $ret .= $this->start('tr'); - $ret .= $this->element('td', $this->listifyTagLookup($lookup)); - $ret .= $this->end('tr'); - } - $ret .= $this->end('table'); - return $ret; - } - - /** - * Renders the Elements ($info) table - */ - protected function renderInfo() { - $ret = ''; - $ret .= $this->start('table'); - $ret .= $this->element('caption', 'Elements ($info)'); - ksort($this->def->info); - $ret .= $this->heavyHeader('Allowed tags', 2); - $ret .= $this->start('tr'); - $ret .= $this->element('td', $this->listifyTagLookup($this->def->info), array('colspan' => 2)); - $ret .= $this->end('tr'); - foreach ($this->def->info as $name => $def) { - $ret .= $this->start('tr'); - $ret .= $this->element('th', "<$name>", array('class'=>'heavy', 'colspan' => 2)); - $ret .= $this->end('tr'); - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Inline content'); - $ret .= $this->element('td', $def->descendants_are_inline ? 'Yes' : 'No'); - $ret .= $this->end('tr'); - if (!empty($def->excludes)) { - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Excludes'); - $ret .= $this->element('td', $this->listifyTagLookup($def->excludes)); - $ret .= $this->end('tr'); - } - if (!empty($def->attr_transform_pre)) { - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Pre-AttrTransform'); - $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_pre)); - $ret .= $this->end('tr'); - } - if (!empty($def->attr_transform_post)) { - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Post-AttrTransform'); - $ret .= $this->element('td', $this->listifyObjectList($def->attr_transform_post)); - $ret .= $this->end('tr'); - } - if (!empty($def->auto_close)) { - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Auto closed by'); - $ret .= $this->element('td', $this->listifyTagLookup($def->auto_close)); - $ret .= $this->end('tr'); - } - $ret .= $this->start('tr'); - $ret .= $this->element('th', 'Allowed attributes'); - $ret .= $this->element('td',$this->listifyAttr($def->attr), array(), 0); - $ret .= $this->end('tr'); - - if (!empty($def->required_attr)) { - $ret .= $this->row('Required attributes', $this->listify($def->required_attr)); - } - - $ret .= $this->renderChildren($def->child); - } - $ret .= $this->end('table'); - return $ret; - } - - /** - * Renders a row describing the allowed children of an element - * @param $def HTMLPurifier_ChildDef of pertinent element - */ - protected function renderChildren($def) { - $context = new HTMLPurifier_Context(); - $ret = ''; - $ret .= $this->start('tr'); - $elements = array(); - $attr = array(); - if (isset($def->elements)) { - if ($def->type == 'strictblockquote') { - $def->validateChildren(array(), $this->config, $context); - } - $elements = $def->elements; - } - if ($def->type == 'chameleon') { - $attr['rowspan'] = 2; - } elseif ($def->type == 'empty') { - $elements = array(); - } elseif ($def->type == 'table') { - $elements = array_flip(array('col', 'caption', 'colgroup', 'thead', - 'tfoot', 'tbody', 'tr')); - } - $ret .= $this->element('th', 'Allowed children', $attr); - - if ($def->type == 'chameleon') { - - $ret .= $this->element('td', - 'Block: ' . - $this->escape($this->listifyTagLookup($def->block->elements)),0,0); - $ret .= $this->end('tr'); - $ret .= $this->start('tr'); - $ret .= $this->element('td', - 'Inline: ' . - $this->escape($this->listifyTagLookup($def->inline->elements)),0,0); - - } elseif ($def->type == 'custom') { - - $ret .= $this->element('td', ''.ucfirst($def->type).': ' . - $def->dtd_regex); - - } else { - $ret .= $this->element('td', - ''.ucfirst($def->type).': ' . - $this->escape($this->listifyTagLookup($elements)),0,0); - } - $ret .= $this->end('tr'); - return $ret; - } - - /** - * Listifies a tag lookup table. - * @param $array Tag lookup array in form of array('tagname' => true) - */ - protected function listifyTagLookup($array) { - ksort($array); - $list = array(); - foreach ($array as $name => $discard) { - if ($name !== '#PCDATA' && !isset($this->def->info[$name])) continue; - $list[] = $name; - } - return $this->listify($list); - } - - /** - * Listifies a list of objects by retrieving class names and internal state - * @param $array List of objects - * @todo Also add information about internal state - */ - protected function listifyObjectList($array) { - ksort($array); - $list = array(); - foreach ($array as $discard => $obj) { - $list[] = $this->getClass($obj, 'AttrTransform_'); - } - return $this->listify($list); - } - - /** - * Listifies a hash of attributes to AttrDef classes - * @param $array Array hash in form of array('attrname' => HTMLPurifier_AttrDef) - */ - protected function listifyAttr($array) { - ksort($array); - $list = array(); - foreach ($array as $name => $obj) { - if ($obj === false) continue; - $list[] = "$name = " . $this->getClass($obj, 'AttrDef_') . ''; - } - return $this->listify($list); - } - - /** - * Creates a heavy header row - */ - protected function heavyHeader($text, $num = 1) { - $ret = ''; - $ret .= $this->start('tr'); - $ret .= $this->element('th', $text, array('colspan' => $num, 'class' => 'heavy')); - $ret .= $this->end('tr'); - return $ret; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/PropertyList.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/PropertyList.php deleted file mode 100644 index 2b99fb7bc..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/PropertyList.php +++ /dev/null @@ -1,86 +0,0 @@ -parent = $parent; - } - - /** - * Recursively retrieves the value for a key - */ - public function get($name) { - if ($this->has($name)) return $this->data[$name]; - // possible performance bottleneck, convert to iterative if necessary - if ($this->parent) return $this->parent->get($name); - throw new HTMLPurifier_Exception("Key '$name' not found"); - } - - /** - * Sets the value of a key, for this plist - */ - public function set($name, $value) { - $this->data[$name] = $value; - } - - /** - * Returns true if a given key exists - */ - public function has($name) { - return array_key_exists($name, $this->data); - } - - /** - * Resets a value to the value of it's parent, usually the default. If - * no value is specified, the entire plist is reset. - */ - public function reset($name = null) { - if ($name == null) $this->data = array(); - else unset($this->data[$name]); - } - - /** - * Squashes this property list and all of its property lists into a single - * array, and returns the array. This value is cached by default. - * @param $force If true, ignores the cache and regenerates the array. - */ - public function squash($force = false) { - if ($this->cache !== null && !$force) return $this->cache; - if ($this->parent) { - return $this->cache = array_merge($this->parent->squash($force), $this->data); - } else { - return $this->cache = $this->data; - } - } - - /** - * Returns the parent plist. - */ - public function getParent() { - return $this->parent; - } - - /** - * Sets the parent plist. - */ - public function setParent($plist) { - $this->parent = $plist; - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/PropertyListIterator.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/PropertyListIterator.php deleted file mode 100644 index 8f250443e..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/PropertyListIterator.php +++ /dev/null @@ -1,32 +0,0 @@ -l = strlen($filter); - $this->filter = $filter; - } - - public function accept() { - $key = $this->getInnerIterator()->key(); - if( strncmp($key, $this->filter, $this->l) !== 0 ) { - return false; - } - return true; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy.php deleted file mode 100644 index 246286521..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy.php +++ /dev/null @@ -1,26 +0,0 @@ -strategies as $strategy) { - $tokens = $strategy->execute($tokens, $config, $context); - } - return $tokens; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy/Core.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy/Core.php deleted file mode 100644 index d90e15860..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy/Core.php +++ /dev/null @@ -1,18 +0,0 @@ -strategies[] = new HTMLPurifier_Strategy_RemoveForeignElements(); - $this->strategies[] = new HTMLPurifier_Strategy_MakeWellFormed(); - $this->strategies[] = new HTMLPurifier_Strategy_FixNesting(); - $this->strategies[] = new HTMLPurifier_Strategy_ValidateAttributes(); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy/FixNesting.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy/FixNesting.php deleted file mode 100644 index d1588b938..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy/FixNesting.php +++ /dev/null @@ -1,346 +0,0 @@ -getHTMLDefinition(); - - $excludes_enabled = !$config->get('Core.DisableExcludes'); - - // insert implicit "parent" node, will be removed at end. - // DEFINITION CALL - $parent_name = $definition->info_parent; - array_unshift($tokens, new HTMLPurifier_Token_Start($parent_name)); - $tokens[] = new HTMLPurifier_Token_End($parent_name); - - // setup the context variable 'IsInline', for chameleon processing - // is 'false' when we are not inline, 'true' when it must always - // be inline, and an integer when it is inline for a certain - // branch of the document tree - $is_inline = $definition->info_parent_def->descendants_are_inline; - $context->register('IsInline', $is_inline); - - // setup error collector - $e =& $context->get('ErrorCollector', true); - - //####################################################################// - // Loop initialization - - // stack that contains the indexes of all parents, - // $stack[count($stack)-1] being the current parent - $stack = array(); - - // stack that contains all elements that are excluded - // it is organized by parent elements, similar to $stack, - // but it is only populated when an element with exclusions is - // processed, i.e. there won't be empty exclusions. - $exclude_stack = array(); - - // variable that contains the start token while we are processing - // nodes. This enables error reporting to do its job - $start_token = false; - $context->register('CurrentToken', $start_token); - - //####################################################################// - // Loop - - // iterate through all start nodes. Determining the start node - // is complicated so it has been omitted from the loop construct - for ($i = 0, $size = count($tokens) ; $i < $size; ) { - - //################################################################// - // Gather information on children - - // child token accumulator - $child_tokens = array(); - - // scroll to the end of this node, report number, and collect - // all children - for ($j = $i, $depth = 0; ; $j++) { - if ($tokens[$j] instanceof HTMLPurifier_Token_Start) { - $depth++; - // skip token assignment on first iteration, this is the - // token we currently are on - if ($depth == 1) continue; - } elseif ($tokens[$j] instanceof HTMLPurifier_Token_End) { - $depth--; - // skip token assignment on last iteration, this is the - // end token of the token we're currently on - if ($depth == 0) break; - } - $child_tokens[] = $tokens[$j]; - } - - // $i is index of start token - // $j is index of end token - - $start_token = $tokens[$i]; // to make token available via CurrentToken - - //################################################################// - // Gather information on parent - - // calculate parent information - if ($count = count($stack)) { - $parent_index = $stack[$count-1]; - $parent_name = $tokens[$parent_index]->name; - if ($parent_index == 0) { - $parent_def = $definition->info_parent_def; - } else { - $parent_def = $definition->info[$parent_name]; - } - } else { - // processing as if the parent were the "root" node - // unknown info, it won't be used anyway, in the future, - // we may want to enforce one element only (this is - // necessary for HTML Purifier to clean entire documents - $parent_index = $parent_name = $parent_def = null; - } - - // calculate context - if ($is_inline === false) { - // check if conditions make it inline - if (!empty($parent_def) && $parent_def->descendants_are_inline) { - $is_inline = $count - 1; - } - } else { - // check if we're out of inline - if ($count === $is_inline) { - $is_inline = false; - } - } - - //################################################################// - // Determine whether element is explicitly excluded SGML-style - - // determine whether or not element is excluded by checking all - // parent exclusions. The array should not be very large, two - // elements at most. - $excluded = false; - if (!empty($exclude_stack) && $excludes_enabled) { - foreach ($exclude_stack as $lookup) { - if (isset($lookup[$tokens[$i]->name])) { - $excluded = true; - // no need to continue processing - break; - } - } - } - - //################################################################// - // Perform child validation - - if ($excluded) { - // there is an exclusion, remove the entire node - $result = false; - $excludes = array(); // not used, but good to initialize anyway - } else { - // DEFINITION CALL - if ($i === 0) { - // special processing for the first node - $def = $definition->info_parent_def; - } else { - $def = $definition->info[$tokens[$i]->name]; - - } - - if (!empty($def->child)) { - // have DTD child def validate children - $result = $def->child->validateChildren( - $child_tokens, $config, $context); - } else { - // weird, no child definition, get rid of everything - $result = false; - } - - // determine whether or not this element has any exclusions - $excludes = $def->excludes; - } - - // $result is now a bool or array - - //################################################################// - // Process result by interpreting $result - - if ($result === true || $child_tokens === $result) { - // leave the node as is - - // register start token as a parental node start - $stack[] = $i; - - // register exclusions if there are any - if (!empty($excludes)) $exclude_stack[] = $excludes; - - // move cursor to next possible start node - $i++; - - } elseif($result === false) { - // remove entire node - - if ($e) { - if ($excluded) { - $e->send(E_ERROR, 'Strategy_FixNesting: Node excluded'); - } else { - $e->send(E_ERROR, 'Strategy_FixNesting: Node removed'); - } - } - - // calculate length of inner tokens and current tokens - $length = $j - $i + 1; - - // perform removal - array_splice($tokens, $i, $length); - - // update size - $size -= $length; - - // there is no start token to register, - // current node is now the next possible start node - // unless it turns out that we need to do a double-check - - // this is a rought heuristic that covers 100% of HTML's - // cases and 99% of all other cases. A child definition - // that would be tricked by this would be something like: - // ( | a b c) where it's all or nothing. Fortunately, - // our current implementation claims that that case would - // not allow empty, even if it did - if (!$parent_def->child->allow_empty) { - // we need to do a double-check [BACKTRACK] - $i = $parent_index; - array_pop($stack); - } - - // PROJECTED OPTIMIZATION: Process all children elements before - // reprocessing parent node. - - } else { - // replace node with $result - - // calculate length of inner tokens - $length = $j - $i - 1; - - if ($e) { - if (empty($result) && $length) { - $e->send(E_ERROR, 'Strategy_FixNesting: Node contents removed'); - } else { - $e->send(E_WARNING, 'Strategy_FixNesting: Node reorganized'); - } - } - - // perform replacement - array_splice($tokens, $i + 1, $length, $result); - - // update size - $size -= $length; - $size += count($result); - - // register start token as a parental node start - $stack[] = $i; - - // register exclusions if there are any - if (!empty($excludes)) $exclude_stack[] = $excludes; - - // move cursor to next possible start node - $i++; - - } - - //################################################################// - // Scroll to next start node - - // We assume, at this point, that $i is the index of the token - // that is the first possible new start point for a node. - - // Test if the token indeed is a start tag, if not, move forward - // and test again. - $size = count($tokens); - while ($i < $size and !$tokens[$i] instanceof HTMLPurifier_Token_Start) { - if ($tokens[$i] instanceof HTMLPurifier_Token_End) { - // pop a token index off the stack if we ended a node - array_pop($stack); - // pop an exclusion lookup off exclusion stack if - // we ended node and that node had exclusions - if ($i == 0 || $i == $size - 1) { - // use specialized var if it's the super-parent - $s_excludes = $definition->info_parent_def->excludes; - } else { - $s_excludes = $definition->info[$tokens[$i]->name]->excludes; - } - if ($s_excludes) { - array_pop($exclude_stack); - } - } - $i++; - } - - } - - //####################################################################// - // Post-processing - - // remove implicit parent tokens at the beginning and end - array_shift($tokens); - array_pop($tokens); - - // remove context variables - $context->destroy('IsInline'); - $context->destroy('CurrentToken'); - - //####################################################################// - // Return - - return $tokens; - - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php deleted file mode 100644 index c7aa1bb86..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy/MakeWellFormed.php +++ /dev/null @@ -1,532 +0,0 @@ -getHTMLDefinition(); - - // local variables - $generator = new HTMLPurifier_Generator($config, $context); - $escape_invalid_tags = $config->get('Core.EscapeInvalidTags'); - // used for autoclose early abortion - $global_parent_allowed_elements = array(); - if (isset($definition->info[$definition->info_parent])) { - // may be unset under testing circumstances - $global_parent_allowed_elements = $definition->info[$definition->info_parent]->child->getAllowedElements($config); - } - $e = $context->get('ErrorCollector', true); - $t = false; // token index - $i = false; // injector index - $token = false; // the current token - $reprocess = false; // whether or not to reprocess the same token - $stack = array(); - - // member variables - $this->stack =& $stack; - $this->t =& $t; - $this->tokens =& $tokens; - $this->config = $config; - $this->context = $context; - - // context variables - $context->register('CurrentNesting', $stack); - $context->register('InputIndex', $t); - $context->register('InputTokens', $tokens); - $context->register('CurrentToken', $token); - - // -- begin INJECTOR -- - - $this->injectors = array(); - - $injectors = $config->getBatch('AutoFormat'); - $def_injectors = $definition->info_injector; - $custom_injectors = $injectors['Custom']; - unset($injectors['Custom']); // special case - foreach ($injectors as $injector => $b) { - // XXX: Fix with a legitimate lookup table of enabled filters - if (strpos($injector, '.') !== false) continue; - $injector = "HTMLPurifier_Injector_$injector"; - if (!$b) continue; - $this->injectors[] = new $injector; - } - foreach ($def_injectors as $injector) { - // assumed to be objects - $this->injectors[] = $injector; - } - foreach ($custom_injectors as $injector) { - if (!$injector) continue; - if (is_string($injector)) { - $injector = "HTMLPurifier_Injector_$injector"; - $injector = new $injector; - } - $this->injectors[] = $injector; - } - - // give the injectors references to the definition and context - // variables for performance reasons - foreach ($this->injectors as $ix => $injector) { - $error = $injector->prepare($config, $context); - if (!$error) continue; - array_splice($this->injectors, $ix, 1); // rm the injector - trigger_error("Cannot enable {$injector->name} injector because $error is not allowed", E_USER_WARNING); - } - - // -- end INJECTOR -- - - // a note on reprocessing: - // In order to reduce code duplication, whenever some code needs - // to make HTML changes in order to make things "correct", the - // new HTML gets sent through the purifier, regardless of its - // status. This means that if we add a start token, because it - // was totally necessary, we don't have to update nesting; we just - // punt ($reprocess = true; continue;) and it does that for us. - - // isset is in loop because $tokens size changes during loop exec - for ( - $t = 0; - $t == 0 || isset($tokens[$t - 1]); - // only increment if we don't need to reprocess - $reprocess ? $reprocess = false : $t++ - ) { - - // check for a rewind - if (is_int($i) && $i >= 0) { - // possibility: disable rewinding if the current token has a - // rewind set on it already. This would offer protection from - // infinite loop, but might hinder some advanced rewinding. - $rewind_to = $this->injectors[$i]->getRewind(); - if (is_int($rewind_to) && $rewind_to < $t) { - if ($rewind_to < 0) $rewind_to = 0; - while ($t > $rewind_to) { - $t--; - $prev = $tokens[$t]; - // indicate that other injectors should not process this token, - // but we need to reprocess it - unset($prev->skip[$i]); - $prev->rewind = $i; - if ($prev instanceof HTMLPurifier_Token_Start) array_pop($this->stack); - elseif ($prev instanceof HTMLPurifier_Token_End) $this->stack[] = $prev->start; - } - } - $i = false; - } - - // handle case of document end - if (!isset($tokens[$t])) { - // kill processing if stack is empty - if (empty($this->stack)) break; - - // peek - $top_nesting = array_pop($this->stack); - $this->stack[] = $top_nesting; - - // send error [TagClosedSuppress] - if ($e && !isset($top_nesting->armor['MakeWellFormed_TagClosedError'])) { - $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by document end', $top_nesting); - } - - // append, don't splice, since this is the end - $tokens[] = new HTMLPurifier_Token_End($top_nesting->name); - - // punt! - $reprocess = true; - continue; - } - - $token = $tokens[$t]; - - //echo '
      '; printTokens($tokens, $t); printTokens($this->stack); - //flush(); - - // quick-check: if it's not a tag, no need to process - if (empty($token->is_tag)) { - if ($token instanceof HTMLPurifier_Token_Text) { - foreach ($this->injectors as $i => $injector) { - if (isset($token->skip[$i])) continue; - if ($token->rewind !== null && $token->rewind !== $i) continue; - $injector->handleText($token); - $this->processToken($token, $i); - $reprocess = true; - break; - } - } - // another possibility is a comment - continue; - } - - if (isset($definition->info[$token->name])) { - $type = $definition->info[$token->name]->child->type; - } else { - $type = false; // Type is unknown, treat accordingly - } - - // quick tag checks: anything that's *not* an end tag - $ok = false; - if ($type === 'empty' && $token instanceof HTMLPurifier_Token_Start) { - // claims to be a start tag but is empty - $token = new HTMLPurifier_Token_Empty($token->name, $token->attr, $token->line, $token->col, $token->armor); - $ok = true; - } elseif ($type && $type !== 'empty' && $token instanceof HTMLPurifier_Token_Empty) { - // claims to be empty but really is a start tag - $this->swap(new HTMLPurifier_Token_End($token->name)); - $this->insertBefore(new HTMLPurifier_Token_Start($token->name, $token->attr, $token->line, $token->col, $token->armor)); - // punt (since we had to modify the input stream in a non-trivial way) - $reprocess = true; - continue; - } elseif ($token instanceof HTMLPurifier_Token_Empty) { - // real empty token - $ok = true; - } elseif ($token instanceof HTMLPurifier_Token_Start) { - // start tag - - // ...unless they also have to close their parent - if (!empty($this->stack)) { - - // Performance note: you might think that it's rather - // inefficient, recalculating the autoclose information - // for every tag that a token closes (since when we - // do an autoclose, we push a new token into the - // stream and then /process/ that, before - // re-processing this token.) But this is - // necessary, because an injector can make an - // arbitrary transformations to the autoclosing - // tokens we introduce, so things may have changed - // in the meantime. Also, doing the inefficient thing is - // "easy" to reason about (for certain perverse definitions - // of "easy") - - $parent = array_pop($this->stack); - $this->stack[] = $parent; - - if (isset($definition->info[$parent->name])) { - $elements = $definition->info[$parent->name]->child->getAllowedElements($config); - $autoclose = !isset($elements[$token->name]); - } else { - $autoclose = false; - } - - if ($autoclose && $definition->info[$token->name]->wrap) { - // Check if an element can be wrapped by another - // element to make it valid in a context (for - // example,
          needs a
        • in between) - $wrapname = $definition->info[$token->name]->wrap; - $wrapdef = $definition->info[$wrapname]; - $elements = $wrapdef->child->getAllowedElements($config); - $parent_elements = $definition->info[$parent->name]->child->getAllowedElements($config); - if (isset($elements[$token->name]) && isset($parent_elements[$wrapname])) { - $newtoken = new HTMLPurifier_Token_Start($wrapname); - $this->insertBefore($newtoken); - $reprocess = true; - continue; - } - } - - $carryover = false; - if ($autoclose && $definition->info[$parent->name]->formatting) { - $carryover = true; - } - - if ($autoclose) { - // check if this autoclose is doomed to fail - // (this rechecks $parent, which his harmless) - $autoclose_ok = isset($global_parent_allowed_elements[$token->name]); - if (!$autoclose_ok) { - foreach ($this->stack as $ancestor) { - $elements = $definition->info[$ancestor->name]->child->getAllowedElements($config); - if (isset($elements[$token->name])) { - $autoclose_ok = true; - break; - } - if ($definition->info[$token->name]->wrap) { - $wrapname = $definition->info[$token->name]->wrap; - $wrapdef = $definition->info[$wrapname]; - $wrap_elements = $wrapdef->child->getAllowedElements($config); - if (isset($wrap_elements[$token->name]) && isset($elements[$wrapname])) { - $autoclose_ok = true; - break; - } - } - } - } - if ($autoclose_ok) { - // errors need to be updated - $new_token = new HTMLPurifier_Token_End($parent->name); - $new_token->start = $parent; - if ($carryover) { - $element = clone $parent; - // [TagClosedAuto] - $element->armor['MakeWellFormed_TagClosedError'] = true; - $element->carryover = true; - $this->processToken(array($new_token, $token, $element)); - } else { - $this->insertBefore($new_token); - } - // [TagClosedSuppress] - if ($e && !isset($parent->armor['MakeWellFormed_TagClosedError'])) { - if (!$carryover) { - $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag auto closed', $parent); - } else { - $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag carryover', $parent); - } - } - } else { - $this->remove(); - } - $reprocess = true; - continue; - } - - } - $ok = true; - } - - if ($ok) { - foreach ($this->injectors as $i => $injector) { - if (isset($token->skip[$i])) continue; - if ($token->rewind !== null && $token->rewind !== $i) continue; - $injector->handleElement($token); - $this->processToken($token, $i); - $reprocess = true; - break; - } - if (!$reprocess) { - // ah, nothing interesting happened; do normal processing - $this->swap($token); - if ($token instanceof HTMLPurifier_Token_Start) { - $this->stack[] = $token; - } elseif ($token instanceof HTMLPurifier_Token_End) { - throw new HTMLPurifier_Exception('Improper handling of end tag in start code; possible error in MakeWellFormed'); - } - } - continue; - } - - // sanity check: we should be dealing with a closing tag - if (!$token instanceof HTMLPurifier_Token_End) { - throw new HTMLPurifier_Exception('Unaccounted for tag token in input stream, bug in HTML Purifier'); - } - - // make sure that we have something open - if (empty($this->stack)) { - if ($escape_invalid_tags) { - if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag to text'); - $this->swap(new HTMLPurifier_Token_Text( - $generator->generateFromToken($token) - )); - } else { - $this->remove(); - if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Unnecessary end tag removed'); - } - $reprocess = true; - continue; - } - - // first, check for the simplest case: everything closes neatly. - // Eventually, everything passes through here; if there are problems - // we modify the input stream accordingly and then punt, so that - // the tokens get processed again. - $current_parent = array_pop($this->stack); - if ($current_parent->name == $token->name) { - $token->start = $current_parent; - foreach ($this->injectors as $i => $injector) { - if (isset($token->skip[$i])) continue; - if ($token->rewind !== null && $token->rewind !== $i) continue; - $injector->handleEnd($token); - $this->processToken($token, $i); - $this->stack[] = $current_parent; - $reprocess = true; - break; - } - continue; - } - - // okay, so we're trying to close the wrong tag - - // undo the pop previous pop - $this->stack[] = $current_parent; - - // scroll back the entire nest, trying to find our tag. - // (feature could be to specify how far you'd like to go) - $size = count($this->stack); - // -2 because -1 is the last element, but we already checked that - $skipped_tags = false; - for ($j = $size - 2; $j >= 0; $j--) { - if ($this->stack[$j]->name == $token->name) { - $skipped_tags = array_slice($this->stack, $j); - break; - } - } - - // we didn't find the tag, so remove - if ($skipped_tags === false) { - if ($escape_invalid_tags) { - $this->swap(new HTMLPurifier_Token_Text( - $generator->generateFromToken($token) - )); - if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag to text'); - } else { - $this->remove(); - if ($e) $e->send(E_WARNING, 'Strategy_MakeWellFormed: Stray end tag removed'); - } - $reprocess = true; - continue; - } - - // do errors, in REVERSE $j order: a,b,c with - $c = count($skipped_tags); - if ($e) { - for ($j = $c - 1; $j > 0; $j--) { - // notice we exclude $j == 0, i.e. the current ending tag, from - // the errors... [TagClosedSuppress] - if (!isset($skipped_tags[$j]->armor['MakeWellFormed_TagClosedError'])) { - $e->send(E_NOTICE, 'Strategy_MakeWellFormed: Tag closed by element end', $skipped_tags[$j]); - } - } - } - - // insert tags, in FORWARD $j order: c,b,a with - $replace = array($token); - for ($j = 1; $j < $c; $j++) { - // ...as well as from the insertions - $new_token = new HTMLPurifier_Token_End($skipped_tags[$j]->name); - $new_token->start = $skipped_tags[$j]; - array_unshift($replace, $new_token); - if (isset($definition->info[$new_token->name]) && $definition->info[$new_token->name]->formatting) { - // [TagClosedAuto] - $element = clone $skipped_tags[$j]; - $element->carryover = true; - $element->armor['MakeWellFormed_TagClosedError'] = true; - $replace[] = $element; - } - } - $this->processToken($replace); - $reprocess = true; - continue; - } - - $context->destroy('CurrentNesting'); - $context->destroy('InputTokens'); - $context->destroy('InputIndex'); - $context->destroy('CurrentToken'); - - unset($this->injectors, $this->stack, $this->tokens, $this->t); - return $tokens; - } - - /** - * Processes arbitrary token values for complicated substitution patterns. - * In general: - * - * If $token is an array, it is a list of tokens to substitute for the - * current token. These tokens then get individually processed. If there - * is a leading integer in the list, that integer determines how many - * tokens from the stream should be removed. - * - * If $token is a regular token, it is swapped with the current token. - * - * If $token is false, the current token is deleted. - * - * If $token is an integer, that number of tokens (with the first token - * being the current one) will be deleted. - * - * @param $token Token substitution value - * @param $injector Injector that performed the substitution; default is if - * this is not an injector related operation. - */ - protected function processToken($token, $injector = -1) { - - // normalize forms of token - if (is_object($token)) $token = array(1, $token); - if (is_int($token)) $token = array($token); - if ($token === false) $token = array(1); - if (!is_array($token)) throw new HTMLPurifier_Exception('Invalid token type from injector'); - if (!is_int($token[0])) array_unshift($token, 1); - if ($token[0] === 0) throw new HTMLPurifier_Exception('Deleting zero tokens is not valid'); - - // $token is now an array with the following form: - // array(number nodes to delete, new node 1, new node 2, ...) - - $delete = array_shift($token); - $old = array_splice($this->tokens, $this->t, $delete, $token); - - if ($injector > -1) { - // determine appropriate skips - $oldskip = isset($old[0]) ? $old[0]->skip : array(); - foreach ($token as $object) { - $object->skip = $oldskip; - $object->skip[$injector] = true; - } - } - - } - - /** - * Inserts a token before the current token. Cursor now points to - * this token. You must reprocess after this. - */ - private function insertBefore($token) { - array_splice($this->tokens, $this->t, 0, array($token)); - } - - /** - * Removes current token. Cursor now points to new token occupying previously - * occupied space. You must reprocess after this. - */ - private function remove() { - array_splice($this->tokens, $this->t, 1); - } - - /** - * Swap current token with new token. Cursor points to new token (no - * change). You must reprocess after this. - */ - private function swap($token) { - $this->tokens[$this->t] = $token; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php deleted file mode 100644 index bccaf14d3..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy/RemoveForeignElements.php +++ /dev/null @@ -1,188 +0,0 @@ -getHTMLDefinition(); - $generator = new HTMLPurifier_Generator($config, $context); - $result = array(); - - $escape_invalid_tags = $config->get('Core.EscapeInvalidTags'); - $remove_invalid_img = $config->get('Core.RemoveInvalidImg'); - - // currently only used to determine if comments should be kept - $trusted = $config->get('HTML.Trusted'); - $comment_lookup = $config->get('HTML.AllowedComments'); - $comment_regexp = $config->get('HTML.AllowedCommentsRegexp'); - $check_comments = $comment_lookup !== array() || $comment_regexp !== null; - - $remove_script_contents = $config->get('Core.RemoveScriptContents'); - $hidden_elements = $config->get('Core.HiddenElements'); - - // remove script contents compatibility - if ($remove_script_contents === true) { - $hidden_elements['script'] = true; - } elseif ($remove_script_contents === false && isset($hidden_elements['script'])) { - unset($hidden_elements['script']); - } - - $attr_validator = new HTMLPurifier_AttrValidator(); - - // removes tokens until it reaches a closing tag with its value - $remove_until = false; - - // converts comments into text tokens when this is equal to a tag name - $textify_comments = false; - - $token = false; - $context->register('CurrentToken', $token); - - $e = false; - if ($config->get('Core.CollectErrors')) { - $e =& $context->get('ErrorCollector'); - } - - foreach($tokens as $token) { - if ($remove_until) { - if (empty($token->is_tag) || $token->name !== $remove_until) { - continue; - } - } - if (!empty( $token->is_tag )) { - // DEFINITION CALL - - // before any processing, try to transform the element - if ( - isset($definition->info_tag_transform[$token->name]) - ) { - $original_name = $token->name; - // there is a transformation for this tag - // DEFINITION CALL - $token = $definition-> - info_tag_transform[$token->name]-> - transform($token, $config, $context); - if ($e) $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Tag transform', $original_name); - } - - if (isset($definition->info[$token->name])) { - - // mostly everything's good, but - // we need to make sure required attributes are in order - if ( - ($token instanceof HTMLPurifier_Token_Start || $token instanceof HTMLPurifier_Token_Empty) && - $definition->info[$token->name]->required_attr && - ($token->name != 'img' || $remove_invalid_img) // ensure config option still works - ) { - $attr_validator->validateToken($token, $config, $context); - $ok = true; - foreach ($definition->info[$token->name]->required_attr as $name) { - if (!isset($token->attr[$name])) { - $ok = false; - break; - } - } - if (!$ok) { - if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Missing required attribute', $name); - continue; - } - $token->armor['ValidateAttributes'] = true; - } - - if (isset($hidden_elements[$token->name]) && $token instanceof HTMLPurifier_Token_Start) { - $textify_comments = $token->name; - } elseif ($token->name === $textify_comments && $token instanceof HTMLPurifier_Token_End) { - $textify_comments = false; - } - - } elseif ($escape_invalid_tags) { - // invalid tag, generate HTML representation and insert in - if ($e) $e->send(E_WARNING, 'Strategy_RemoveForeignElements: Foreign element to text'); - $token = new HTMLPurifier_Token_Text( - $generator->generateFromToken($token) - ); - } else { - // check if we need to destroy all of the tag's children - // CAN BE GENERICIZED - if (isset($hidden_elements[$token->name])) { - if ($token instanceof HTMLPurifier_Token_Start) { - $remove_until = $token->name; - } elseif ($token instanceof HTMLPurifier_Token_Empty) { - // do nothing: we're still looking - } else { - $remove_until = false; - } - if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign meta element removed'); - } else { - if ($e) $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Foreign element removed'); - } - continue; - } - } elseif ($token instanceof HTMLPurifier_Token_Comment) { - // textify comments in script tags when they are allowed - if ($textify_comments !== false) { - $data = $token->data; - $token = new HTMLPurifier_Token_Text($data); - } elseif ($trusted || $check_comments) { - // always cleanup comments - $trailing_hyphen = false; - if ($e) { - // perform check whether or not there's a trailing hyphen - if (substr($token->data, -1) == '-') { - $trailing_hyphen = true; - } - } - $token->data = rtrim($token->data, '-'); - $found_double_hyphen = false; - while (strpos($token->data, '--') !== false) { - $found_double_hyphen = true; - $token->data = str_replace('--', '-', $token->data); - } - if ($trusted || !empty($comment_lookup[trim($token->data)]) || ($comment_regexp !== NULL && preg_match($comment_regexp, trim($token->data)))) { - // OK good - if ($e) { - if ($trailing_hyphen) { - $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Trailing hyphen in comment removed'); - } - if ($found_double_hyphen) { - $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Hyphens in comment collapsed'); - } - } - } else { - if ($e) { - $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed'); - } - continue; - } - } else { - // strip comments - if ($e) $e->send(E_NOTICE, 'Strategy_RemoveForeignElements: Comment removed'); - continue; - } - } elseif ($token instanceof HTMLPurifier_Token_Text) { - } else { - continue; - } - $result[] = $token; - } - if ($remove_until && $e) { - // we removed tokens until the end, throw error - $e->send(E_ERROR, 'Strategy_RemoveForeignElements: Token removed to end', $remove_until); - } - - $context->destroy('CurrentToken'); - - return $result; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy/ValidateAttributes.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy/ValidateAttributes.php deleted file mode 100644 index c3328a9d4..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Strategy/ValidateAttributes.php +++ /dev/null @@ -1,39 +0,0 @@ -register('CurrentToken', $token); - - foreach ($tokens as $key => $token) { - - // only process tokens that have attributes, - // namely start and empty tags - if (!$token instanceof HTMLPurifier_Token_Start && !$token instanceof HTMLPurifier_Token_Empty) continue; - - // skip tokens that are armored - if (!empty($token->armor['ValidateAttributes'])) continue; - - // note that we have no facilities here for removing tokens - $validator->validateToken($token, $config, $context); - - $tokens[$key] = $token; // for PHP 4 - } - $context->destroy('CurrentToken'); - - return $tokens; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/StringHash.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/StringHash.php deleted file mode 100644 index 62085c5c2..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/StringHash.php +++ /dev/null @@ -1,39 +0,0 @@ -accessed[$index] = true; - return parent::offsetGet($index); - } - - /** - * Returns a lookup array of all array indexes that have been accessed. - * @return Array in form array($index => true). - */ - public function getAccessed() { - return $this->accessed; - } - - /** - * Resets the access array. - */ - public function resetAccessed() { - $this->accessed = array(); - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/StringHashParser.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/StringHashParser.php deleted file mode 100644 index f3e70c712..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/StringHashParser.php +++ /dev/null @@ -1,110 +0,0 @@ - 'DefaultKeyValue', - * 'KEY' => 'Value', - * 'KEY2' => 'Value2', - * 'MULTILINE-KEY' => "Multiline\nvalue.\n", - * ) - * - * We use this as an easy to use file-format for configuration schema - * files, but the class itself is usage agnostic. - * - * You can use ---- to forcibly terminate parsing of a single string-hash; - * this marker is used in multi string-hashes to delimit boundaries. - */ -class HTMLPurifier_StringHashParser -{ - - public $default = 'ID'; - - /** - * Parses a file that contains a single string-hash. - */ - public function parseFile($file) { - if (!file_exists($file)) return false; - $fh = fopen($file, 'r'); - if (!$fh) return false; - $ret = $this->parseHandle($fh); - fclose($fh); - return $ret; - } - - /** - * Parses a file that contains multiple string-hashes delimited by '----' - */ - public function parseMultiFile($file) { - if (!file_exists($file)) return false; - $ret = array(); - $fh = fopen($file, 'r'); - if (!$fh) return false; - while (!feof($fh)) { - $ret[] = $this->parseHandle($fh); - } - fclose($fh); - return $ret; - } - - /** - * Internal parser that acepts a file handle. - * @note While it's possible to simulate in-memory parsing by using - * custom stream wrappers, if such a use-case arises we should - * factor out the file handle into its own class. - * @param $fh File handle with pointer at start of valid string-hash - * block. - */ - protected function parseHandle($fh) { - $state = false; - $single = false; - $ret = array(); - do { - $line = fgets($fh); - if ($line === false) break; - $line = rtrim($line, "\n\r"); - if (!$state && $line === '') continue; - if ($line === '----') break; - if (strncmp('--#', $line, 3) === 0) { - // Comment - continue; - } elseif (strncmp('--', $line, 2) === 0) { - // Multiline declaration - $state = trim($line, '- '); - if (!isset($ret[$state])) $ret[$state] = ''; - continue; - } elseif (!$state) { - $single = true; - if (strpos($line, ':') !== false) { - // Single-line declaration - list($state, $line) = explode(':', $line, 2); - $line = trim($line); - } else { - // Use default declaration - $state = $this->default; - } - } - if ($single) { - $ret[$state] = $line; - $single = false; - $state = false; - } else { - $ret[$state] .= "$line\n"; - } - } while (!feof($fh)); - return $ret; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/TagTransform.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/TagTransform.php deleted file mode 100644 index 210a44721..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/TagTransform.php +++ /dev/null @@ -1,36 +0,0 @@ - 'xx-small', - '1' => 'xx-small', - '2' => 'small', - '3' => 'medium', - '4' => 'large', - '5' => 'x-large', - '6' => 'xx-large', - '7' => '300%', - '-1' => 'smaller', - '-2' => '60%', - '+1' => 'larger', - '+2' => '150%', - '+3' => '200%', - '+4' => '300%' - ); - - public function transform($tag, $config, $context) { - - if ($tag instanceof HTMLPurifier_Token_End) { - $new_tag = clone $tag; - $new_tag->name = $this->transform_to; - return $new_tag; - } - - $attr = $tag->attr; - $prepend_style = ''; - - // handle color transform - if (isset($attr['color'])) { - $prepend_style .= 'color:' . $attr['color'] . ';'; - unset($attr['color']); - } - - // handle face transform - if (isset($attr['face'])) { - $prepend_style .= 'font-family:' . $attr['face'] . ';'; - unset($attr['face']); - } - - // handle size transform - if (isset($attr['size'])) { - // normalize large numbers - if ($attr['size'] !== '') { - if ($attr['size']{0} == '+' || $attr['size']{0} == '-') { - $size = (int) $attr['size']; - if ($size < -2) $attr['size'] = '-2'; - if ($size > 4) $attr['size'] = '+4'; - } else { - $size = (int) $attr['size']; - if ($size > 7) $attr['size'] = '7'; - } - } - if (isset($this->_size_lookup[$attr['size']])) { - $prepend_style .= 'font-size:' . - $this->_size_lookup[$attr['size']] . ';'; - } - unset($attr['size']); - } - - if ($prepend_style) { - $attr['style'] = isset($attr['style']) ? - $prepend_style . $attr['style'] : - $prepend_style; - } - - $new_tag = clone $tag; - $new_tag->name = $this->transform_to; - $new_tag->attr = $attr; - - return $new_tag; - - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/TagTransform/Simple.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/TagTransform/Simple.php deleted file mode 100644 index 0e36130f2..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/TagTransform/Simple.php +++ /dev/null @@ -1,35 +0,0 @@ -transform_to = $transform_to; - $this->style = $style; - } - - public function transform($tag, $config, $context) { - $new_tag = clone $tag; - $new_tag->name = $this->transform_to; - if (!is_null($this->style) && - ($new_tag instanceof HTMLPurifier_Token_Start || $new_tag instanceof HTMLPurifier_Token_Empty) - ) { - $this->prependCSS($new_tag->attr, $this->style); - } - return $new_tag; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Token.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Token.php deleted file mode 100644 index 7900e6cb1..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Token.php +++ /dev/null @@ -1,57 +0,0 @@ -line = $l; - $this->col = $c; - } - - /** - * Convenience function for DirectLex settings line/col position. - */ - public function rawPosition($l, $c) { - if ($c === -1) $l++; - $this->line = $l; - $this->col = $c; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Token/Comment.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Token/Comment.php deleted file mode 100644 index dc6bdcabb..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Token/Comment.php +++ /dev/null @@ -1,22 +0,0 @@ -data = $data; - $this->line = $line; - $this->col = $col; - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Token/Empty.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Token/Empty.php deleted file mode 100644 index 2a82b47ad..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Token/Empty.php +++ /dev/null @@ -1,11 +0,0 @@ -!empty($obj->is_tag) - * without having to use a function call is_a(). - */ - public $is_tag = true; - - /** - * The lower-case name of the tag, like 'a', 'b' or 'blockquote'. - * - * @note Strictly speaking, XML tags are case sensitive, so we shouldn't - * be lower-casing them, but these tokens cater to HTML tags, which are - * insensitive. - */ - public $name; - - /** - * Associative array of the tag's attributes. - */ - public $attr = array(); - - /** - * Non-overloaded constructor, which lower-cases passed tag name. - * - * @param $name String name. - * @param $attr Associative array of attributes. - */ - public function __construct($name, $attr = array(), $line = null, $col = null, $armor = array()) { - $this->name = ctype_lower($name) ? $name : strtolower($name); - foreach ($attr as $key => $value) { - // normalization only necessary when key is not lowercase - if (!ctype_lower($key)) { - $new_key = strtolower($key); - if (!isset($attr[$new_key])) { - $attr[$new_key] = $attr[$key]; - } - if ($new_key !== $key) { - unset($attr[$key]); - } - } - } - $this->attr = $attr; - $this->line = $line; - $this->col = $col; - $this->armor = $armor; - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Token/Text.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Token/Text.php deleted file mode 100644 index 82efd823d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/Token/Text.php +++ /dev/null @@ -1,33 +0,0 @@ -data = $data; - $this->is_whitespace = ctype_space($data); - $this->line = $line; - $this->col = $col; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/TokenFactory.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/TokenFactory.php deleted file mode 100644 index 7cf48fb41..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/TokenFactory.php +++ /dev/null @@ -1,94 +0,0 @@ -p_start = new HTMLPurifier_Token_Start('', array()); - $this->p_end = new HTMLPurifier_Token_End(''); - $this->p_empty = new HTMLPurifier_Token_Empty('', array()); - $this->p_text = new HTMLPurifier_Token_Text(''); - $this->p_comment= new HTMLPurifier_Token_Comment(''); - } - - /** - * Creates a HTMLPurifier_Token_Start. - * @param $name Tag name - * @param $attr Associative array of attributes - * @return Generated HTMLPurifier_Token_Start - */ - public function createStart($name, $attr = array()) { - $p = clone $this->p_start; - $p->__construct($name, $attr); - return $p; - } - - /** - * Creates a HTMLPurifier_Token_End. - * @param $name Tag name - * @return Generated HTMLPurifier_Token_End - */ - public function createEnd($name) { - $p = clone $this->p_end; - $p->__construct($name); - return $p; - } - - /** - * Creates a HTMLPurifier_Token_Empty. - * @param $name Tag name - * @param $attr Associative array of attributes - * @return Generated HTMLPurifier_Token_Empty - */ - public function createEmpty($name, $attr = array()) { - $p = clone $this->p_empty; - $p->__construct($name, $attr); - return $p; - } - - /** - * Creates a HTMLPurifier_Token_Text. - * @param $data Data of text token - * @return Generated HTMLPurifier_Token_Text - */ - public function createText($data) { - $p = clone $this->p_text; - $p->__construct($data); - return $p; - } - - /** - * Creates a HTMLPurifier_Token_Comment. - * @param $data Data of comment token - * @return Generated HTMLPurifier_Token_Comment - */ - public function createComment($data) { - $p = clone $this->p_comment; - $p->__construct($data); - return $p; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URI.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URI.php deleted file mode 100644 index f158ef5e3..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URI.php +++ /dev/null @@ -1,242 +0,0 @@ -scheme = is_null($scheme) || ctype_lower($scheme) ? $scheme : strtolower($scheme); - $this->userinfo = $userinfo; - $this->host = $host; - $this->port = is_null($port) ? $port : (int) $port; - $this->path = $path; - $this->query = $query; - $this->fragment = $fragment; - } - - /** - * Retrieves a scheme object corresponding to the URI's scheme/default - * @param $config Instance of HTMLPurifier_Config - * @param $context Instance of HTMLPurifier_Context - * @return Scheme object appropriate for validating this URI - */ - public function getSchemeObj($config, $context) { - $registry = HTMLPurifier_URISchemeRegistry::instance(); - if ($this->scheme !== null) { - $scheme_obj = $registry->getScheme($this->scheme, $config, $context); - if (!$scheme_obj) return false; // invalid scheme, clean it out - } else { - // no scheme: retrieve the default one - $def = $config->getDefinition('URI'); - $scheme_obj = $def->getDefaultScheme($config, $context); - if (!$scheme_obj) { - // something funky happened to the default scheme object - trigger_error( - 'Default scheme object "' . $def->defaultScheme . '" was not readable', - E_USER_WARNING - ); - return false; - } - } - return $scheme_obj; - } - - /** - * Generic validation method applicable for all schemes. May modify - * this URI in order to get it into a compliant form. - * @param $config Instance of HTMLPurifier_Config - * @param $context Instance of HTMLPurifier_Context - * @return True if validation/filtering succeeds, false if failure - */ - public function validate($config, $context) { - - // ABNF definitions from RFC 3986 - $chars_sub_delims = '!$&\'()*+,;='; - $chars_gen_delims = ':/?#[]@'; - $chars_pchar = $chars_sub_delims . ':@'; - - // validate host - if (!is_null($this->host)) { - $host_def = new HTMLPurifier_AttrDef_URI_Host(); - $this->host = $host_def->validate($this->host, $config, $context); - if ($this->host === false) $this->host = null; - } - - // validate scheme - // NOTE: It's not appropriate to check whether or not this - // scheme is in our registry, since a URIFilter may convert a - // URI that we don't allow into one we do. So instead, we just - // check if the scheme can be dropped because there is no host - // and it is our default scheme. - if (!is_null($this->scheme) && is_null($this->host) || $this->host === '') { - // support for relative paths is pretty abysmal when the - // scheme is present, so axe it when possible - $def = $config->getDefinition('URI'); - if ($def->defaultScheme === $this->scheme) { - $this->scheme = null; - } - } - - // validate username - if (!is_null($this->userinfo)) { - $encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . ':'); - $this->userinfo = $encoder->encode($this->userinfo); - } - - // validate port - if (!is_null($this->port)) { - if ($this->port < 1 || $this->port > 65535) $this->port = null; - } - - // validate path - $path_parts = array(); - $segments_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/'); - if (!is_null($this->host)) { // this catches $this->host === '' - // path-abempty (hier and relative) - // http://www.example.com/my/path - // //www.example.com/my/path (looks odd, but works, and - // recognized by most browsers) - // (this set is valid or invalid on a scheme by scheme - // basis, so we'll deal with it later) - // file:///my/path - // ///my/path - $this->path = $segments_encoder->encode($this->path); - } elseif ($this->path !== '') { - if ($this->path[0] === '/') { - // path-absolute (hier and relative) - // http:/my/path - // /my/path - if (strlen($this->path) >= 2 && $this->path[1] === '/') { - // This could happen if both the host gets stripped - // out - // http://my/path - // //my/path - $this->path = ''; - } else { - $this->path = $segments_encoder->encode($this->path); - } - } elseif (!is_null($this->scheme)) { - // path-rootless (hier) - // http:my/path - // Short circuit evaluation means we don't need to check nz - $this->path = $segments_encoder->encode($this->path); - } else { - // path-noscheme (relative) - // my/path - // (once again, not checking nz) - $segment_nc_encoder = new HTMLPurifier_PercentEncoder($chars_sub_delims . '@'); - $c = strpos($this->path, '/'); - if ($c !== false) { - $this->path = - $segment_nc_encoder->encode(substr($this->path, 0, $c)) . - $segments_encoder->encode(substr($this->path, $c)); - } else { - $this->path = $segment_nc_encoder->encode($this->path); - } - } - } else { - // path-empty (hier and relative) - $this->path = ''; // just to be safe - } - - // qf = query and fragment - $qf_encoder = new HTMLPurifier_PercentEncoder($chars_pchar . '/?'); - - if (!is_null($this->query)) { - $this->query = $qf_encoder->encode($this->query); - } - - if (!is_null($this->fragment)) { - $this->fragment = $qf_encoder->encode($this->fragment); - } - - return true; - - } - - /** - * Convert URI back to string - * @return String URI appropriate for output - */ - public function toString() { - // reconstruct authority - $authority = null; - // there is a rendering difference between a null authority - // (http:foo-bar) and an empty string authority - // (http:///foo-bar). - if (!is_null($this->host)) { - $authority = ''; - if(!is_null($this->userinfo)) $authority .= $this->userinfo . '@'; - $authority .= $this->host; - if(!is_null($this->port)) $authority .= ':' . $this->port; - } - - // Reconstruct the result - // One might wonder about parsing quirks from browsers after - // this reconstruction. Unfortunately, parsing behavior depends - // on what *scheme* was employed (file:///foo is handled *very* - // differently than http:///foo), so unfortunately we have to - // defer to the schemes to do the right thing. - $result = ''; - if (!is_null($this->scheme)) $result .= $this->scheme . ':'; - if (!is_null($authority)) $result .= '//' . $authority; - $result .= $this->path; - if (!is_null($this->query)) $result .= '?' . $this->query; - if (!is_null($this->fragment)) $result .= '#' . $this->fragment; - - return $result; - } - - /** - * Returns true if this URL might be considered a 'local' URL given - * the current context. This is true when the host is null, or - * when it matches the host supplied to the configuration. - * - * Note that this does not do any scheme checking, so it is mostly - * only appropriate for metadata that doesn't care about protocol - * security. isBenign is probably what you actually want. - */ - public function isLocal($config, $context) { - if ($this->host === null) return true; - $uri_def = $config->getDefinition('URI'); - if ($uri_def->host === $this->host) return true; - return false; - } - - /** - * Returns true if this URL should be considered a 'benign' URL, - * that is: - * - * - It is a local URL (isLocal), and - * - It has a equal or better level of security - */ - public function isBenign($config, $context) { - if (!$this->isLocal($config, $context)) return false; - - $scheme_obj = $this->getSchemeObj($config, $context); - if (!$scheme_obj) return false; // conservative approach - - $current_scheme_obj = $config->getDefinition('URI')->getDefaultScheme($config, $context); - if ($current_scheme_obj->secure) { - if (!$scheme_obj->secure) { - return false; - } - } - return true; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIDefinition.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIDefinition.php deleted file mode 100644 index 4dbde8062..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIDefinition.php +++ /dev/null @@ -1,103 +0,0 @@ -registerFilter(new HTMLPurifier_URIFilter_DisableExternal()); - $this->registerFilter(new HTMLPurifier_URIFilter_DisableExternalResources()); - $this->registerFilter(new HTMLPurifier_URIFilter_DisableResources()); - $this->registerFilter(new HTMLPurifier_URIFilter_HostBlacklist()); - $this->registerFilter(new HTMLPurifier_URIFilter_SafeIframe()); - $this->registerFilter(new HTMLPurifier_URIFilter_MakeAbsolute()); - $this->registerFilter(new HTMLPurifier_URIFilter_Munge()); - } - - public function registerFilter($filter) { - $this->registeredFilters[$filter->name] = $filter; - } - - public function addFilter($filter, $config) { - $r = $filter->prepare($config); - if ($r === false) return; // null is ok, for backwards compat - if ($filter->post) { - $this->postFilters[$filter->name] = $filter; - } else { - $this->filters[$filter->name] = $filter; - } - } - - protected function doSetup($config) { - $this->setupMemberVariables($config); - $this->setupFilters($config); - } - - protected function setupFilters($config) { - foreach ($this->registeredFilters as $name => $filter) { - if ($filter->always_load) { - $this->addFilter($filter, $config); - } else { - $conf = $config->get('URI.' . $name); - if ($conf !== false && $conf !== null) { - $this->addFilter($filter, $config); - } - } - } - unset($this->registeredFilters); - } - - protected function setupMemberVariables($config) { - $this->host = $config->get('URI.Host'); - $base_uri = $config->get('URI.Base'); - if (!is_null($base_uri)) { - $parser = new HTMLPurifier_URIParser(); - $this->base = $parser->parse($base_uri); - $this->defaultScheme = $this->base->scheme; - if (is_null($this->host)) $this->host = $this->base->host; - } - if (is_null($this->defaultScheme)) $this->defaultScheme = $config->get('URI.DefaultScheme'); - } - - public function getDefaultScheme($config, $context) { - return HTMLPurifier_URISchemeRegistry::instance()->getScheme($this->defaultScheme, $config, $context); - } - - public function filter(&$uri, $config, $context) { - foreach ($this->filters as $name => $f) { - $result = $f->filter($uri, $config, $context); - if (!$result) return false; - } - return true; - } - - public function postFilter(&$uri, $config, $context) { - foreach ($this->postFilters as $name => $f) { - $result = $f->filter($uri, $config, $context); - if (!$result) return false; - } - return true; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter.php deleted file mode 100644 index 6a1b0b08e..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter.php +++ /dev/null @@ -1,67 +0,0 @@ -getDefinition('URI')->host; - if ($our_host !== null) $this->ourHostParts = array_reverse(explode('.', $our_host)); - } - public function filter(&$uri, $config, $context) { - if (is_null($uri->host)) return true; - if ($this->ourHostParts === false) return false; - $host_parts = array_reverse(explode('.', $uri->host)); - foreach ($this->ourHostParts as $i => $x) { - if (!isset($host_parts[$i])) return false; - if ($host_parts[$i] != $this->ourHostParts[$i]) return false; - } - return true; - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/DisableExternalResources.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/DisableExternalResources.php deleted file mode 100644 index 881abc43c..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/DisableExternalResources.php +++ /dev/null @@ -1,12 +0,0 @@ -get('EmbeddedURI', true)) return true; - return parent::filter($uri, $config, $context); - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/DisableResources.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/DisableResources.php deleted file mode 100644 index 67538c7bb..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/DisableResources.php +++ /dev/null @@ -1,11 +0,0 @@ -get('EmbeddedURI', true); - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php deleted file mode 100644 index 55fde3bf4..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/HostBlacklist.php +++ /dev/null @@ -1,25 +0,0 @@ -blacklist = $config->get('URI.HostBlacklist'); - return true; - } - public function filter(&$uri, $config, $context) { - foreach($this->blacklist as $blacklisted_host_fragment) { - if (strpos($uri->host, $blacklisted_host_fragment) !== false) { - return false; - } - } - return true; - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php deleted file mode 100644 index f46ab2630..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/MakeAbsolute.php +++ /dev/null @@ -1,114 +0,0 @@ -getDefinition('URI'); - $this->base = $def->base; - if (is_null($this->base)) { - trigger_error('URI.MakeAbsolute is being ignored due to lack of value for URI.Base configuration', E_USER_WARNING); - return false; - } - $this->base->fragment = null; // fragment is invalid for base URI - $stack = explode('/', $this->base->path); - array_pop($stack); // discard last segment - $stack = $this->_collapseStack($stack); // do pre-parsing - $this->basePathStack = $stack; - return true; - } - public function filter(&$uri, $config, $context) { - if (is_null($this->base)) return true; // abort early - if ( - $uri->path === '' && is_null($uri->scheme) && - is_null($uri->host) && is_null($uri->query) && is_null($uri->fragment) - ) { - // reference to current document - $uri = clone $this->base; - return true; - } - if (!is_null($uri->scheme)) { - // absolute URI already: don't change - if (!is_null($uri->host)) return true; - $scheme_obj = $uri->getSchemeObj($config, $context); - if (!$scheme_obj) { - // scheme not recognized - return false; - } - if (!$scheme_obj->hierarchical) { - // non-hierarchal URI with explicit scheme, don't change - return true; - } - // special case: had a scheme but always is hierarchical and had no authority - } - if (!is_null($uri->host)) { - // network path, don't bother - return true; - } - if ($uri->path === '') { - $uri->path = $this->base->path; - } elseif ($uri->path[0] !== '/') { - // relative path, needs more complicated processing - $stack = explode('/', $uri->path); - $new_stack = array_merge($this->basePathStack, $stack); - if ($new_stack[0] !== '' && !is_null($this->base->host)) { - array_unshift($new_stack, ''); - } - $new_stack = $this->_collapseStack($new_stack); - $uri->path = implode('/', $new_stack); - } else { - // absolute path, but still we should collapse - $uri->path = implode('/', $this->_collapseStack(explode('/', $uri->path))); - } - // re-combine - $uri->scheme = $this->base->scheme; - if (is_null($uri->userinfo)) $uri->userinfo = $this->base->userinfo; - if (is_null($uri->host)) $uri->host = $this->base->host; - if (is_null($uri->port)) $uri->port = $this->base->port; - return true; - } - - /** - * Resolve dots and double-dots in a path stack - */ - private function _collapseStack($stack) { - $result = array(); - $is_folder = false; - for ($i = 0; isset($stack[$i]); $i++) { - $is_folder = false; - // absorb an internally duplicated slash - if ($stack[$i] == '' && $i && isset($stack[$i+1])) continue; - if ($stack[$i] == '..') { - if (!empty($result)) { - $segment = array_pop($result); - if ($segment === '' && empty($result)) { - // error case: attempted to back out too far: - // restore the leading slash - $result[] = ''; - } elseif ($segment === '..') { - $result[] = '..'; // cannot remove .. with .. - } - } else { - // relative path, preserve the double-dots - $result[] = '..'; - } - $is_folder = true; - continue; - } - if ($stack[$i] == '.') { - // silently absorb - $is_folder = true; - continue; - } - $result[] = $stack[$i]; - } - if ($is_folder) $result[] = ''; - return $result; - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/Munge.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/Munge.php deleted file mode 100644 index de695df14..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/Munge.php +++ /dev/null @@ -1,53 +0,0 @@ -target = $config->get('URI.' . $this->name); - $this->parser = new HTMLPurifier_URIParser(); - $this->doEmbed = $config->get('URI.MungeResources'); - $this->secretKey = $config->get('URI.MungeSecretKey'); - return true; - } - public function filter(&$uri, $config, $context) { - if ($context->get('EmbeddedURI', true) && !$this->doEmbed) return true; - - $scheme_obj = $uri->getSchemeObj($config, $context); - if (!$scheme_obj) return true; // ignore unknown schemes, maybe another postfilter did it - if (!$scheme_obj->browsable) return true; // ignore non-browseable schemes, since we can't munge those in a reasonable way - if ($uri->isBenign($config, $context)) return true; // don't redirect if a benign URL - - $this->makeReplace($uri, $config, $context); - $this->replace = array_map('rawurlencode', $this->replace); - - $new_uri = strtr($this->target, $this->replace); - $new_uri = $this->parser->parse($new_uri); - // don't redirect if the target host is the same as the - // starting host - if ($uri->host === $new_uri->host) return true; - $uri = $new_uri; // overwrite - return true; - } - - protected function makeReplace($uri, $config, $context) { - $string = $uri->toString(); - // always available - $this->replace['%s'] = $string; - $this->replace['%r'] = $context->get('EmbeddedURI', true); - $token = $context->get('CurrentToken', true); - $this->replace['%n'] = $token ? $token->name : null; - $this->replace['%m'] = $context->get('CurrentAttr', true); - $this->replace['%p'] = $context->get('CurrentCSSProperty', true); - // not always available - if ($this->secretKey) $this->replace['%t'] = sha1($this->secretKey . ':' . $string); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/SafeIframe.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/SafeIframe.php deleted file mode 100644 index 284bb13de..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIFilter/SafeIframe.php +++ /dev/null @@ -1,35 +0,0 @@ -regexp = $config->get('URI.SafeIframeRegexp'); - return true; - } - public function filter(&$uri, $config, $context) { - // check if filter not applicable - if (!$config->get('HTML.SafeIframe')) return true; - // check if the filter should actually trigger - if (!$context->get('EmbeddedURI', true)) return true; - $token = $context->get('CurrentToken', true); - if (!($token && $token->name == 'iframe')) return true; - // check if we actually have some whitelists enabled - if ($this->regexp === null) return false; - // actually check the whitelists - return preg_match($this->regexp, $uri->toString()); - } -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIParser.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIParser.php deleted file mode 100644 index 7179e4ab8..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIParser.php +++ /dev/null @@ -1,70 +0,0 @@ -percentEncoder = new HTMLPurifier_PercentEncoder(); - } - - /** - * Parses a URI. - * @param $uri string URI to parse - * @return HTMLPurifier_URI representation of URI. This representation has - * not been validated yet and may not conform to RFC. - */ - public function parse($uri) { - - $uri = $this->percentEncoder->normalize($uri); - - // Regexp is as per Appendix B. - // Note that ["<>] are an addition to the RFC's recommended - // characters, because they represent external delimeters. - $r_URI = '!'. - '(([^:/?#"<>]+):)?'. // 2. Scheme - '(//([^/?#"<>]*))?'. // 4. Authority - '([^?#"<>]*)'. // 5. Path - '(\?([^#"<>]*))?'. // 7. Query - '(#([^"<>]*))?'. // 8. Fragment - '!'; - - $matches = array(); - $result = preg_match($r_URI, $uri, $matches); - - if (!$result) return false; // *really* invalid URI - - // seperate out parts - $scheme = !empty($matches[1]) ? $matches[2] : null; - $authority = !empty($matches[3]) ? $matches[4] : null; - $path = $matches[5]; // always present, can be empty - $query = !empty($matches[6]) ? $matches[7] : null; - $fragment = !empty($matches[8]) ? $matches[9] : null; - - // further parse authority - if ($authority !== null) { - $r_authority = "/^((.+?)@)?(\[[^\]]+\]|[^:]*)(:(\d*))?/"; - $matches = array(); - preg_match($r_authority, $authority, $matches); - $userinfo = !empty($matches[1]) ? $matches[2] : null; - $host = !empty($matches[3]) ? $matches[3] : ''; - $port = !empty($matches[4]) ? (int) $matches[5] : null; - } else { - $port = $host = $userinfo = null; - } - - return new HTMLPurifier_URI( - $scheme, $userinfo, $host, $port, $path, $query, $fragment); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme.php deleted file mode 100644 index 7be958143..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme.php +++ /dev/null @@ -1,95 +0,0 @@ -, resolves edge cases - * with making relative URIs absolute - */ - public $hierarchical = false; - - /** - * Whether or not the URI may omit a hostname when the scheme is - * explicitly specified, ala file:///path/to/file. As of writing, - * 'file' is the only scheme that browsers support his properly. - */ - public $may_omit_host = false; - - /** - * Validates the components of a URI for a specific scheme. - * @param $uri Reference to a HTMLPurifier_URI object - * @param $config HTMLPurifier_Config object - * @param $context HTMLPurifier_Context object - * @return Bool success or failure - */ - public abstract function doValidate(&$uri, $config, $context); - - /** - * Public interface for validating components of a URI. Performs a - * bunch of default actions. Don't overload this method. - * @param $uri Reference to a HTMLPurifier_URI object - * @param $config HTMLPurifier_Config object - * @param $context HTMLPurifier_Context object - * @return Bool success or failure - */ - public function validate(&$uri, $config, $context) { - if ($this->default_port == $uri->port) $uri->port = null; - // kludge: browsers do funny things when the scheme but not the - // authority is set - if (!$this->may_omit_host && - // if the scheme is present, a missing host is always in error - (!is_null($uri->scheme) && ($uri->host === '' || is_null($uri->host))) || - // if the scheme is not present, a *blank* host is in error, - // since this translates into '///path' which most browsers - // interpret as being 'http://path'. - (is_null($uri->scheme) && $uri->host === '') - ) { - do { - if (is_null($uri->scheme)) { - if (substr($uri->path, 0, 2) != '//') { - $uri->host = null; - break; - } - // URI is '////path', so we cannot nullify the - // host to preserve semantics. Try expanding the - // hostname instead (fall through) - } - // first see if we can manually insert a hostname - $host = $config->get('URI.Host'); - if (!is_null($host)) { - $uri->host = $host; - } else { - // we can't do anything sensible, reject the URL. - return false; - } - } while (false); - } - return $this->doValidate($uri, $config, $context); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/data.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/data.php deleted file mode 100644 index ab56a3e96..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/data.php +++ /dev/null @@ -1,98 +0,0 @@ - true, - 'image/gif' => true, - 'image/png' => true, - ); - // this is actually irrelevant since we only write out the path - // component - public $may_omit_host = true; - - public function doValidate(&$uri, $config, $context) { - $result = explode(',', $uri->path, 2); - $is_base64 = false; - $charset = null; - $content_type = null; - if (count($result) == 2) { - list($metadata, $data) = $result; - // do some legwork on the metadata - $metas = explode(';', $metadata); - while(!empty($metas)) { - $cur = array_shift($metas); - if ($cur == 'base64') { - $is_base64 = true; - break; - } - if (substr($cur, 0, 8) == 'charset=') { - // doesn't match if there are arbitrary spaces, but - // whatever dude - if ($charset !== null) continue; // garbage - $charset = substr($cur, 8); // not used - } else { - if ($content_type !== null) continue; // garbage - $content_type = $cur; - } - } - } else { - $data = $result[0]; - } - if ($content_type !== null && empty($this->allowed_types[$content_type])) { - return false; - } - if ($charset !== null) { - // error; we don't allow plaintext stuff - $charset = null; - } - $data = rawurldecode($data); - if ($is_base64) { - $raw_data = base64_decode($data); - } else { - $raw_data = $data; - } - // XXX probably want to refactor this into a general mechanism - // for filtering arbitrary content types - $file = tempnam("/tmp", ""); - file_put_contents($file, $raw_data); - if (function_exists('exif_imagetype')) { - $image_code = exif_imagetype($file); - unlink($file); - } elseif (function_exists('getimagesize')) { - set_error_handler(array($this, 'muteErrorHandler')); - $info = getimagesize($file); - restore_error_handler(); - unlink($file); - if ($info == false) return false; - $image_code = $info[2]; - } else { - trigger_error("could not find exif_imagetype or getimagesize functions", E_USER_ERROR); - } - $real_content_type = image_type_to_mime_type($image_code); - if ($real_content_type != $content_type) { - // we're nice guys; if the content type is something else we - // support, change it over - if (empty($this->allowed_types[$real_content_type])) return false; - $content_type = $real_content_type; - } - // ok, it's kosher, rewrite what we need - $uri->userinfo = null; - $uri->host = null; - $uri->port = null; - $uri->fragment = null; - $uri->query = null; - $uri->path = "$content_type;base64," . base64_encode($raw_data); - return true; - } - - public function muteErrorHandler($errno, $errstr) {} - -} - diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/file.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/file.php deleted file mode 100644 index d74a3f198..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/file.php +++ /dev/null @@ -1,32 +0,0 @@ -userinfo = null; - // file:// makes no provisions for accessing the resource - $uri->port = null; - // While it seems to work on Firefox, the querystring has - // no possible effect and is thus stripped. - $uri->query = null; - return true; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/ftp.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/ftp.php deleted file mode 100644 index 0fb2abf64..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/ftp.php +++ /dev/null @@ -1,42 +0,0 @@ -query = null; - - // typecode check - $semicolon_pos = strrpos($uri->path, ';'); // reverse - if ($semicolon_pos !== false) { - $type = substr($uri->path, $semicolon_pos + 1); // no semicolon - $uri->path = substr($uri->path, 0, $semicolon_pos); - $type_ret = ''; - if (strpos($type, '=') !== false) { - // figure out whether or not the declaration is correct - list($key, $typecode) = explode('=', $type, 2); - if ($key !== 'type') { - // invalid key, tack it back on encoded - $uri->path .= '%3B' . $type; - } elseif ($typecode === 'a' || $typecode === 'i' || $typecode === 'd') { - $type_ret = ";type=$typecode"; - } - } else { - $uri->path .= '%3B' . $type; - } - $uri->path = str_replace(';', '%3B', $uri->path); - $uri->path .= $type_ret; - } - - return true; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/http.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/http.php deleted file mode 100644 index 959b8daff..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/http.php +++ /dev/null @@ -1,19 +0,0 @@ -userinfo = null; - return true; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/https.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/https.php deleted file mode 100644 index 159c2874e..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/https.php +++ /dev/null @@ -1,13 +0,0 @@ -userinfo = null; - $uri->host = null; - $uri->port = null; - // we need to validate path against RFC 2368's addr-spec - return true; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/news.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/news.php deleted file mode 100644 index 84a6748d8..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/news.php +++ /dev/null @@ -1,22 +0,0 @@ -userinfo = null; - $uri->host = null; - $uri->port = null; - $uri->query = null; - // typecode check needed on path - return true; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/nntp.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/nntp.php deleted file mode 100644 index 4ccea0dfc..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URIScheme/nntp.php +++ /dev/null @@ -1,19 +0,0 @@ -userinfo = null; - $uri->query = null; - return true; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URISchemeRegistry.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URISchemeRegistry.php deleted file mode 100644 index 576bf7b6d..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/URISchemeRegistry.php +++ /dev/null @@ -1,68 +0,0 @@ -get('URI.AllowedSchemes'); - if (!$config->get('URI.OverrideAllowedSchemes') && - !isset($allowed_schemes[$scheme]) - ) { - return; - } - - if (isset($this->schemes[$scheme])) return $this->schemes[$scheme]; - if (!isset($allowed_schemes[$scheme])) return; - - $class = 'HTMLPurifier_URIScheme_' . $scheme; - if (!class_exists($class)) return; - $this->schemes[$scheme] = new $class(); - return $this->schemes[$scheme]; - } - - /** - * Registers a custom scheme to the cache, bypassing reflection. - * @param $scheme Scheme name - * @param $scheme_obj HTMLPurifier_URIScheme object - */ - public function register($scheme, $scheme_obj) { - $this->schemes[$scheme] = $scheme_obj; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/UnitConverter.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/UnitConverter.php deleted file mode 100644 index 545d42622..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/UnitConverter.php +++ /dev/null @@ -1,254 +0,0 @@ - array( - 'px' => 3, // This is as per CSS 2.1 and Firefox. Your mileage may vary - 'pt' => 4, - 'pc' => 48, - 'in' => 288, - self::METRIC => array('pt', '0.352777778', 'mm'), - ), - self::METRIC => array( - 'mm' => 1, - 'cm' => 10, - self::ENGLISH => array('mm', '2.83464567', 'pt'), - ), - ); - - /** - * Minimum bcmath precision for output. - */ - protected $outputPrecision; - - /** - * Bcmath precision for internal calculations. - */ - protected $internalPrecision; - - /** - * Whether or not BCMath is available - */ - private $bcmath; - - public function __construct($output_precision = 4, $internal_precision = 10, $force_no_bcmath = false) { - $this->outputPrecision = $output_precision; - $this->internalPrecision = $internal_precision; - $this->bcmath = !$force_no_bcmath && function_exists('bcmul'); - } - - /** - * Converts a length object of one unit into another unit. - * @param HTMLPurifier_Length $length - * Instance of HTMLPurifier_Length to convert. You must validate() - * it before passing it here! - * @param string $to_unit - * Unit to convert to. - * @note - * About precision: This conversion function pays very special - * attention to the incoming precision of values and attempts - * to maintain a number of significant figure. Results are - * fairly accurate up to nine digits. Some caveats: - * - If a number is zero-padded as a result of this significant - * figure tracking, the zeroes will be eliminated. - * - If a number contains less than four sigfigs ($outputPrecision) - * and this causes some decimals to be excluded, those - * decimals will be added on. - */ - public function convert($length, $to_unit) { - - if (!$length->isValid()) return false; - - $n = $length->getN(); - $unit = $length->getUnit(); - - if ($n === '0' || $unit === false) { - return new HTMLPurifier_Length('0', false); - } - - $state = $dest_state = false; - foreach (self::$units as $k => $x) { - if (isset($x[$unit])) $state = $k; - if (isset($x[$to_unit])) $dest_state = $k; - } - if (!$state || !$dest_state) return false; - - // Some calculations about the initial precision of the number; - // this will be useful when we need to do final rounding. - $sigfigs = $this->getSigFigs($n); - if ($sigfigs < $this->outputPrecision) $sigfigs = $this->outputPrecision; - - // BCMath's internal precision deals only with decimals. Use - // our default if the initial number has no decimals, or increase - // it by how ever many decimals, thus, the number of guard digits - // will always be greater than or equal to internalPrecision. - $log = (int) floor(log(abs($n), 10)); - $cp = ($log < 0) ? $this->internalPrecision - $log : $this->internalPrecision; // internal precision - - for ($i = 0; $i < 2; $i++) { - - // Determine what unit IN THIS SYSTEM we need to convert to - if ($dest_state === $state) { - // Simple conversion - $dest_unit = $to_unit; - } else { - // Convert to the smallest unit, pending a system shift - $dest_unit = self::$units[$state][$dest_state][0]; - } - - // Do the conversion if necessary - if ($dest_unit !== $unit) { - $factor = $this->div(self::$units[$state][$unit], self::$units[$state][$dest_unit], $cp); - $n = $this->mul($n, $factor, $cp); - $unit = $dest_unit; - } - - // Output was zero, so bail out early. Shouldn't ever happen. - if ($n === '') { - $n = '0'; - $unit = $to_unit; - break; - } - - // It was a simple conversion, so bail out - if ($dest_state === $state) { - break; - } - - if ($i !== 0) { - // Conversion failed! Apparently, the system we forwarded - // to didn't have this unit. This should never happen! - return false; - } - - // Pre-condition: $i == 0 - - // Perform conversion to next system of units - $n = $this->mul($n, self::$units[$state][$dest_state][1], $cp); - $unit = self::$units[$state][$dest_state][2]; - $state = $dest_state; - - // One more loop around to convert the unit in the new system. - - } - - // Post-condition: $unit == $to_unit - if ($unit !== $to_unit) return false; - - // Useful for debugging: - //echo "
          n";
          -        //echo "$n\nsigfigs = $sigfigs\nnew_log = $new_log\nlog = $log\nrp = $rp\n
          \n"; - - $n = $this->round($n, $sigfigs); - if (strpos($n, '.') !== false) $n = rtrim($n, '0'); - $n = rtrim($n, '.'); - - return new HTMLPurifier_Length($n, $unit); - } - - /** - * Returns the number of significant figures in a string number. - * @param string $n Decimal number - * @return int number of sigfigs - */ - public function getSigFigs($n) { - $n = ltrim($n, '0+-'); - $dp = strpos($n, '.'); // decimal position - if ($dp === false) { - $sigfigs = strlen(rtrim($n, '0')); - } else { - $sigfigs = strlen(ltrim($n, '0.')); // eliminate extra decimal character - if ($dp !== 0) $sigfigs--; - } - return $sigfigs; - } - - /** - * Adds two numbers, using arbitrary precision when available. - */ - private function add($s1, $s2, $scale) { - if ($this->bcmath) return bcadd($s1, $s2, $scale); - else return $this->scale($s1 + $s2, $scale); - } - - /** - * Multiples two numbers, using arbitrary precision when available. - */ - private function mul($s1, $s2, $scale) { - if ($this->bcmath) return bcmul($s1, $s2, $scale); - else return $this->scale($s1 * $s2, $scale); - } - - /** - * Divides two numbers, using arbitrary precision when available. - */ - private function div($s1, $s2, $scale) { - if ($this->bcmath) return bcdiv($s1, $s2, $scale); - else return $this->scale($s1 / $s2, $scale); - } - - /** - * Rounds a number according to the number of sigfigs it should have, - * using arbitrary precision when available. - */ - private function round($n, $sigfigs) { - $new_log = (int) floor(log(abs($n), 10)); // Number of digits left of decimal - 1 - $rp = $sigfigs - $new_log - 1; // Number of decimal places needed - $neg = $n < 0 ? '-' : ''; // Negative sign - if ($this->bcmath) { - if ($rp >= 0) { - $n = bcadd($n, $neg . '0.' . str_repeat('0', $rp) . '5', $rp + 1); - $n = bcdiv($n, '1', $rp); - } else { - // This algorithm partially depends on the standardized - // form of numbers that comes out of bcmath. - $n = bcadd($n, $neg . '5' . str_repeat('0', $new_log - $sigfigs), 0); - $n = substr($n, 0, $sigfigs + strlen($neg)) . str_repeat('0', $new_log - $sigfigs + 1); - } - return $n; - } else { - return $this->scale(round($n, $sigfigs - $new_log - 1), $rp + 1); - } - } - - /** - * Scales a float to $scale digits right of decimal point, like BCMath. - */ - private function scale($r, $scale) { - if ($scale < 0) { - // The f sprintf type doesn't support negative numbers, so we - // need to cludge things manually. First get the string. - $r = sprintf('%.0f', (float) $r); - // Due to floating point precision loss, $r will more than likely - // look something like 4652999999999.9234. We grab one more digit - // than we need to precise from $r and then use that to round - // appropriately. - $precise = (string) round(substr($r, 0, strlen($r) + $scale), -1); - // Now we return it, truncating the zero that was rounded off. - return substr($precise, 0, -1) . str_repeat('0', -$scale + 1); - } - return sprintf('%.' . $scale . 'f', (float) $r); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/VarParser.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/VarParser.php deleted file mode 100644 index 68e72ae86..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/VarParser.php +++ /dev/null @@ -1,154 +0,0 @@ - self::STRING, - 'istring' => self::ISTRING, - 'text' => self::TEXT, - 'itext' => self::ITEXT, - 'int' => self::INT, - 'float' => self::FLOAT, - 'bool' => self::BOOL, - 'lookup' => self::LOOKUP, - 'list' => self::ALIST, - 'hash' => self::HASH, - 'mixed' => self::MIXED - ); - - /** - * Lookup table of types that are string, and can have aliases or - * allowed value lists. - */ - static public $stringTypes = array( - self::STRING => true, - self::ISTRING => true, - self::TEXT => true, - self::ITEXT => true, - ); - - /** - * Validate a variable according to type. Throws - * HTMLPurifier_VarParserException if invalid. - * It may return NULL as a valid type if $allow_null is true. - * - * @param $var Variable to validate - * @param $type Type of variable, see HTMLPurifier_VarParser->types - * @param $allow_null Whether or not to permit null as a value - * @return Validated and type-coerced variable - */ - final public function parse($var, $type, $allow_null = false) { - if (is_string($type)) { - if (!isset(HTMLPurifier_VarParser::$types[$type])) { - throw new HTMLPurifier_VarParserException("Invalid type '$type'"); - } else { - $type = HTMLPurifier_VarParser::$types[$type]; - } - } - $var = $this->parseImplementation($var, $type, $allow_null); - if ($allow_null && $var === null) return null; - // These are basic checks, to make sure nothing horribly wrong - // happened in our implementations. - switch ($type) { - case (self::STRING): - case (self::ISTRING): - case (self::TEXT): - case (self::ITEXT): - if (!is_string($var)) break; - if ($type == self::ISTRING || $type == self::ITEXT) $var = strtolower($var); - return $var; - case (self::INT): - if (!is_int($var)) break; - return $var; - case (self::FLOAT): - if (!is_float($var)) break; - return $var; - case (self::BOOL): - if (!is_bool($var)) break; - return $var; - case (self::LOOKUP): - case (self::ALIST): - case (self::HASH): - if (!is_array($var)) break; - if ($type === self::LOOKUP) { - foreach ($var as $k) if ($k !== true) $this->error('Lookup table contains value other than true'); - } elseif ($type === self::ALIST) { - $keys = array_keys($var); - if (array_keys($keys) !== $keys) $this->error('Indices for list are not uniform'); - } - return $var; - case (self::MIXED): - return $var; - default: - $this->errorInconsistent(get_class($this), $type); - } - $this->errorGeneric($var, $type); - } - - /** - * Actually implements the parsing. Base implementation is to not - * do anything to $var. Subclasses should overload this! - */ - protected function parseImplementation($var, $type, $allow_null) { - return $var; - } - - /** - * Throws an exception. - */ - protected function error($msg) { - throw new HTMLPurifier_VarParserException($msg); - } - - /** - * Throws an inconsistency exception. - * @note This should not ever be called. It would be called if we - * extend the allowed values of HTMLPurifier_VarParser without - * updating subclasses. - */ - protected function errorInconsistent($class, $type) { - throw new HTMLPurifier_Exception("Inconsistency in $class: ".HTMLPurifier_VarParser::getTypeName($type)." not implemented"); - } - - /** - * Generic error for if a type didn't work. - */ - protected function errorGeneric($var, $type) { - $vtype = gettype($var); - $this->error("Expected type ".HTMLPurifier_VarParser::getTypeName($type).", got $vtype"); - } - - static public function getTypeName($type) { - static $lookup; - if (!$lookup) { - // Lazy load the alternative lookup table - $lookup = array_flip(HTMLPurifier_VarParser::$types); - } - if (!isset($lookup[$type])) return 'unknown'; - return $lookup[$type]; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/VarParser/Flexible.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/VarParser/Flexible.php deleted file mode 100644 index 21b87675a..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/VarParser/Flexible.php +++ /dev/null @@ -1,103 +0,0 @@ - $j) $var[$i] = trim($j); - if ($type === self::HASH) { - // key:value,key2:value2 - $nvar = array(); - foreach ($var as $keypair) { - $c = explode(':', $keypair, 2); - if (!isset($c[1])) continue; - $nvar[trim($c[0])] = trim($c[1]); - } - $var = $nvar; - } - } - if (!is_array($var)) break; - $keys = array_keys($var); - if ($keys === array_keys($keys)) { - if ($type == self::ALIST) return $var; - elseif ($type == self::LOOKUP) { - $new = array(); - foreach ($var as $key) { - $new[$key] = true; - } - return $new; - } else break; - } - if ($type === self::ALIST) { - trigger_error("Array list did not have consecutive integer indexes", E_USER_WARNING); - return array_values($var); - } - if ($type === self::LOOKUP) { - foreach ($var as $key => $value) { - if ($value !== true) { - trigger_error("Lookup array has non-true value at key '$key'; maybe your input array was not indexed numerically", E_USER_WARNING); - } - $var[$key] = true; - } - } - return $var; - default: - $this->errorInconsistent(__CLASS__, $type); - } - $this->errorGeneric($var, $type); - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/VarParser/Native.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/VarParser/Native.php deleted file mode 100644 index b02a6de54..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/VarParser/Native.php +++ /dev/null @@ -1,26 +0,0 @@ -evalExpression($var); - } - - protected function evalExpression($expr) { - $var = null; - $result = eval("\$var = $expr;"); - if ($result === false) { - throw new HTMLPurifier_VarParserException("Fatal error in evaluated code"); - } - return $var; - } - -} - -// vim: et sw=4 sts=4 diff --git a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/VarParserException.php b/src/Classes/Vendor/htmlpurifier/HTMLPurifier/VarParserException.php deleted file mode 100644 index 5df341495..000000000 --- a/src/Classes/Vendor/htmlpurifier/HTMLPurifier/VarParserException.php +++ /dev/null @@ -1,11 +0,0 @@ - $value ) - $req .= $key . '=' . urlencode( stripslashes($value) ) . '&'; - - // Cut the last '&' - $req=substr($req,0,strlen($req)-1); - return $req; -} - - - -/** - * Submits an HTTP POST to a reCAPTCHA server - * @param string $host - * @param string $path - * @param array $data - * @param int port - * @return array response - */ -function _recaptcha_http_post($host, $path, $data, $port = 80) { - - $req = _recaptcha_qsencode ($data); - - $http_request = "POST $path HTTP/1.0\r\n"; - $http_request .= "Host: $host\r\n"; - $http_request .= "Content-Type: application/x-www-form-urlencoded;\r\n"; - $http_request .= "Content-Length: " . strlen($req) . "\r\n"; - $http_request .= "User-Agent: reCAPTCHA/PHP\r\n"; - $http_request .= "\r\n"; - $http_request .= $req; - - $response = ''; - if( false == ( $fs = @fsockopen($host, $port, $errno, $errstr, 10) ) ) { - die ('Could not open socket'); - } - - fwrite($fs, $http_request); - - while ( !feof($fs) ) - $response .= fgets($fs, 1160); // One TCP-IP packet - fclose($fs); - $response = explode("\r\n\r\n", $response, 2); - - return $response; -} - - - -/** - * Gets the challenge HTML (javascript and non-javascript version). - * This is called from the browser, and the resulting reCAPTCHA HTML widget - * is embedded within the HTML form it was called from. - * @param string $pubkey A public key for reCAPTCHA - * @param string $error The error given by reCAPTCHA (optional, default is null) - * @param boolean $use_ssl Should the request be made over ssl? (optional, default is false) - - * @return string - The HTML to be embedded in the user's form. - */ -function recaptcha_get_html ($pubkey, $error = null, $use_ssl = false) -{ - if ($pubkey == null || $pubkey == '') { - die ("To use reCAPTCHA you must get an API key from https://www.google.com/recaptcha/admin/create"); - } - - if ($use_ssl) { - $server = RECAPTCHA_API_SECURE_SERVER; - } else { - $server = RECAPTCHA_API_SERVER; - } - - $errorpart = ""; - if ($error) { - $errorpart = "&error=" . $error; - } - return ' - - '; -} - - - - -/** - * A ReCaptchaResponse is returned from recaptcha_check_answer() - */ -class ReCaptchaResponse { - var $is_valid; - var $error; -} - - -/** - * Calls an HTTP POST function to verify if the user's guess was correct - * @param string $privkey - * @param string $remoteip - * @param string $challenge - * @param string $response - * @param array $extra_params an array of extra variables to post to the server - * @return ReCaptchaResponse - */ -function recaptcha_check_answer ($privkey, $remoteip, $challenge, $response, $extra_params = array()) -{ - if ($privkey == null || $privkey == '') { - die ("To use reCAPTCHA you must get an API key from https://www.google.com/recaptcha/admin/create"); - } - - if ($remoteip == null || $remoteip == '') { - die ("For security reasons, you must pass the remote ip to reCAPTCHA"); - } - - - - //discard spam submissions - if ($challenge == null || strlen($challenge) == 0 || $response == null || strlen($response) == 0) { - $recaptcha_response = new ReCaptchaResponse(); - $recaptcha_response->is_valid = false; - $recaptcha_response->error = 'incorrect-captcha-sol'; - return $recaptcha_response; - } - - $response = _recaptcha_http_post (RECAPTCHA_VERIFY_SERVER, "/recaptcha/api/verify", - array ( - 'privatekey' => $privkey, - 'remoteip' => $remoteip, - 'challenge' => $challenge, - 'response' => $response - ) + $extra_params - ); - - $answers = explode ("\n", $response [1]); - $recaptcha_response = new ReCaptchaResponse(); - - if (trim ($answers [0]) == 'true') { - $recaptcha_response->is_valid = true; - } - else { - $recaptcha_response->is_valid = false; - $recaptcha_response->error = $answers [1]; - } - return $recaptcha_response; - -} - -/** - * gets a URL where the user can sign up for reCAPTCHA. If your application - * has a configuration page where you enter a key, you should provide a link - * using this function. - * @param string $domain The domain where the page is hosted - * @param string $appname The name of your application - */ -function recaptcha_get_signup_url ($domain = null, $appname = null) { - return "https://www.google.com/recaptcha/admin/create?" . _recaptcha_qsencode (array ('domains' => $domain, 'app' => $appname)); -} - -function _recaptcha_aes_pad($val) { - $block_size = 16; - $numpad = $block_size - (strlen ($val) % $block_size); - return str_pad($val, strlen ($val) + $numpad, chr($numpad)); -} - -/* Mailhide related code */ - -function _recaptcha_aes_encrypt($val,$ky) { - if (! function_exists ("mcrypt_encrypt")) { - die ("To use reCAPTCHA Mailhide, you need to have the mcrypt php module installed."); - } - $mode=MCRYPT_MODE_CBC; - $enc=MCRYPT_RIJNDAEL_128; - $val=_recaptcha_aes_pad($val); - return mcrypt_encrypt($enc, $ky, $val, $mode, "\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0"); -} - - -function _recaptcha_mailhide_urlbase64 ($x) { - return strtr(base64_encode ($x), '+/', '-_'); -} - -/* gets the reCAPTCHA Mailhide url for a given email, public key and private key */ -function recaptcha_mailhide_url($pubkey, $privkey, $email) { - if ($pubkey == '' || $pubkey == null || $privkey == "" || $privkey == null) { - die ("To use reCAPTCHA Mailhide, you have to sign up for a public and private key, " . - "you can do so at http://www.google.com/recaptcha/mailhide/apikey"); - } - - - $ky = pack('H*', $privkey); - $cryptmail = _recaptcha_aes_encrypt ($email, $ky); - - return "http://www.google.com/recaptcha/mailhide/d?k=" . $pubkey . "&c=" . _recaptcha_mailhide_urlbase64 ($cryptmail); -} - -/** - * gets the parts of the email to expose to the user. - * eg, given johndoe@example,com return ["john", "example.com"]. - * the email is then displayed as john...@example.com - */ -function _recaptcha_mailhide_email_parts ($email) { - $arr = preg_split("/@/", $email ); - - if (strlen ($arr[0]) <= 4) { - $arr[0] = substr ($arr[0], 0, 1); - } else if (strlen ($arr[0]) <= 6) { - $arr[0] = substr ($arr[0], 0, 3); - } else { - $arr[0] = substr ($arr[0], 0, 4); - } - return $arr; -} - -/** - * Gets html to display an email address given a public an private key. - * to get a key, go to: - * - * http://www.google.com/recaptcha/mailhide/apikey - */ -function recaptcha_mailhide_html($pubkey, $privkey, $email) { - $emailparts = _recaptcha_mailhide_email_parts ($email); - $url = recaptcha_mailhide_url ($pubkey, $privkey, $email); - - return htmlentities($emailparts[0]) . "...@" . htmlentities ($emailparts [1]); - -} - - -?> diff --git a/src/Classes/iTones/iTones_Playlist.php b/src/Classes/iTones/iTones_Playlist.php index 5cecd9a9d..501dc724f 100644 --- a/src/Classes/iTones/iTones_Playlist.php +++ b/src/Classes/iTones/iTones_Playlist.php @@ -1,317 +1,867 @@ - * @package MyRadio_iTones - * @uses \Database + * The iTones_Playlist class helps provide control and access to managed playlists. + * + * @uses \Database */ -class iTones_Playlist extends ServiceAPI { - private $playlistid; - private $title; - private $image; - private $description; - private $lock; - private $locktime; - protected $tracks = array(); - private $weight = 0; - protected $revisionid; - - /** - * Initiates the ManagedPlaylist variables - * @param int $playlistid The ID of the managed playlist to initialise - * Note: Only links *non-expired* items - */ - protected function __construct($playlistid) { - $this->playlistid = $playlistid; - $result = self::$db->fetch_one('SELECT * FROM jukebox.playlists WHERE playlistid=$1 LIMIT 1', array($playlistid)); - if (empty($result)) { - throw new MyRadioException('The specified iTones Playlist does not seem to exist'); - return; +class iTones_Playlist extends \MyRadio\ServiceAPI\ServiceAPI +{ + private $playlistid; + private $title; + private $image; + private $description; + private $lock; + private $locktime; + protected $tracks = []; + protected $revisionid; + private $categoryid; + private $archived; + + /** + * Initiates the ManagedPlaylist variables. + * + * @param string $playlistid The ID of the managed playlist to initialise + * Note: Only links *non-expired* items + */ + protected function __construct($playlistid) + { + $this->playlistid = $playlistid; + $result = self::$db->fetchOne('SELECT * FROM jukebox.playlists WHERE playlistid=$1 LIMIT 1', [$playlistid]); + if (empty($result)) { + throw new MyRadioException('The specified iTones Playlist does not seem to exist', 404); + + return; + } + + $this->title = $result['title']; + $this->image = $result['image']; + $this->description = $result['description']; + $this->lock = empty($result['lock']) ? null : MyRadio_User::getInstance($result['lock']); + $this->locktime = (int) $result['locktime']; + $this->categoryid = (int) $result['category']; + if (is_null($this->categoryid)) { + throw new MyRadioException('Playlist ' . $playlistid . ' has a null category!', 500); + } + + $this->revisionid = (int) self::$db->fetchOne( + 'SELECT revisionid FROM jukebox.playlist_revisions + WHERE playlistid=$1 ORDER BY revisionid DESC LIMIT 1', + [$this->getID()] + )['revisionid']; + + $this->archived = ($result['archived'] == 't'); + } + + public static function getTracksForm() + { + return (new MyRadioForm( + 'itones_playlistedit', + 'iTones', + 'editPlaylist', + [ + 'title' => 'Edit Campus Jukebox Playlist', + ] + ))->addField( + new MyRadioFormField( + 'tracks', + MyRadioFormField::TYPE_TABULARSET, + [ + 'options' => [ + new MyRadioFormField( + 'track', + MyRadioFormField::TYPE_TRACK, + [ + 'label' => 'Tracks', + 'options' => [ + 'digitised' => true, + ], + ] + ), + new MyRadioFormField( + 'artist', + MyRadioFormField::TYPE_ARTIST, + [ + 'label' => 'Artists', + ] + ), + ], + ] + ) + )->addField( + new MyRadioFormField( + 'notes', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Notes', + 'explanation' => 'Optional. Enter notes about this change.', + 'required' => false, + ] + ) + ); + } + + public function getTracksEditForm() + { + return self::getTracksForm() + ->setTitle('Edit Playlist') + ->editMode( + $this->getID(), + [ + 'tracks.track' => $this->getTracks(), + 'tracks.artist' => array_map( + function ($track) { + return $track->getArtist(); + }, + $this->getTracks() + ), + ] + ); + } + + public static function getForm() + { + return (new MyRadioForm( + 'itones_playlistedit', + 'iTones', + 'configurePlaylist', + [ + 'title' => 'Configure Jukebox Playlist', + ] + ))->addField( + new MyRadioFormField( + 'title', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Name', + 'explanation' => 'Name the playlist. I named my last playlist Scott.', + 'required' => true, + ] + ) + )->addField( + new MyRadioFormField( + 'category', + MyRadioFormField::TYPE_SELECT, + [ + 'label' => 'Category', + 'explanation' => 'Set the category for this playlist', + 'options' => iTones_PlaylistCategory::getOptions() + ] + ) + )->addField( + new MyRadioFormField( + 'description', + MyRadioFormField::TYPE_BLOCKTEXT, + [ + 'label' => 'Description', + 'explanation' => 'What is this playlist even for?', + 'required' => false, + ] + ) + )->addField( + new MyRadioFormField( + 'archived', + MyRadioFormField::TYPE_CHECK, + [ + 'label' => 'Archived', + 'explanation' => "An archived playlist won't be in the jukebox rotation, or be + availble to presenters. It can be unarchived by reaching it through the 'All Podcasts' page", + 'required' => false + ] + ) + ); + } + + public function getEditForm() + { + return self::getForm() + ->setTitle('Configure Playlist') + ->editMode( + $this->getID(), + [ + 'title' => $this->getTitle(), + 'description' => $this->getDescription(), + 'category' => $this->getCategory()->getID(), + 'archived' => $this->isArchived() + ] + ); + } + + /** + * Return the MyRadio_Tracks that belong to this playlist. + * + * @return array of MyRadio_Track objects + */ + public function getTracks() + { + if (empty($this->tracks)) { + $items = self::$db->fetchColumn( + 'SELECT trackid FROM jukebox.playlist_entries WHERE playlistid=$1 + AND revision_removed IS NULL + ORDER BY entryid', + [$this->playlistid] + ); + + foreach ($items as $id) { + $this->tracks[] = MyRadio_Track::getInstance($id); + } + } + + return $this->tracks; + } + + /** + * Get the Title of the Playlist. + * + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Get the unique playlistid of the Playlist. + * + * @return string + */ + public function getID() + { + return $this->playlistid; + } + + /** + * Get the long description of the Playlist. + * + * @return string + */ + public function getDescription() + { + return $this->description; + } + + /** + * Get the category of this playlist. + * @return iTones_PlaylistCategory + */ + public function getCategory() + { + return iTones_PlaylistCategory::getInstance($this->categoryid); + } + + /** + * Get the current Revision ID of the Playlist. + * + * @return int + */ + public function getRevisionID() + { + return $this->revisionid; + } + + /** + * Is the playlist archived? + * + * @return bool + * + */ + + public function isArchived() + { + return $this->archived; } - $this->title = $result['title']; - $this->image = $result['image']; - $this->description = $result['description']; - $this->lock = empty($result['lock']) ? null : MyRadio_User::getInstance($result['lock']); - $this->locktime = (int) $result['locktime']; - $this->weight = (int) $result['weight']; - - $this->revisionid = (int) self::$db->fetch_one('SELECT revisionid FROM jukebox.playlist_revisions - WHERE playlistid=$1 ORDER BY revisionid DESC LIMIT 1', array($this->getID()))['revisionid']; - } - - /** - * Return the MyRadio_Tracks that belong to this playlist - * @return Array of MyRadio_Track objects - */ - public function getTracks() { - if (empty($this->tracks)) { - $items = self::$db->fetch_column('SELECT trackid FROM jukebox.playlist_entries WHERE playlistid=$1 - AND revision_removed IS NULL - ORDER BY entryid', array($this->playlistid)); - - foreach ($items as $id) { - $this->tracks[] = MyRadio_Track::getInstance($id); - } + /** + * Takes a lock on this playlist - stores a notification to all other systems that it should not be edited. + * + * @param string $lockstr If you already have a lock, put it here. It will be renewed if it is still valid. + * @param MyRadio_User $user The user that has acquired the lock. Defaults to current user. + * Required for CLI requests. This String will be invalidated by the update. + * + * @return bool|string false if the lock is not available, or a sha1 that proves ownership of the lock. + * No, the hash isn't all that fancy, but it prevents people being stupid. + * Write operations require this String. + */ + public function acquireOrRenewLock($lockstr = null, MyRadio_User $user = null) + { + if ($user === null) { + $user = MyRadio_User::getInstance(); + } + //Acquire a lock on the lock row - we don't want someone else acquiring a lock while we are! + self::$db->query('BEGIN'); + self::$db->query('SELECT * FROM jukebox.playlists WHERE playlistid=$1 FOR UPDATE', [$this->getID()]); + + //Refresh the local lock information - threads using this could have been running for a *while* + $this->refreshLockInformation(); + + if ($this->locktime >= time() - Config::$playlist_lock_time) { + //There's a lock in place. Is it held by this client? + if ($lockstr !== $this->generateLockKey($this->lock, $this->locktime)) { + //It is not. Return false. + return false; + } + //It's held by this user, we can update it. + } + //Or, if there isn't an active lock + $locktime = time(); + self::$db->query( + 'UPDATE jukebox.playlists SET lock=$1, locktime=$2 WHERE playlistid=$3', + [$user->getID(), $locktime, $this->getID()] + ); + self::$db->query('COMMIT'); //This releases the lock + $this->refreshLockInformation(); + + return $this->generateLockKey($user, $locktime); } - return $this->tracks; - } - - /** - * Get the Title of the Playlist - * @return String - */ - public function getTitle() { - return $this->title; - } - - /** - * Get the unique playlistid of the Playlist - * @return String - */ - public function getID() { - return $this->playlistid; - } - - /** - * Get the long description of the Playlist - * @return string - */ - public function getDescription() { - return $this->description; - } - - /** - * Get the jukebox weight of the Playlist - * @return int - */ - public function getWeight() { - return $this->weight; - } - - /** - * Get the current Revision ID of the Playlist - * @return int - */ - public function getRevisionID() { - return $this->revisionid; - } - - /** - * Takes a lock on this playlist - stores a notification to all other systems that it should not be edited. - * - * @param String $lockstr If you already have a lock, put it here. It will be renewed if it is still valid. - * @param MyRadio_User $user The user that has acquired the lock. Defaults to current user. Required for CLI requests. - * This String will be invalidated by the update. - * - * @return bool|String false if the lock is not available, or a sha1 that proves ownership of the lock. - * No, the hash isn't all that fancy, but it prevents people being stupid. Write operations require this String. - */ - public function acquireOrRenewLock($lockstr = null, MyRadio_User $user = null) { - if ($user === null) - $user = MyRadio_User::getInstance(); - //Acquire a lock on the lock row - we don't want someone else acquiring a lock while we are! - self::$db->query('BEGIN'); - self::$db->query('SELECT * FROM jukebox.playlists WHERE playlistid=$1 FOR UPDATE', array($this->getID()), true); - - //Refresh the local lock information - threads using this could have been running for a *while* - $this->refreshLockInformation(); - - if ($this->locktime >= time() - Config::$playlist_lock_time) { - //There's a lock in place. Is it held by this client? - if ($lockstr !== $this->generateLockKey($this->lock, $this->locktime)) { - //It is not. Return false. - return false; - } - //It's held by this user, we can update it. + + /** + * Release your lock on this Playlist. + * + * @param string $lockstr + */ + public function releaseLock($lockstr) + { + if ($this->validateLock($lockstr)) { + self::$db->query('UPDATE jukebox.playlists SET locktime=NULL WHERE playlistid=$1', [$this->getID()]); + } } - //Or, if there isn't an active lock - $locktime = time(); - self::$db->query('UPDATE jukebox.playlists SET lock=$1, locktime=$2 WHERE playlistid=$3', array($user->getID(), $locktime, $this->getID()), true); - self::$db->query('COMMIT'); //This releases the lock - $this->refreshLockInformation(); - return $this->generateLockKey($user, $locktime); - } - - /** - * Release your lock on this Playlist - * @param String $lockstr - */ - public function releaseLock($lockstr) { - if ($this->validateLock($lockstr)) { - self::$db->query('UPDATE jukebox.playlists SET locktime=NULL WHERE playlistid=$1', array($this->getID())); + + /** + * Updates the locally stored Lock information to ensure it is up to date. + */ + private function refreshLockInformation() + { + $result = self::$db->fetchOne( + 'SELECT lock, locktime FROM jukebox.playlists WHERE playlistid=$1', + [$this->getID()] + ); + $this->lock = empty($result['lock']) ? null : MyRadio_User::getInstance($result['lock']); + $this->locktime = (int) $result['locktime']; } - } - - /** - * Updates the locally stored Lock information to ensure it is up to date - */ - private function refreshLockInformation() { - $result = self::$db->fetch_one('SELECT lock, locktime FROM jukebox.playlists WHERE playlistid=$1', array($this->getID())); - $this->lock = empty($result['lock']) ? null : MyRadio_User::getInstance($result['lock']); - $this->locktime = (int) $result['locktime']; - } - - /** - * Generates a key to the provided lock - * @param MyRadio_User $lock - * @param int $locktime - * @return String - */ - private function generateLockKey(MyRadio_User $lock, $locktime) { - return sha1('myuryitoneslockkey' . $lock->__toString() . $locktime . $this->getID()); - } - - /** - * Returns if the provided Lock string is valid for this Playlist - * @param String $lockstr - * @return bool - */ - public function validateLock($lockstr) { - $this->refreshLockInformation(); - return $lockstr === $this->generateLockKey($this->lock, $this->locktime); - } - - /** - * Update the Tracks that belong to this playlist. - * - * It gets a list of all tracks in the Playlist, then iterates over each Track in $tracks - * - If the Track is in the existing list, remove it from the temporary list - * - If the Track is not in the list, INSERT it into the database from the current revision - * - * Once that's done, go over every Track still in the temporary list and remove them from the Playlist - * - * @param MyRadio_Track[] $tracks Tracks to put in the playlist. - * @param String $lockstr The string that provides Write access to this Playlist. Acquired from acquireLock(); - * @param String $notes Optional. A textual commit message about the change. - * - * @todo Push these changes to the playlist files on playoutsvc.ury.york.ac.uk. This should probably be a MyRadioDaemon - * configured to run only on that server. - */ - public function setTracks($tracks, $lockstr, $notes = null, MyRadio_User $user = null) { - if ($user === null) { - $user = MyRadio_User::getInstance(); - } - //Remove duplicates - $tracks = array_unique($tracks); - $old_list = $this->getTracks(); - - //Check if anything has actually changed - if ($tracks == $old_list) { - return; + + /** + * Generates a key to the provided lock. + * + * @param MyRadio_User $lock + * @param int $locktime + * + * @return string + */ + private function generateLockKey(MyRadio_User $lock, $locktime) + { + return sha1('myradioitoneslockkey' . $lock->__toString() . $locktime . $this->getID()); } - //Okay, it has. They'll need a lock to go any further - if (!$this->validateLock($lockstr)) { - throw new MyRadioException('You do not have a valid lock on this playlist.'); + /** + * Returns if the provided Lock string is valid for this Playlist. + * + * @param string $lockstr + * + * @return bool + */ + public function validateLock($lockstr) + { + $this->refreshLockInformation(); + + return $lockstr === $this->generateLockKey($this->lock, $this->locktime); } - $new_additions = array(); - - foreach ($tracks as $track) { - $key = array_search($track, $old_list); - if ($key === false) { - //This is a new addition - $new_additions[] = $track; - } else { - //This is an existing item - unset($old_list[$key]); - } + /** + * Update the Tracks that belong to this playlist. + * + * It gets a list of all tracks in the Playlist, then iterates over each Track in $tracks + * - If the Track is in the existing list, remove it from the temporary list + * - If the Track is not in the list, INSERT it into the database from the current revision + * + * Once that's done, go over every Track still in the temporary list and remove them from the Playlist + * + * @param MyRadio_Track[]|int[] $tracks Tracks to put in the playlist. + * @param string $lockstr String that provides Write access to this Playlist. + * Acquired from acquireLock(); + * @param string|null $notes Optional. A textual commit message about the change. + * + * @todo Push these changes to the playlist files on playoutsvc.ury.york.ac.uk. This should probably be a + * MyRadioDaemon configured to run only on that server. + */ + public function setTracks($tracks, $lockstr, $notes = null) + { + $user = MyRadio_User::getCurrentOrSystemUser(); + foreach ($tracks as $idx => $track) { + if (!($track instanceof MyRadio_Track)) { + try { + $tracks[$idx] = MyRadio_Track::getInstance($track); + } catch (\Exception $e) { + // (blame Matt Strat if any of this breaks) + $tracks[$idx] = null; + continue; + } + } + + // Remove any undigitised tracks + if (!$track->getDigitised()) { + $tracks[$idx] = null; + } + } + + //Remove duplicates + $tracks = array_unique($tracks); + $old_list = $this->getTracks(); + + //Check if anything has actually changed + if ($tracks == $old_list) { + return; + } + + //Okay, it has. They'll need a lock to go any further + if (!$this->validateLock($lockstr)) { + throw new MyRadioException('You do not have a valid lock on this playlist.'); + } + + $new_additions = []; + + foreach ($tracks as $i => $track) { + if (empty($track)) { + unset($tracks[$i]); + } + $key = array_search($track, $old_list); + if ($key === false) { + //This is a new addition + $new_additions[] = $track; + } else { + //This is an existing item + unset($old_list[$key]); + } + } + + //Cool, now we know what needs to be done. + self::$db->query('BEGIN'); + $revisionid = $this->getRevisionID() + 1; + //Get the new revision ID + self::$db->query( + 'INSERT INTO jukebox.playlist_revisions (playlistid, revisionid, author, notes) + VALUES ($1, $2, $3, $4) RETURNING revisionid', + [$this->getID(), $revisionid, $user->getID(), $notes] + ); + //Add new tracks + foreach ($new_additions as $track) { + if (!$track instanceof MyRadio_Track) { + trigger_error('Discarding non-track item: ' . print_r($track, true)); + continue; + } + self::$db->query( + 'INSERT INTO jukebox.playlist_entries (playlistid, trackid, revision_added) VALUES ($1, $2, $3)', + [$this->getID(), $track->getID(), $revisionid] + ); + } + //Remove old tracks + foreach ($old_list as $track) { + if ($track instanceof MyRadio_Track) { + self::$db->query( + 'UPDATE jukebox.playlist_entries SET revision_removed=$1 WHERE playlistid=$2 AND trackid=$3 + AND revision_removed IS NULL', + [$revisionid, $this->getID(), $track->getID()] + ); + } + } + //All is happy. Commit! + self::$db->query('COMMIT'); + $this->tracks = $tracks; + $this->revisionid = $revisionid; + $this->updateCacheObject(); } - //Cool, now we know what needs to be done. - self::$db->query('BEGIN'); - $revisionid = $this->getRevisionID() + 1; - //Get the new revision ID - self::$db->query('INSERT INTO jukebox.playlist_revisions (playlistid, revisionid, author, notes) - VALUES ($1, $2, $3, $4) RETURNING revisionid', array($this->getID(), $revisionid, $user->getID(), $notes), true); - //Add new tracks - foreach ($new_additions as $track) { - if (empty($track)) { - continue; - } - self::$db->query('INSERT INTO jukebox.playlist_entries (playlistid, trackid, revision_added) VALUES ($1, $2, $3)', array($this->getID(), $track->getID(), $revisionid), true); + /** + * Update the title. + * + * @param string $title + */ + public function setTitle($title) + { + self::$db->query('UPDATE jukebox.playlists SET title=$1 WHERE playlistid=$2', [$title, $this->getID()]); + $this->title = $title; + $this->updateCacheObject(); } - //Remove old tracks - foreach ($old_list as $track) { - if ($track instanceof MyRadio_Track) { - self::$db->query('UPDATE jukebox.playlist_entries SET revision_removed=$1 WHERE playlistid=$2 AND trackid=$3 - AND revision_removed IS NULL', array($revisionid, $this->getID(), $track->getID()), true); - } + + /** + * Update the description. + * + * @param string $description + */ + public function setDescription($description) + { + self::$db->query( + 'UPDATE jukebox.playlists SET description=$1 WHERE playlistid=$2', + [$description, $this->getID()] + ); + $this->description = $description; + $this->updateCacheObject(); } - //All is happy. Commit! - self::$db->query('COMMIT'); - $this->tracks = $tracks; - $this->revisionid = $revisionid; - $this->updateCacheObject(); - } - - /** - * Get an array of all Playlists - * @return Array of iTones_Playlist objects - */ - public static function getAlliTonesPlaylists() { - self::wakeup(); - $result = self::$db->fetch_column('SELECT playlistid FROM jukebox.playlists ORDER BY title'); - - return self::resultSetToObjArray($result); - } - - /** - * Uses weighted playout values to select a random Playlist, returning it. - * @return iTones_Playlist - */ - public static function getPlaylistFromWeights() { - self::wakeup(); - - $result = self::$db->fetch_all('SELECT playlistid AS item, weight FROM jukebox.playlists ORDER BY title'); - - return self::getInstance(CoreUtils::biased_random($result)); - } - - /** - * Find out what Playlists have this Track in them, if any - * @param MyRadio_Track $track The track to search for - * @return Array One or more iTones_Playlists, each of which contain $track - */ - public static function getPlaylistsWithTrack(MyRadio_Track $track) { - $result = self::$db->fetch_column('SELECT playlistid FROM jukebox.playlist_entries WHERE trackid=$1 - AND revision_removed IS NULL', array($track->getID())); - - return self::resultSetToObjArray($result); - } - - /** - * Returns an array of key information, useful for Twig rendering and JSON requests - * @todo Expand the information this returns - * @return Array - */ - public function toDataSource() { - return array( - 'title' => $this->getTitle(), - 'playlistid' => $this->getID(), - 'description' => $this->getDescription(), - 'edittrackslink' => array('display' => 'icon', - 'value' => 'folder-open', - 'title' => 'Edit Tracks in this playlist', - 'url' => CoreUtils::makeURL('iTones', 'editPlaylist', array('playlistid' => $this->getID()))), - 'configurelink' => array('display' => 'icon', - 'value' => 'wrench', - 'title' => 'Alter playlist settings', - 'url' => CoreUtils::makeURL('iTones', 'configurePlaylist', array('playlistid' => $this->getID()))), - 'revisionslink' => array('display' => 'icon', - 'value' => 'clock', - 'title' => 'View revision history', - 'url' => CoreUtils::makeURL('iTones', 'viewPlaylistHistory', array('playlistid' => $this->getID()))) - ); - } + /** + * Update the archival state of a playlist + * + * @param bool $archived + */ + + public function setArchived($archived) + { + self::$db->query( + "UPDATE jukebox.playlists SET archived=$1 WHERE playlistid=$2", + [$archived, $this->getID()] + ); + $this->archived = $archived; + $this->updateCacheObject(); + } + + /** + * Is this playlist available right now? + * @return boolean + */ + public function isAvailable() + { + if ($this->archived) { + return false; + } + + $result = self::$db->fetchOne('select count(*) AS valid + from jukebox.playlists + inner join jukebox.playlist_availability on playlists.playlistid = playlist_availability.playlistid + inner join jukebox.playlist_timeslot + on playlist_availability.playlist_availability_id = playlist_timeslot.playlist_availability_id + where playlists.playlistid = $1 + and playlist_availability.effective_from <= NOW() + and (playlist_availability.effective_to is null or playlist_availability.effective_to >= NOW()) + and ( + day=EXTRACT(DOW FROM NOW()) + or (EXTRACT(DOW FROM NOW())=0 and day=7) + ) + and start_time <= "time"(NOW()) + and end_time >= "time"(NOW())', [$this->getID()]); + + return $result['valid'] > 0; + } + + /** + * Update the category. + * @param $category + */ + public function setCategoryById($category) + { + if (!is_int($category)) { + throw new MyRadioException('Expected $category to be an integer'); + } + self::$db->query( + 'UPDATE jukebox.playlists SET category=$1 WHERE playlistid=$2', + [$category, $this->getID()] + ); + $this->categoryid = $category; + $this->updateCacheObject(); + } + + /** + * Get an array of all Playlists. + * + * @param bool $includeArchived whether to include archived playlists (default false) + * + * @return array of iTones_Playlist objects + */ + public static function getAlliTonesPlaylists($includeArchived = false) + { + $where = ""; + if (!$includeArchived) { + $where = "WHERE archived = false"; + } + + self::wakeup(); + $result = self::$db->fetchColumn("SELECT playlistid FROM jukebox.playlists $where ORDER BY title"); + + return self::resultSetToObjArray($result); + } + + /** + * Get all the playlists that are available right now + * + * @param bool $includeArchived whether to include archived playlists (default false) + * + * @return array of iTones_Playlist objects + */ + public static function getAllAvailablePlaylists($includeArchived = false) + { + $where = ""; + if (!$includeArchived) { + $where = "AND archived = false"; + } + + self::wakeup(); + $result = self::$db->fetchColumn("select playlists.playlistid + from jukebox.playlists + inner join jukebox.playlist_availability on playlists.playlistid = playlist_availability.playlistid + inner join jukebox.playlist_timeslot + on playlist_availability.playlist_availability_id = playlist_timeslot.playlist_availability_id + where playlist_availability.effective_from <= NOW() + and (playlist_availability.effective_to is null or playlist_availability.effective_to >= NOW()) + and ( + day=EXTRACT(DOW FROM NOW()) + or (EXTRACT(DOW FROM NOW())=0 and day=7) + ) + and start_time <= \"time\"(NOW()) + and end_time >= \"time\"(NOW()) $where"); + return self::resultSetToObjArray($result); + } + + /** + * Uses weighted playout values to select a random Playlist, returning it. + * + * Only includes Playlists with a currently running slot, and a Track. + * + * @param iTones_Playlist[] A list of one or more playlists to not return. + * @param bool $includeArchived whether to include archived playlists (default false) + * @throws MyRadioException If no playlists are available. + * + * @return iTones_Playlist + */ + public static function getPlaylistFromWeights($playlists_to_ignore = [], $includeArchived = false) + { + $where = ""; + if (!$includeArchived) { + $where = "AND archived = false"; + } + + self::wakeup(); + + $result = self::$db->fetchAll( + "SELECT playlists.playlistid AS item, MAX(playlist_availability.weight) AS weight + FROM jukebox.playlists, jukebox.playlist_availability, jukebox.playlist_timeslot + WHERE playlists.playlistid=playlist_availability.playlistid + AND playlist_availability.playlist_availability_id=playlist_timeslot.playlist_availability_id + AND effective_from <= NOW() + AND (effective_to IS NULL OR effective_to >= NOW()) + AND start_time <= \"time\"(NOW()) + AND end_time >= \"time\"(NOW()) + AND ( + day=EXTRACT(DOW FROM NOW()) + OR (EXTRACT(DOW FROM NOW())=0 AND day=7) + ) + AND EXISTS ( + SELECT 1 + FROM jukebox.playlist_entries + WHERE playlistid=jukebox.playlists.playlistid + AND revision_removed IS NULL + LIMIT 1 + ) + $where + GROUP BY playlists.playlistid" + ); + + if (!sizeof($result)) { + throw new MyRadioException('No weighted playlists currently available.'); + } + + for ($i = 0; $i < sizeof($result); $i++) { + foreach ($playlists_to_ignore as $playlist) { + if ($result[$i]['item'] === $playlist->getID()) { + unset($result[$i]); + break; + } + } + } + + return self::getInstance(CoreUtils::biasedRandom($result)); + } + + /** + * Uses weighted playout values to select a random Playlist from a category, returning it. + * + * Only includes Playlists with a currently running slot, and a Track. + * + * @param int $categoryId + * @param array $playlists_to_ignore one or more playlists to not return + * @param bool $includeArchived whether to include archived playlists (default false) + * @return iTones_Playlist + */ + public static function getPlaylistOfCategoryFromWeights( + $categoryId, + $playlists_to_ignore = [], + $includeArchived = false + ) { + if (!is_int($categoryId)) { + throw new MyRadioException('Expected $categoryId to be an integer'); + } + // TODO: this is a straight copy-paste of the above. If we need to do this again, + // consider refactoring. + $where = ""; + if (!$includeArchived) { + $where = "AND archived = false"; + } + self::wakeup(); + + $result = self::$db->fetchAll( + "SELECT playlists.playlistid AS item, MAX(playlist_availability.weight) AS weight + FROM jukebox.playlists, jukebox.playlist_availability, jukebox.playlist_timeslot + WHERE playlists.category = $1 + AND playlists.playlistid=playlist_availability.playlistid + AND playlist_availability.playlist_availability_id=playlist_timeslot.playlist_availability_id + AND effective_from <= NOW() + AND (effective_to IS NULL OR effective_to >= NOW()) + AND start_time <= \"time\"(NOW()) + AND end_time >= \"time\"(NOW()) + AND ( + day=EXTRACT(DOW FROM NOW()) + OR (EXTRACT(DOW FROM NOW())=0 AND day=7) + ) + AND EXISTS ( + SELECT 1 + FROM jukebox.playlist_entries + WHERE playlistid=jukebox.playlists.playlistid + AND revision_removed IS NULL + LIMIT 1 + ) + $where + GROUP BY playlists.playlistid", + [$categoryId] + ); + + if (!sizeof($result)) { + throw new MyRadioException('No weighted playlists currently available.'); + } + + for ($i = 0; $i < sizeof($result); $i++) { + foreach ($playlists_to_ignore as $playlist) { + if ($result[$i]['item'] === $playlist->getID()) { + unset($result[$i]); + break; + } + } + } + + return self::getInstance(CoreUtils::biasedRandom($result)); + } + + /** + * Find all playlists with a given playlist category. + * @param $categoryId + * @param bool $includeArchived whether to include archived playlists (default false) + * @return iTones_Playlist[] + */ + public static function getAllPlaylistsOfCategory($categoryId, $includeArchived = false) + { + if (!is_int($categoryId)) { + throw new MyRadioException('Expected $categoryId to be an integer'); + } + $where = ""; + if (!$includeArchived) { + $where = "AND archived = false"; + } + self::wakeup(); + $result = self::$db->fetchColumn( + "SELECT * FROM jukebox.playlists WHERE category = $1 $where ORDER BY title", + [$categoryId] + ); + + return self::resultSetToObjArray($result); + } + + /** + * Find out what Playlists have this Track in them, if any. + * + * @param MyRadio_Track $track The track to search for + * @param bool $includeArchived whether to include archived playlists (default false) + * + * @return array One or more iTones_Playlists, each of which contain $track + */ + public static function getPlaylistsWithTrack(MyRadio_Track $track, $includeArchived = false) + { + $result = self::$db->fetchColumn( + 'SELECT playlistid FROM jukebox.playlist_entries WHERE trackid=$1 + AND revision_removed IS NULL', + [$track->getID()] + ); + + if ($includeArchived) { + return self::resultSetToObjArray($result); + } + + return array_filter(self::resultSetToObjArray($result), function ($playlist) { + return !$playlist->isArchived(); + }); + } + + public static function create($title, $description, $category, $archived) + { + if (!is_int($category)) { + throw new MyRadioException('Expected $category to be an integer'); + } + $id = str_replace(' ', '-', $title); + $id = strtolower(preg_replace('/[^a-z0-9-]/i', '', $id)); + + // You may think, "hmm, why would you create a playlist archived immediately?" + // Well, I'm justifying it by saying that music team may want to not release + // a playlist immediately, but still be working on it. The real reason is + // cause it was easier to have the form have an archive option on creation + // because its there for editing, and so we should have the button + // do what it says it'll do, even if it seems a bit odd. + // Michael, nearly 3am, 2021 :) + + self::$db->query( + 'INSERT INTO jukebox.playlists (playlistid, title, description, category, archived) + VALUES ($1, $2, $3, $4, $5)', + [$id, $title, $description, $category, $archived] + ); + + return self::getInstance($id); + } + + /** + * Returns an array of key information, useful for Twig rendering and JSON requests. + * @param $mixins Mixins. Currently unused. + * @return array + * @todo Expand the information this returns + */ + public function toDataSource($mixins = []) + { + return [ + 'title' => $this->getTitle(), + 'playlistid' => $this->getID(), + 'description' => $this->getDescription(), + 'category' => array_merge($this->getCategory()->toDataSource($mixins), [ + // I don't like using html here, but if I use text it adds an unnecessary and ugly tag + 'display' => 'html', + 'html' => $this->getCategory()->getName() + ]), + 'archived' => ($this->isArchived()) ? 'Archved' : 'Active', + 'edittrackslink' => [ + 'display' => 'icon', + 'value' => 'folder-open', + 'title' => 'Edit Tracks in this playlist', + 'url' => URLUtils::makeURL('iTones', 'editPlaylist', ['playlistid' => $this->getID()]), + ], + 'configurelink' => [ + 'display' => 'icon', + 'value' => 'wrench', + 'title' => 'Alter playlist settings', + 'url' => URLUtils::makeURL('iTones', 'configurePlaylist', ['playlistid' => $this->getID()]), + ], + 'revisionslink' => [ + 'display' => 'icon', + 'value' => 'time', + 'title' => 'View revision history', + 'url' => URLUtils::makeURL('iTones', 'viewPlaylistHistory', ['playlistid' => $this->getID()]), + ], + ]; + } } diff --git a/src/Classes/iTones/iTones_PlaylistAvailability.php b/src/Classes/iTones/iTones_PlaylistAvailability.php new file mode 100644 index 000000000..835d04d84 --- /dev/null +++ b/src/Classes/iTones/iTones_PlaylistAvailability.php @@ -0,0 +1,212 @@ +availability_table = 'jukebox.playlist_availability'; + $this->timeslot_table = 'jukebox.playlist_timeslot'; + $this->id_field = 'playlist_availability_id'; + + $result = self::$db->fetchOne( + 'SELECT * FROM '.$this->availability_table.' WHERE '.$this->id_field.'=$1', + [$id] + ); + if (empty($result)) { + throw new MyRadioException('Playlist Availability '.$id.' does not exist!'); + } + + parent::__construct($id, $result); + + $this->playlist = iTones_Playlist::getInstance($result['playlistid']); + $this->weight = intval($result['weight']); + } + + /** + * Returns data about the Availability. + * @param array $mixins Mixins. Also includes data about the parent Availability object + * @return array + */ + public function toDataSource($mixins = []) + { + $data = parent::toDataSource($mixins); + $data['playlist'] = $this->getPlaylist()->toDataSource(); + $data['weight'] = $this->getWeight(); + $data['edit'] = [ + 'display' => 'icon', + 'value' => 'pencil', + 'title' => 'Click to edit this availability', + 'url' => URLUtils::makeURL('iTones', 'editAvailability', ['availabilityid' => $this->getID()]), + ]; + + return $data; + } + + /** + * Get the Playlist this is a Campaign for. + * + * @return iTones_Playlist + */ + public function getPlaylist() + { + return $this->playlist; + } + + /** + * Returns the weight of the Availability. + * + * @return int + */ + public function getWeight() + { + return $this->weight; + } + + public function setWeight($weight) + { + $this->weight = $weight; + self::$db->query( + 'UPDATE '.$this->availability_table.' SET weight=$1 WHERE '.$this->id_field.'=$2', + [$weight, $this->getID()] + ); + $this->updateCacheObject(); + } + + /** + * Returns a MyRadioForm filled in and ripe for being used to edit this Availability. + * + * @return MyRadioForm + */ + public function getEditForm() + { + return self::getForm($this->getPlaylist()->getID()) + ->editMode( + $this->getID(), + [ + 'timeslots' => $this->getTimeslots(), + 'effective_from' => CoreUtils::happyTime($this->getEffectiveFrom()), + 'effective_to' => $this->getEffectiveTo() === null ? null : + CoreUtils::happyTime($this->getEffectiveTo()), + 'weight' => $this->getWeight(), + ] + ); + } + + /** + * Creates a new Availability. + * + * @param iTones_Playlist $playlist The Playlist that is being Availabled. + * @param int $weight The weight of the Availability. + * @param int $effective_from Epoch time that the Availability is starts at. Default now. + * @param int $effective_to Epoch time that the Availability ends at. Default never. + * @param array $timeslots An array of Timeslots the Availability is active during. + * + * @return iTones_PlaylistAvailability The new Availability + */ + public static function create( + iTones_Playlist $playlist, + $weight, + $effective_from = null, + $effective_to = null, + $timeslots = [] + ) { + if ($effective_from == null) { + $effective_from = time(); + } + + $result = self::$db->fetchColumn( + 'INSERT INTO jukebox.playlist_availability + (playlistid, weight, effective_from, effective_to, memberid, approvedid) + VALUES ($1, $2, $3, $4, $5, $5) RETURNING playlist_availability_id', + [ + $playlist->getID(), + $weight, + CoreUtils::getTimestamp($effective_from), + $effective_to ? CoreUtils::getTimestamp($effective_to) : null, + MyRadio_User::getInstance()->getID(), + ] + ); + + $availability = self::getInstance($result[0]); + + foreach ($timeslots as $timeslot) { + $availability->addTimeslot($timeslot['day'], $timeslot['start_time'], $timeslot['end_time']); + } + + return $availability; + } + + /** + * Returns the form needed to create or edit Playlist Availabilities. + * + * @param int $playlistid The ID of the Playlist that this Availability will be/is linked to + * + * @return MyRadioForm + */ + public static function getForm($playlistid = null) + { + return parent::getForm('iTones', 'editAvailability') + ->setTitle('Edit Playlist Availability') + ->addField( + new MyRadioFormField( + 'weight', + MyRadioFormField::TYPE_NUMBER, + [ + 'required' => true, + 'label' => 'Weight', + 'explanation' => 'A heavier playlist is more likely to be played.', + ] + ) + ) + ->addField( + new MyRadioFormField( + 'playlistid', + MyRadioFormField::TYPE_HIDDEN, + [ + 'value' => $playlistid, + ] + ) + ); + } + + public static function getAvailabilitiesForPlaylist($playlistid) + { + return self::resultSetToObjArray( + self::$db->fetchColumn( + 'SELECT playlist_availability_id FROM jukebox.playlist_availability WHERE playlistid=$1', + [$playlistid] + ) + ); + } +} diff --git a/src/Classes/iTones/iTones_PlaylistCategory.php b/src/Classes/iTones/iTones_PlaylistCategory.php new file mode 100644 index 000000000..a22172205 --- /dev/null +++ b/src/Classes/iTones/iTones_PlaylistCategory.php @@ -0,0 +1,100 @@ +id = $data['id']; + $this->name = $data['name']; + $this->description = $data['description']; + } + + /** + * Get the ID of this category. + * @return int + */ + public function getID() + { + return $this->id; + } + + /** + * Gets the name of the category. + * @return string + */ + public function getName() + { + return $this->name; + } + + /** + * Gets the description of this category. May contain HTML. + * @return string + */ + public function getDescription() + { + return $this->description; + } + + public function toDataSource($mixins = []) + { + return [ + 'id' => $this->getID(), + 'name' => $this->getName(), + 'description' => $this->getDescription() + ]; + } + + + /** + * Gets all defined playlist categories. + * @return array + */ + public static function getAll() + { + self::wakeup(); + $rows = self::$db->fetchAll('SELECT id, name, description FROM jukebox.playlist_categories'); + + $vals = []; + foreach ($rows as $row) { + $vals[] = new self($row); + } + + return \MyRadio\MyRadio\CoreUtils::setToDataSource($vals); + } + + /** + * Gets all the categories, formatted for use in a MyRadioFormField::TYPE_SELECT + * @return array + */ + public static function getOptions() + { + self::wakeup(); + + return self::$db->fetchAll('SELECT id AS value, name AS text FROM jukebox.playlist_categories ORDER BY id ASC'); + } + + protected static function factory($id) + { + $sql = 'SELECT id, name, description FROM jukebox.playlist_categories WHERE id = $1 LIMIT 1'; + $result = self::$db->fetchOne($sql, [$id]); + + if (empty($result)) { + throw new MyRadioException('That playlist category is in the twilight zone.', 404); + } + + return new self($result); + } +} diff --git a/src/Classes/iTones/iTones_PlaylistRevision.php b/src/Classes/iTones/iTones_PlaylistRevision.php index 722302299..93e9d08f2 100644 --- a/src/Classes/iTones/iTones_PlaylistRevision.php +++ b/src/Classes/iTones/iTones_PlaylistRevision.php @@ -1,137 +1,178 @@ - * @package MyRadio_iTones - * @uses \Database + * The iTones_PlaylistRevision class helps to manage previous versions of an iTones_Playlist. */ -class iTones_PlaylistRevision extends iTones_Playlist { - - /** - * When this revision was created - * @var int - */ - private $timestamp; - - /** - * Who created this revision - * @var MyRadio_User - */ - private $author; - - /** - * A commit message about the change - * @var String - */ - private $notes; - - /** - * Initiates the PlaylistRevision variables - * @param string $id $playlistid~$revisionid - */ - protected function __construct($id) { - list($playlistid, $revisionid) = explode('~', $id); - parent::__construct($playlistid); - - $result = self::$db->fetch_one('SELECT * FROM jukebox.playlist_revisions - WHERE playlistid=$1 AND revisionid=$2 LIMIT 1', - array($playlistid, $revisionid)); - if (empty($result)) { - throw new MyRadioException('The specified iTones Playlist Revision does not seem to exist'); - return; +class iTones_PlaylistRevision extends iTones_Playlist +{ + /** + * When this revision was created. + * + * @var int + */ + private $timestamp; + + /** + * Who created this revision. + * + * @var MyRadio_User + */ + private $author; + + /** + * A commit message about the change. + * + * @var string + */ + private $notes; + + /** + * Initiates the PlaylistRevision variables. + * + * @param string $id $playlistid~$revisionid + */ + protected function __construct($id) + { + list($playlistid, $revisionid) = explode('~', $id); + parent::__construct($playlistid); + + $result = self::$db->fetchOne( + 'SELECT * FROM jukebox.playlist_revisions + WHERE playlistid=$1 AND revisionid=$2 LIMIT 1', + [$playlistid, $revisionid] + ); + if (empty($result)) { + throw new MyRadioException('The specified iTones Playlist Revision does not seem to exist', 404); + + return; + } + + $this->revisionid = $revisionid; + $this->author = MyRadio_User::getInstance($result['author']); + $this->notes = $result['notes']; + $this->timestamp = strtotime($result['timestamp']); + + $items = self::$db->fetchColumn( + 'SELECT trackid FROM jukebox.playlist_entries WHERE playlistid=$1 + AND revision_added <= $2 AND (revision_removed >= $2 OR revision_removed IS NULL) + ORDER BY entryid', + [$this->getID(), $this->getRevisionID()] + ); + + foreach ($items as $id) { + $this->tracks[] = MyRadio_Track::getInstance($id); + } + } + + /** + * Return the MyRadio_Tracks that belong to this playlist. + * + * @return array of MyRadio_Track objects + */ + public function getTracks() + { + return $this->tracks; + } + + public function getAuthor() + { + return $this->author; + } + + public function getNotes() + { + return $this->notes; + } + + public function getTimestamp() + { + return $this->timestamp; + } + + public function getRevisionID() + { + return $this->revisionid; } - - $this->revisionid = $revisionid; - $this->author = MyRadio_User::getInstance($result['author']); - $this->notes = $result['notes']; - $this->timestamp = strtotime($result['timestamp']); - - $items = self::$db->fetch_column('SELECT trackid FROM jukebox.playlist_entries WHERE playlistid=$1 - AND revision_added <= $2 AND (revision_removed >= $2 OR revision_removed IS NULL) - ORDER BY entryid', array($this->getID(), $this->getRevisionID())); - - foreach ($items as $id) { - $this->tracks[] = MyRadio_Track::getInstance($id); + + /** + * Prevents idiots attempting to edit this revision. + */ + public function acquireOrRenewLock($lockstr = null, MyRadio_User $user = null) + { + throw new MyRadioException('You can\'t lock an archived playlist revision, poopyhead!'); + } + + /** + * Prevents idiots attempting to edit this revision. + */ + public function setTracks($tracks, $lockstr = null, $notes = null) + { + throw new MyRadioException('You can\'t lock an archived playlist revision, poopyhead!'); + } + + public static function getAllRevisions($playlistid) + { + $data = []; + foreach (self::$db->fetchColumn( + 'SELECT revisionid FROM jukebox.playlist_revisions WHERE playlistid=$1', + [$playlistid] + ) as $revisionid) { + $data[] = self::getInstance($playlistid.'~'.$revisionid); + } + + return $data; } - } - - /** - * Return the MyRadio_Tracks that belong to this playlist - * @return Array of MyRadio_Track objects - */ - public function getTracks() { - return $this->tracks; - } - - public function getAuthor() { - return $this->author; - } - - public function getNotes() { - return $this->notes; - } - - public function getTimestamp() { - return $this->timestamp; - } - - public function getRevisionID() { - return $this->revisionid; - } - - /** - * Prevents idiots attempting to edit this revision. - */ - public function acquireOrRenewLock($lockstr = null, MyRadio_User $user = null) { - throw new MyRadioException('You can\'t lock an archived playlist revision, poopyhead!'); - } - - /** - * Prevents idiots attempting to edit this revision. - */ - public function setTracks($tracks, $lockstr = null, $notes = null) { - throw new MyRadioException('You can\'t lock an archived playlist revision, poopyhead!'); - } - - public static function getAllRevisions($playlistid) { - $data = array(); - foreach (self::$db->fetch_column('SELECT revisionid FROM jukebox.playlist_revisions WHERE playlistid=$1', - array($playlistid)) as $revisionid) { - $data[] = self::getInstance($playlistid.'~'.$revisionid); + /** + * Returns an array of key information, useful for Twig rendering and JSON requests. + * @param array $mixins Mixins. Currently unused. + * @return array + * @todo Expand the information this returns + */ + public function toDataSource($mixins = []) + { + return [ + 'revisionid' => $this->getRevisionID(), + 'timestamp' => CoreUtils::happyTime($this->getTimestamp()), + 'notes' => $this->getNotes(), + 'author' => $this->getAuthor()->getName(), + 'viewtrackslink' => [ + 'display' => 'icon', + 'value' => 'folder-open', + 'title' => 'View Tracks in this playlist revision', + 'url' => URLUtils::makeURL( + 'iTones', + 'viewPlaylistRevision', + [ + 'playlistid' => $this->getID(), + 'revisionid' => $this->getRevisionID(), + ] + ), + ], + 'restorelink' => [ + 'display' => 'icon', + 'value' => 'retweet', + 'title' => 'Restore this revision', + 'url' => URLUtils::makeURL( + 'iTones', + 'restorePlaylistRevision', + [ + 'playlistid' => $this->getID(), + 'revisionid' => $this->getRevisionID(), + ] + ), + ], + ]; } - return $data; - } - - /** - * Returns an array of key information, useful for Twig rendering and JSON requests - * @todo Expand the information this returns - * @return Array - */ - public function toDataSource() { - return array( - 'revisionid' => $this->getRevisionID(), - 'timestamp' => CoreUtils::happyTime($this->getTimestamp()), - 'notes' => $this->getNotes(), - 'author' => $this->getAuthor()->getName(), - 'viewtrackslink' => array('display' => 'icon', - 'value' => 'folder-open', - 'title' => 'View Tracks in this playlist revision', - 'url' => CoreUtils::makeURL('iTones', 'viewPlaylistRevision', - array('playlistid'=>$this->getID(), 'revisionid' => $this->getRevisionID()))), - 'restorelink' => array('display' => 'icon', - 'value' => 'refresh', - 'title' => 'Restore this revision', - 'url' => CoreUtils::makeURL('iTones', 'restorePlaylistRevision', - array('playlistid'=>$this->getID(), 'revisionid' => $this->getRevisionID()))), - - ); - } } diff --git a/src/Classes/iTones/iTones_TrackRequest.php b/src/Classes/iTones/iTones_TrackRequest.php index 8c5f5a67f..fab560cde 100644 --- a/src/Classes/iTones/iTones_TrackRequest.php +++ b/src/Classes/iTones/iTones_TrackRequest.php @@ -1,20 +1,23 @@ - * @author Matt Windsor - * @package MyRadio_iTones * @uses \Database */ -class iTones_TrackRequest { +class iTones_TrackRequest +{ const CAN_MAKE_REQUEST_SQL = ' SELECT (COUNT(trackid) <= $1) AS allowed @@ -38,26 +41,27 @@ class iTones_TrackRequest { * @param MyRadio_Track $track The track being requested. * @param MyRadio_User $requester The user performing the request. * @param Database $database The database to query for request data. - * @param String $queue The iTones queue to request into. + * @param string $queue The iTones queue to request into. */ public function __construct( MyRadio_Track $track, MyRadio_User $requester, Database $database, - $queue = 'requests' + $queue = 'requests' ) { - $this->track = $track; + $this->track = $track; $this->requester = $requester; - $this->database = $database; - $this->queue = $queue; + $this->database = $database; + $this->queue = $queue; } /** * Performs the track request. - * + * * @return bool Whether the operation was successful */ - public function request() { + public function request() + { $success = false; if ($this->canRequestTrack() === true) { @@ -70,13 +74,14 @@ public function request() { /** * Checks whether the given track can be requested by the current user. * - * @return bool Whether the track can be requested. + * @return bool Whether the track can be requested. */ - private function canRequestTrack() { - return ( + private function canRequestTrack() + { + return $this->trackCanBePlayed() && $this->userCanMakeRequests() - ); + ; } /** @@ -84,10 +89,12 @@ private function canRequestTrack() { * * This generally means playing it won't trip licencing quotae. * - * @return bool Whether the track can be played. + * @return bool Whether the track can be played. */ - private function trackCanBePlayed() { - return !(MyRadio_TracklistItem::getIfPlayedRecently($this->track)); + private function trackCanBePlayed() + { + return !MyRadio_TracklistItem::getIfPlayedRecently($this->track) && + $this->track->getClean() !== 'n'; } /** @@ -95,30 +102,33 @@ private function trackCanBePlayed() { * * This generally means requesting won't trip the user's request quota. * - * @return bool Whether the current user can make a request. + * @return bool Whether the current user can make a request. */ - public function userCanMakeRequests() { + public function userCanMakeRequests() + { return $this->areRequestsAllowedBy($this->userCanMakeRequestsQuery()); } /** * Checks to see if the database said the current user can make requests. * - * @param object $results The results from a can-make-requests query. + * @param object $results The results from a can-make-requests query. * - * @return bool Whether the current user can make a request. + * @return bool Whether the current user can make a request. */ - private function areRequestsAllowedBy($results) { + private function areRequestsAllowedBy($results) + { return $results['allowed'] == 't'; } /** * Runs a query to see if the current user can make requests at the oment. * - * @return object The database query results. + * @return object The database query results. */ - private function userCanMakeRequestsQuery() { - return $this->database->fetch_one( + private function userCanMakeRequestsQuery() + { + return $this->database->fetchOne( self::CAN_MAKE_REQUEST_SQL, $this->userCanMakeRequestsParams() ); @@ -127,14 +137,15 @@ private function userCanMakeRequestsQuery() { /** * Creates the parameter list for a can-make-requests query. * - * @return array The parameter list. + * @return array The parameter list. */ - private function userCanMakeRequestsParams() { - return [ - Config::$itones_request_maximum, - $this->requester->getID(), - Config::$itones_request_period - ]; + private function userCanMakeRequestsParams() + { + return [ + Config::$itones_request_maximum, + $this->requester->getID(), + Config::$itones_request_period, + ]; } /** @@ -142,7 +153,8 @@ private function userCanMakeRequestsParams() { * * @return bool Whether the request was successful. */ - private function requestTrackAndLog() { + private function requestTrackAndLog() + { $success = iTones_Utils::requestFile( $this->track->getPath(), $this->queue @@ -151,17 +163,17 @@ private function requestTrackAndLog() { if ($success) { $this->logRequest(); } + return $success; } /** * Logs that the current user has made a request. * - * @param MyRadio_Track $track The track to log in the database. - * - * @return null Nothing. + * @param MyRadio_Track $track The track to log in the database. */ - private function logRequest() { + private function logRequest() + { $this->database->query( self::LOG_REQUEST_SQL, $this->logRequestParams() @@ -171,15 +183,16 @@ private function logRequest() { /** * Creates the parameter list for a log-request query. * - * @param MyRadio_Track $track The track to log in the database. + * @param MyRadio_Track $track The track to log in the database. * - * @return array The parameters array. + * @return array The parameters array. */ - private function logRequestParams() { + private function logRequestParams() + { return [ $this->track->getID(), $this->requester->getID(), - $this->queue + $this->queue, ]; } } diff --git a/src/Classes/iTones/iTones_Utils.php b/src/Classes/iTones/iTones_Utils.php index 38a8a5eb3..8750b10cc 100644 --- a/src/Classes/iTones/iTones_Utils.php +++ b/src/Classes/iTones/iTones_Utils.php @@ -1,24 +1,29 @@ - * @package MyRadio_iTones - * @uses \Database + * The iTones_Utils class provides generic utilities for controlling iTones - URY's Campus Jukebox. + * + * @uses \Database */ -class iTones_Utils extends ServiceAPI { - +class iTones_Utils extends \MyRadio\ServiceAPI\ServiceAPI +{ private static $telnet_handle; - private static $queues = array('requests', 'main'); - private static $queue_cache = array(); - public static $ops = array(); + private static $queues = ['requests']; + private static $queue_cache = []; + public static $ops = []; const REQUESTS_REMAINING_SQL = ' SELECT @@ -31,98 +36,154 @@ class iTones_Utils extends ServiceAPI { ;'; /** - * Gets the number of tracks the current user can currently request + * Gets the number of tracks the current user can currently request. * - * @return int The number of tracks requestable as of now. + * @return int The number of tracks requestable as of now. */ - public static function getRemainingRequests() { - return self::$db->fetch_one( - self::REQUESTS_REMAINING_SQL, - self::getRemainingRequestsParams() - )['remaining']; + public static function getRemainingRequests() + { + return self::$db->fetchOne(self::REQUESTS_REMAINING_SQL, self::getRemainingRequestsParams())['remaining']; } /** * Creates the parameter list for a requests-remaining query. * - * @return array The parameter list. + * @return array The parameter list. */ - private static function getRemainingRequestsParams() { - return [ - Config::$itones_request_maximum, - MyRadio_User::getInstance()->getID(), - Config::$itones_request_period - ]; + private static function getRemainingRequestsParams() + { + return [ + Config::$itones_request_maximum, + MyRadio_User::getInstance()->getID(), + Config::$itones_request_period, + ]; + } + + /** + * Based on the set of configured playlists, schedules, weights, track history + * and other stuff, select a track that the jukebox should play. + * + * @return MyRadio_Track + */ + public static function getTrackForJukebox() + { + $playlists_to_ignore = []; + + while ($playlist = iTones_Playlist::getPlaylistOfCategoryFromWeights( + Config::$jukebox_playlist_category_id, + $playlists_to_ignore + ) + ) { + $tracks = $playlist->getTracks(); + + // Randomly sort the array, then pop them out until one is playable (or we run out) + shuffle($tracks); + + while ($track = array_pop($tracks)) { + // $track-> calls first because in theory these checks are really fast + if ($track->getClean() !== 'n' + && !$track->isBlacklisted() + // These ones involve running more queries... + && !MyRadio_TracklistItem::getIfPlayedRecently($track) + && MyRadio_TracklistItem::getIfAlbumArtistCompliant($track) + // And these ones involve telnet! + && !iTones_Utils::getIfQueued($track) + && !iTones_Utils::getIfNowPlaying($track) + ) { + return $track; + } + } + + // We've reached the end of the track list and none of them are playable + // ignore the playlist we've been given, and try again + $playlists_to_ignore[] = $playlist; + } } /** * Push a track into the iTones request queue, if it hasn't been played * recently. - * + * * @param MyRadio_Track $track - * @param $queue The jukebox_[x] queue to push to. Default requests. - * "main" is the queue used for the main track scheduler, i.e. non-user entries. + * @param $queue The jukebox_[x] queue to push to. Default requests. + * "main" is the queue used for the main track scheduler, i.e. non-user entries. + * * @return bool Whether the operation was successful */ - public static function requestTrack(MyRadio_Track $track, $queue = 'requests') { + public static function requestTrack(MyRadio_Track $track, $queue = 'requests') + { $track_request = new iTones_TrackRequest( $track, MyRadio_User::getInstance(), self::$db, $queue ); + return $track_request->request(); } - + /** * Pushes the file at the given path to the iTones request queue. - * - * @param String $file Path to file on iTones server. + * + * @param string $file Path to file on iTones server. + * * @return bool Whether the operation was successful */ - public static function requestFile($file, $queue = 'requests') { + public static function requestFile($file, $queue = 'requests') + { self::verifyQueue($queue); - $r = self::telnetOp('jukebox_' . $queue . '.push ' . $file); + $r = self::telnetOp('jukebox_'.$queue.'.push replay_gain:'.$file); + return is_numeric($r); } /** - * Returns Request IDs and Track IDs currently in the queue - * @param String $queue Optional, as per definition in requestTrack() - * @return Array 2D, such as [['requestid' => 1, 'trackid' => 72830, 'queue' => 'requests'], ...] + * Returns Request IDs and Track IDs currently in the queue. + * + * @param string $queue Optional, as per definition in requestTrack() + * + * @return array 2D, such as [['requestid' => 1, 'trackid' => 72830, 'queue' => 'requests'], ...] */ - public static function getTracksInQueue($queue = 'requests') { + public static function getTracksInQueue($queue = 'requests') + { self::verifyQueue($queue); if (isset(self::$queue_cache[$queue])) { return self::$queue_cache[$queue]; } - $info = explode(' ', self::telnetOp('jukebox_' . $queue . '.queue')); + $info = explode(' ', self::telnetOp('jukebox_'.$queue.'.queue')); - $items = array(); + $items = []; foreach ($info as $item) { if (is_numeric($item)) { - $meta = self::telnetOp('request.metadata ' . $item); + $meta = self::telnetOp('request.metadata '.$item); //Don't include items that are set to ignore if (stristr($meta, 'skip="true"') === false) { //Get the trackid - $tid = preg_replace('/^.*filename=\"' . str_replace('/', '\\/', Config::$music_central_db_path) - . '\/records\/[0-9]+\/([0-9]+)\.mp3.*$/is', '$1', $meta); + $tid = preg_replace( + '/^.*filename=\"'.str_replace('/', '\\/', Config::$music_central_db_path) + .'\/records\/[0-9]+\/([0-9]+)\.mp3.*$/is', + '$1', + $meta + ); //Push the item - $items[] = array('requestid' => (int) $item, 'trackid' => (int) $tid, 'queue' => $queue); + $items[] = ['requestid' => (int) $item, 'trackid' => (int) $tid, 'queue' => $queue]; } } } self::$queue_cache[$queue] = $items; + return $items; } /** - * Returns all the tracks in all Queues - * @return Array compatible with getTracksInQueue + * Returns all the tracks in all Queues. + * + * @return array compatible with getTracksInQueue */ - public static function getTracksInAllQueues() { + public static function getTracksInAllQueues() + { $d = []; foreach (self::$queues as $queue) { $d = array_merge($d, self::getTracksInQueue($queue)); @@ -133,121 +194,167 @@ public static function getTracksInAllQueues() { /** * Check if a track is currently queued to be played in any queue. - * @return boolean + * + * @return bool */ - public static function getIfQueued(MyRadio_Track $track) { + public static function getIfQueued(MyRadio_Track $track) + { foreach (self::$queues as $queue) { $r = self::getTracksInQueue($queue); foreach ($r as $req) { - if ((int) $req['trackid'] === (int) $track->getID()) + if ((int) $req['trackid'] === (int) $track->getID()) { return true; + } } } + return false; } /** - * Goes through the Queues, removing duplicate items - * + * Check if a track is being played by Jukebox right now. + * @param MyRadio_Track $track + * @return bool + */ + public static function getIfNowPlaying(MyRadio_Track $track) + { + $id = self::telnetOp('now_playing'); + if (!is_numeric($id)) { + return false; + } + return (int) $id === $track->getID(); + } + + /** + * Goes through the Queues, removing duplicate items. + * * @return int The number of tracks that were removed from queues */ - public static function removeDuplicateItemsInQueues() { + public static function removeDuplicateItemsInQueues() + { //Get the tracks in all the queues - $tracks = array(); + $tracks = []; foreach (self::$queues as $queue) { $tracks = array_merge($tracks, self::getTracksInQueue($queue)); } //Go over each track, marking it as identified. If it's encountered a second time, kill it. - $found = array(); + $found = []; $removed = 0; foreach ($tracks as $track) { if (in_array($track['trackid'], $found)) { self::removeRequestFromQueue($track['queue'], $track['requestid']); - $removed++; + ++$removed; } else { $found[] = $track['trackid']; } } + return $removed; } /** * Empties all request queues. */ - public static function emptyQueues() { + public static function emptyQueues() + { //Get the tracks in all the queues - $tracks = array(); + $tracks = []; foreach (self::$queues as $queue) { $tracks = array_merge($tracks, self::getTracksInQueue($queue)); } - + foreach ($tracks as $track) { self::removeRequestFromQueue($track['queue'], $track['requestid']); } } /** - * "Deletes" the given request from the given queue. It marks the item as + * "Deletes" the given request from the given queue. It marks the item as * "ignore" but the rid remains in the queue. - * @param String $queue - * @param int $requestid + * + * @param string $queue + * @param int $requestid */ - private static function removeRequestFromQueue($queue, $requestid) { + private static function removeRequestFromQueue($queue, $requestid) + { self::verifyQueue($queue); - self::telnetOp('jukebox_' . $queue . '.ignore ' . $requestid); + self::telnetOp('jukebox_'.$queue.'.ignore '.$requestid); unset(self::$queue_cache[$queue]); } - + /** * Skips to the next track. - * @return String telnet response. + * + * @return string telnet response. */ - public static function skip() { + public static function skip() + { return self::telnetOp('jukebox.skip'); } - private static function verifyQueue($queue) { + private static function verifyQueue($queue) + { if (in_array($queue, self::$queues) === false) { throw new MyRadioException('Invalid Queue!'); } } /** - * Runs a telnet command - * @param String $command - * @return String + * Runs a telnet command. + * + * @param string $command + * + * @return string */ - private static function telnetOp($command) { + private static function telnetOp($command) + { self::$ops[] = $command; if (empty(self::$telnet_handle)) { self::telnetStart(); } - fwrite(self::$telnet_handle, $command . "\n"); + fwrite(self::$telnet_handle, $command."\n"); $response = ''; $line = ''; + $empty_line_counter = 0; do { $response .= $line; - $line = fgets(self::$telnet_handle, 1048576); //Read a max of 1MB of data + $line = fgets(self::$telnet_handle, 1024); //Read a max of 1KB of data + if (empty(trim($line))) { + $empty_line_counter++; + if ($empty_line_counter > 5) { + break; + } + } else { + $empty_line_counter = 0; + } } while (trim($line) !== 'END'); - - - //Remove the END + //Intentionally doesn't append END return trim($response); } - private static function telnetStart() { - self::$telnet_handle = fsockopen('tcp://' . Config::$itones_telnet_host, Config::$itones_telnet_port, $errno, $errstr, 10); - register_shutdown_function(array(__CLASS__, 'telnetEnd')); + private static function telnetStart() + { + self::$telnet_handle = fsockopen( + 'tcp://'.Config::$itones_telnet_host, + Config::$itones_telnet_port, + $errno, + $errstr, + 10 + ); + register_shutdown_function([__CLASS__, 'telnetEnd']); } - public static function telnetEnd() { - fwrite(self::$telnet_handle, "quit\n"); - fclose(self::$telnet_handle); + public static function telnetEnd() + { + if (self::$telnet_handle) { + fwrite(self::$telnet_handle, "quit\n"); + fclose(self::$telnet_handle); + self::$telnet_handle = null; + } } - } diff --git a/src/Controllers/Charts/default.php b/src/Controllers/Charts/default.php index 1ee70ef5b..96c6ba548 100644 --- a/src/Controllers/Charts/default.php +++ b/src/Controllers/Charts/default.php @@ -1,15 +1,10 @@ setTemplate( - 'table.twig' -)->addVariable( - 'tablescript', - 'myury.datatable.default' -)->addVariable( - 'title', - 'Charts' -)->addVariable( - 'tabledata', - ServiceAPI::setToDataSource(MyRadio_ChartType::getAll()) -)->render(); -?> + +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_ChartType; + +CoreUtils::getTemplateObject()->setTemplate('table.twig') + ->addVariable('title', 'Charts') + ->addVariable('tablescript', 'myradio.charts') + ->addVariable('tabledata', CoreUtils::setToDataSource(MyRadio_ChartType::getAll())) + ->render(); diff --git a/src/Controllers/Charts/doEditChartRelease.php b/src/Controllers/Charts/doEditChartRelease.php deleted file mode 100644 index fdba1527b..000000000 --- a/src/Controllers/Charts/doEditChartRelease.php +++ /dev/null @@ -1,111 +0,0 @@ - - * @package MyRadio_Charts - */ - -/* - * Creates a new chart row. - * - * @param $chart_release_id The numeric ID of the chart release to which this - * row belongs. - * @param $position The position in the chart of this row (from 1). - * @param $track The MyRadio_Track this row contains. - * - * @return Nothing. This function writes directly to the database. - */ -function create_chart_row($chart_release_id, $position, $track) { - MyRadio_ChartRow::create( - [ - 'chart_release_id' => $chart_release_id, - 'position' => $position, - 'trackid' => $track->getID() - ] - ); -} - -/* - * Creates a new chart release. - * - * @param $data The data hash from the chart releases form. - * - * @return Nothing. This function writes directly to the database. - */ -function create_chart_release($data) { - MyRadio_ChartRelease::create($data); - $chart_release_id = MyRadio_ChartRelease::findReleaseIDOn( - $data['submitted_time'], - $data['chart_type_id'] - ); - - for ($i = 1; $i <= 10; $i++) { - create_chart_row($chart_release_id, $i, track_at($i, $data)); - } -} - -/* - * Edits the given chart row. - * - * @param $chart_row The chart row to edit. - * @param $track The new track for the chart row. - * - * @return Nothing. This function writes directly to the database. - */ -function edit_chart_row($chart_row, $track) { - $chart_row->setTrackID($track->getID()); -} - -/* - * Edits the chart release with the given ID. - * - * @param $id The ID of the chart release to edit. - * @param $data The data hash from the chart releases form. - * - * @return Nothing. This function writes directly to the database. - */ -function edit_chart_release($id, $data) { - $chart_release = MyRadio_ChartRelease::getInstance($id); - $chart_release->setChartTypeID( - $data['chart_type_id'] - )->setReleaseTime($data['submitted_time']); - - // TODO: Handle existing chart releases with differing numbers of chart rows. - // Currently, this case will explode dramatically. - foreach($chart_release->getChartRows() as $chart_row) { - edit_chart_row($chart_row, track_at($chart_row->getPosition(), $data)); - } -} - -/* - * Gets the track for the given position from the data. - * - * @param $position The position whose track is sought (starting from 1). - * @param $data The dataset containing the tracks. - * - * @return MyRadio_Track the track at the given position. - */ -function track_at($position, $data) { - return $data['track' . $position]; -} - - -/* - * END OF HELPER FUNCTIONS - */ - -$form = MyRadio_JsonFormLoader::loadFromModule( - $module, 'chartreleasefrm', 'doEditChartRelease', - ['chart_types' => []] -); - -$data = $form->readValues(); - -if (empty($data['id'])) { - create_chart_release($data); -} else { - edit_chart_release($data['id'], $data); -} - -CoreUtils::redirect($module); diff --git a/src/Controllers/Charts/doEditChartType.php b/src/Controllers/Charts/doEditChartType.php deleted file mode 100644 index 8d62996b4..000000000 --- a/src/Controllers/Charts/doEditChartType.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @package MyRadio_Charts - */ - -$form = MyRadio_JsonFormLoader::loadFromModule( - $module, 'charttypefrm', 'doEditChartType' -); - -$data = $form->editMode(null, null)->readValues(); -$chart_type = MyRadio_ChartType::getInstance($data['myradiofrmedid']); -$chart_type->setName($data['name'])->setDescription($data['description']); - -require 'Views/MyRadio/back.php'; diff --git a/src/Controllers/Charts/editChartRelease.php b/src/Controllers/Charts/editChartRelease.php index 88f4e0cf0..b2f8f6027 100644 --- a/src/Controllers/Charts/editChartRelease.php +++ b/src/Controllers/Charts/editChartRelease.php @@ -1,57 +1,46 @@ - * @package MyRadio_Charts */ - -$types = MyRadio_ChartType::getAll(); -$type_select = [['text' => 'Please select...', 'disabled' => true]]; -foreach ($types as $type) { - $type_select[] = [ - 'value' => $type->getID(), - 'text' => $type->getDescription() - ]; -} - -$form = MyRadio_JsonFormLoader::loadFromModule( - $module, 'editChartRelease', 'doEditChartRelease', - ['chart_types' => $type_select] -); - -if (empty($_REQUEST['chart_release_id'])) { - $_REQUEST['chart_release_id'] = null; -} - -if ($_REQUEST['chart_release_id']) { - $chart_release = MyRadio_ChartRelease::getInstance($_REQUEST['chart_release_id']); - - // Temporary hack until tabular stuff appears. - $chart_rows = $chart_release->getChartRows(); - $chart_rows_form = []; - for ($i = 0; $i < 10; $i++) { - $row = $chart_rows[$i]; - $chart_rows_form['track' . ($i + 1)] = $row->getTrack(); - } - - $form->editMode( - $chart_release->getID(), - array_merge( - [ - 'submitted_time' => - CoreUtils::happyTime($chart_release->getReleaseTime(), false), - 'chart_type_id' => - $chart_release->getChartTypeID() - ], - $chart_rows_form - ) - ); - +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_ChartRelease; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = MyRadio_ChartRelease::getForm()->readValues(); + + if (empty($data['id'])) { + //create new + $chart_release = MyRadio_ChartRelease::create($data); + } else { + //submit edit + $chart_release = MyRadio_ChartRelease::getInstance($data['id']); + $chart_release + ->setChartTypeID($data['chart_type_id']) + ->setReleaseTime($data['submitted_time']); + } + + foreach ($data['tracks']['track'] as $track) { + if (is_object($track)) { + $tracks[] = $track->getID(); + } + } + + $chart_release->setChartRows($tracks); + + URLUtils::backWithMessage('Chart Release Updated.'); } else { - $form->setTitle('Create Chart Release'); - $form->setFieldValue('submitted_time', CoreUtils::happyTime(time(), false)); + //Not Submitted + if (isset($_REQUEST['chart_release_id'])) { + //edit form + MyRadio_ChartRelease::getInstance($_REQUEST['chart_release_id']) + ->getEditForm() + ->render(); + } else { + //create form + MyRadio_ChartRelease::getForm() + ->setFieldValue('submitted_time', CoreUtils::happyTime(time(), false)) + ->render(); + } } - -$form->render(); -?> diff --git a/src/Controllers/Charts/editChartType.php b/src/Controllers/Charts/editChartType.php index 9b8d1e7a7..cf09f1334 100644 --- a/src/Controllers/Charts/editChartType.php +++ b/src/Controllers/Charts/editChartType.php @@ -1,21 +1,24 @@ - * @package MyRadio_Charts */ +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_ChartType; -$form = MyRadio_JsonFormLoader::loadFromModule( - $module, 'editChartType', 'doEditChartType' -); +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = MyRadio_ChartType::getform()->readValues(); + $chart_type = MyRadio_ChartType::getInstance($data['myradiofrmedid']); + $chart_type->setName($data['name'])->setDescription($data['description']); -$chart_type = MyRadio_ChartType::getInstance($_REQUEST['chart_type_id']); + URLUtils::backWithMessage('Chart Type Updated.'); +} else { + //Not Submitted + if (!isset($_REQUEST['chart_type_id'])) { + throw new MyRadioException('You must provide a chart_type_id', 400); + } -$form->editMode( - $chart_type->getID(), - [ - 'name' => $chart_type->getName(), - 'description' => $chart_type->getDescription() - ] -)->render(); + $chart_type = MyRadio_ChartType::getInstance($_REQUEST['chart_type_id']); + $chart_type->getEditForm()->render(); +} diff --git a/src/Controllers/Charts/listChartReleases.php b/src/Controllers/Charts/listChartReleases.php index e53ec526f..263eb4b9f 100644 --- a/src/Controllers/Charts/listChartReleases.php +++ b/src/Controllers/Charts/listChartReleases.php @@ -1,26 +1,23 @@ - * @package MyRadio_Charts */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_ChartType; -CoreUtils::getTemplateObject( -)->setTemplate( - 'table.twig' +CoreUtils::getTemplateObject()->setTemplate( + 'table.twig' )->addVariable( - 'tablescript', - 'myury.datatable.default' + 'tablescript', + 'myradio.datatable.default' )->addVariable( - 'title', - 'Chart Releases' + 'title', + 'Chart Releases' )->addVariable( - 'tabledata', - ServiceAPI::setToDataSource( - MyRadio_ChartType::getInstance( - $_REQUEST['chart_type_id'] - )->getReleases() - ) + 'tabledata', + CoreUtils::setToDataSource( + MyRadio_ChartType::getInstance( + $_REQUEST['chart_type_id'] + )->getReleases() + ) )->render(); -?> diff --git a/src/Controllers/Errors/400.php b/src/Controllers/Errors/400.php index 68a598bc3..cd4819619 100644 --- a/src/Controllers/Errors/400.php +++ b/src/Controllers/Errors/400.php @@ -1,25 +1,27 @@ - * @version 20131016 - * @package MyRadio_Core + * If you don't know what a 403 page is, you have a long way to go. */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_User; $twig = CoreUtils::getTemplateObject(); header('HTTP/1.1 400 Bad Request'); CoreUtils::getTemplateObject()->setTemplate('error.twig') - ->addVariable('serviceName', 'Error') - ->addVariable('title', 'Bad Request') - ->addVariable('body', '

          I\'m sorry, but the Action you are trying to perform needs more information from you.

          - '.(empty($message) ? '' : $message).' -
          Detailed Request Information - Error: HTTP/1.1 403: Forbidden
          - Module Requested: '.$module.'
          - Action Requested: '.$action.'
          - User Requesting: '.(class_exists('User') ? (MyRadio_User::getInstance()->getName()) : '').' -
          ') - ->render(); -exit; \ No newline at end of file + ->addVariable('serviceName', 'Error') + ->addVariable('title', 'Bad Request') + ->addVariable( + 'body', + '

          I\'m sorry, but the Action you are trying to perform needs more information from you.

          ' + .(empty($message) ? '' : $message) + .'
          Detailed Request Information + Error: HTTP/1.1 400: Bad Request
          + Module Requested: '.$module.'
          + Action Requested: '.$action.'
          + User Requesting: ' + .(class_exists('MyRadio_User') ? (MyRadio_User::getInstance()->getName()) : '') + .'
          ' + ) + ->render(); +exit; diff --git a/src/Controllers/Errors/403.php b/src/Controllers/Errors/403.php index 02b62c1cd..8049c1b3a 100644 --- a/src/Controllers/Errors/403.php +++ b/src/Controllers/Errors/403.php @@ -1,11 +1,8 @@ - * @version 21072012 - * @package MyRadio_Core + * If you don't know what a 403 page is, you have a long way to go. */ +use \MyRadio\MyRadio\CoreUtils; $twig = CoreUtils::getTemplateObject(); -require_once 'Views/Errors/403.php'; \ No newline at end of file +require 'Views/Errors/403.php'; diff --git a/src/Controllers/Errors/404.php b/src/Controllers/Errors/404.php index d382fd76b..4babc48c9 100644 --- a/src/Controllers/Errors/404.php +++ b/src/Controllers/Errors/404.php @@ -1,11 +1,8 @@ - * @version 21072012 - * @package MyRadio_Core + * If you don't know what a 404 page is, you have a long way to go. */ +use \MyRadio\MyRadio\CoreUtils; $twig = CoreUtils::getTemplateObject(); -require_once 'Views/Errors/404.php'; \ No newline at end of file +require 'Views/Errors/404.php'; diff --git a/src/Controllers/Errors/Maintenance.php b/src/Controllers/Errors/Maintenance.php new file mode 100644 index 000000000..76a6f26e6 --- /dev/null +++ b/src/Controllers/Errors/Maintenance.php @@ -0,0 +1,8 @@ + [ + 'missing start or end' + ] + ]); + exit; +} + +$data = CoreUtils::setToDataSource(MyRadio_Event::getInRange( + $_REQUEST['start'], + $_REQUEST['end'] +)); + +URLUtils::dataToJSON($data); diff --git a/src/Controllers/Events/addToCalendar.php b/src/Controllers/Events/addToCalendar.php new file mode 100644 index 000000000..cbcf85f4d --- /dev/null +++ b/src/Controllers/Events/addToCalendar.php @@ -0,0 +1,29 @@ +setTemplate('Events/addToCalendar.twig') + ->addVariable('link', URLUtils::makeURL('Events', 'iCal', [ + 'token' => $token + ])) + ->render(); diff --git a/src/Controllers/Events/default.php b/src/Controllers/Events/default.php new file mode 100644 index 000000000..d7c981161 --- /dev/null +++ b/src/Controllers/Events/default.php @@ -0,0 +1,7 @@ +setTemplate('Events/calendar.twig') + ->render(); diff --git a/src/Controllers/Events/deleteEvent.php b/src/Controllers/Events/deleteEvent.php new file mode 100644 index 000000000..9831d021a --- /dev/null +++ b/src/Controllers/Events/deleteEvent.php @@ -0,0 +1,20 @@ +checkEditPermissions(); +$event->delete(); + +URLUtils::redirectWithMessage( + 'Events', + 'default', + 'Event deleted.' +); diff --git a/src/Controllers/Events/duplicateEvent.php b/src/Controllers/Events/duplicateEvent.php new file mode 100644 index 000000000..2c2e9e67a --- /dev/null +++ b/src/Controllers/Events/duplicateEvent.php @@ -0,0 +1,25 @@ +checkEditPermissions(); + +$vals = $event->toDataSource(); +unset($vals['host']); +unset($vals['start']); +unset($vals['end']); +$vals['start_time'] = date('d/m/Y H:i', $event->getStartTime()); +$vals['end_time'] = date('d/m/Y H:i', $event->getEndTime()); + +MyRadio_Event::getForm() + ->setValues($vals) + ->setSubtitle('Duplicating event ' . $event->getTitle() . '.') + ->render(); diff --git a/src/Controllers/Events/editEvent.php b/src/Controllers/Events/editEvent.php new file mode 100644 index 000000000..1280e36a8 --- /dev/null +++ b/src/Controllers/Events/editEvent.php @@ -0,0 +1,56 @@ +readValues(); + if (empty($data['id'])) { + // create new + $event = MyRadio_Event::create($data); + URLUtils::redirectWithMessage( + 'Events', + 'viewEvent', + 'Your event has been created.', + [ + 'eventid' => $event->getID() + ] + ); + } else { + // edit + /** @var MyRadio_Event $event */ + $event = MyRadio_Event::getInstance($data['id']); + + // check permissions + $event->checkEditPermissions(); + + $event->update($data); + + URLUtils::redirectWithMessage( + 'Events', + 'viewEvent', + 'Event updated.', + [ + 'eventid' => $event->getID() + ] + ); + } +} else { + if (isset($_REQUEST['eventid'])) { + // editing + /** @var MyRadio_Event $event */ + $event = MyRadio_Event::getInstance($_REQUEST['eventid']); + + // check permissions + $event->checkEditPermissions(); + + $event->getEditForm()->render(); + } else { + // creating new + MyRadio_Event::getForm() + ->setFieldValue('host', MyRadio_User::getInstance()) + ->render(); + } +} diff --git a/src/Controllers/Events/iCal.php b/src/Controllers/Events/iCal.php new file mode 100644 index 000000000..742f5747c --- /dev/null +++ b/src/Controllers/Events/iCal.php @@ -0,0 +1,43 @@ + [ + 'no_token' + ] + ]); + exit; +} + +$memberid = MyRadio_Event::validateCalendarToken($_GET['token']); +if ($memberid === null) { + URLUtils::dataToJSON([ + 'myradio_errors' => [ + 'invalid' + ] + ]); + exit; +} +// TODO: when we support RSVPs, filter here + +$events = MyRadio_Event::getNext(25); + +$cal = Calendar::create(Config::$long_name) + ->refreshInterval(360); +foreach ($events as $evt) { + $cal = $cal->event($evt->toIcalEvent()); +} + +$upcomingShows = MyRadio_Timeslot::getUserNextTimeslots($memberid, 25); +foreach ($upcomingShows as $ts) { + $cal = $cal->event($ts->toIcalEvent()); +} + +header('Content-Type: text/calendar'); +echo $cal->get(); diff --git a/src/Controllers/Events/viewEvent.php b/src/Controllers/Events/viewEvent.php new file mode 100644 index 000000000..f5776e116 --- /dev/null +++ b/src/Controllers/Events/viewEvent.php @@ -0,0 +1,10 @@ +setTemplate('Events/viewEvent.twig') + ->addVariable('event', $event) + ->render(); diff --git a/src/Controllers/Library/acceptTrackCorrection.php b/src/Controllers/Library/acceptTrackCorrection.php index 6ff5d87a7..a44a8091e 100644 --- a/src/Controllers/Library/acceptTrackCorrection.php +++ b/src/Controllers/Library/acceptTrackCorrection.php @@ -1,18 +1,17 @@ - * @version 20130720 - * @package MyRadio_Library */ +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_TrackCorrection; if (isset($_REQUEST['correctionid'])) { - $correction = MyRadio_TrackCorrection::getInstance($_REQUEST['correctionid']); + $correction = MyRadio_TrackCorrection::getInstance($_REQUEST['correctionid']); } else { - throw new MyRadioException('Correctionid is required!', 400); + throw new MyRadioException('Correctionid is required!', 400); } $correction->apply(empty($_REQUEST['ignorealbum']) ? false : (bool) $_REQUEST['ignorealbum']); -CoreUtils::backWithMessage('The correction was applied succesfully!'); \ No newline at end of file +URLUtils::backWithMessage('The correction was applied successfully!'); diff --git a/src/Controllers/Library/addTrack.php b/src/Controllers/Library/addTrack.php index dbda85b20..298aa8eb1 100644 --- a/src/Controllers/Library/addTrack.php +++ b/src/Controllers/Library/addTrack.php @@ -1,4 +1,13 @@ setTemplate('MyRadio/text.twig') - ->addVariable('text', '') - ->render(); \ No newline at end of file + ->addVariable('title', 'Upload Track') + ->addVariable( + 'text', + '' + )->render(); diff --git a/src/Controllers/Library/default.php b/src/Controllers/Library/default.php index 38b59728f..ec63afd6c 100644 --- a/src/Controllers/Library/default.php +++ b/src/Controllers/Library/default.php @@ -2,8 +2,14 @@ /* * Initial buggeration of the Library Management Page - * @author Anthony Williams - * @version 25072012 * @package MyRadio_Library */ +use \MyRadio\MyRadio\CoreUtils; + +CoreUtils::getTemplateObject()->setTemplate('MyRadio/text.twig') + ->addVariable('title', 'Library') + ->addVariable( + 'text', + 'This part of MyRadio allows you to do some library management.' + )->render(); diff --git a/src/Controllers/Library/doEditTrack.php b/src/Controllers/Library/doEditTrack.php deleted file mode 100644 index c87734fc3..000000000 --- a/src/Controllers/Library/doEditTrack.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @version 25042013 - * @package MyRadio_Library - */ - -//The Form definition -require 'Models/Library/trackfrm.php'; - -$data = $form->readValues(); - -$track = MyRadio_Track::getInstance($data['id']); -$track->setTitle($data['title']); -$track->setArtist($data['artist']); -$track->setAlbum($data['album']); - -require 'Views/MyRadio/back.php'; \ No newline at end of file diff --git a/src/Controllers/Library/editTrack.php b/src/Controllers/Library/editTrack.php index f5f1d68f4..3e1dff4c1 100644 --- a/src/Controllers/Library/editTrack.php +++ b/src/Controllers/Library/editTrack.php @@ -1,20 +1,36 @@ - * @version 20130722 - * @package MyRadio_Library + * Allows URY Librarians to create edit Tracks. */ +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Track; +use \MyRadio\MyRadioException; -//The Form definition -require 'Models/Library/trackfrm.php'; +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = MyRadio_Track::getForm()->readValues(); -$track = MyRadio_Track::getInstance($_REQUEST['trackid']); + $track = MyRadio_Track::getInstance($data['id']); + $track->setTitle($data['title']); + $track->setArtist($data['artist']); + $track->setAlbum($data['album']); + $track->setPosition($data['position']); + $track->setIntro($data['intro']); + $track->setOutro($data['outro']); + $track->setClean($data['clean']); + $track->setGenre($data['genre']); + $track->setDigitised($data['digitised']); + $track->setBlacklisted($data['blacklisted']); + $track->setLastEdited(); -$form->editMode($track->getID(), - array( - 'title' => $track->getTitle(), - 'artist' => $track->getArtist(), - 'album' => $track->getAlbum()->getID() - ))->render(); \ No newline at end of file + URLUtils::backWithMessage('Track Updated.'); +} else { + //Not Submitted + if (isset($_REQUEST['trackid'])) { + MyRadio_Track::getInstance($_REQUEST['trackid']) + ->getEditForm() + ->render(); + } else { + throw new MyRadioException('A TrackID to edit has not been provided, please try again.', 400); + } +} diff --git a/src/Controllers/Library/findDuplicate.php b/src/Controllers/Library/findDuplicate.php index ba69b496d..dc3d0e2bc 100644 --- a/src/Controllers/Library/findDuplicate.php +++ b/src/Controllers/Library/findDuplicate.php @@ -1,68 +1,80 @@ - * @version 24052013 - * @package MyRadio_Library + * Scan music library, finding tracks that seem to exist more than once. */ +use \MyRadio\Database; +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_Track; + //This stores the last ID checked so that we can collect Tracks in batches, preventing us from using ALL the RAM $finalid = 0; //Stores trackids of tracks that have been searched through already -$alreadydone = array(); +$alreadydone = []; //Stores MyRadio_Tracks that are duplicates -$duplicates = array(); +$duplicates = []; //We use this here to keep track of the query counter $db = Database::getInstance(); //If query count goes over this, bail and show what you've found so far $query_limit = 25000; do { - //Get the next batch of tracks from where we left off - $tracks = MyRadio_Track::findByOptions(array('limit' => 500, 'digitised' => true, 'idsort' => true, - 'custom' => 'trackid > ' . $finalid)); + //Get the next batch of tracks from where we left off + $tracks = MyRadio_Track::findByOptions( + [ + 'limit' => 500, + 'digitised' => true, + 'idsort' => true, + 'custom' => 'trackid > '.$finalid, + ] + ); - foreach ($tracks as $track) { - //If this has already appeared as a duplicate, don't search again or we'll duplicate the duplications - if (in_array($track->getID(), $alreadydone)) - continue; + foreach ($tracks as $track) { + //If this has already appeared as a duplicate, don't search again or we'll duplicate the duplications + if (in_array($track->getID(), $alreadydone)) { + continue; + } - //Find tracks that match this name and artist - $matches = MyRadio_Track::findByOptions( - array('title' => $track->getTitle(), - 'artist' => $track->getArtist(), - 'limit' => 0, - 'precise' => true) - ); + //Find tracks that match this name and artist + $matches = MyRadio_Track::findByOptions( + [ + 'title' => $track->getTitle(), + 'artist' => $track->getArtist(), + 'limit' => 0, + 'precise' => true, + ] + ); - //If there's more than one match, then there are duplicates for this item - if (sizeof($matches) > 1) { - foreach ($matches as $match) { - $alreadydone[] = $match->getID(); - $duplicates[] = $match; - } - } + //If there's more than one match, then there are duplicates for this item + if (sizeof($matches) > 1) { + foreach ($matches as $match) { + $alreadydone[] = $match->getID(); + $duplicates[] = $match; + } + } - //Log the latest ID used - $finalid = $track->getID(); + //Log the latest ID used + $finalid = $track->getID(); - //Remove the Singleton store's built in reference to this track to reduce memory usage - $track->removeInstance(); - unset($track); + //Remove the Singleton store's built in reference to this track to reduce memory usage + $track->removeInstance(); + unset($track); - //Kill the loop if we've used too many queries - if ($db->getCounter() > $query_limit) - break; - } - echo "$finalid ({$db->getCounter()}/" . sizeof($duplicates) . ")
          "; - gc_collect_cycles(); + //Kill the loop if we've used too many queries + if ($db->getCounter() > $query_limit) { + break; + } + } + echo "$finalid ({$db->getCounter()}/".sizeof($duplicates).')
          '; + gc_collect_cycles(); - if ($db->getCounter() > $query_limit) - break; + if ($db->getCounter() > $query_limit) { + break; + } } while (!empty($tracks)); CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('tablescript', 'myury.datatable.default') - ->addVariable('title', 'Duplicate Tracks') - ->addVariable('tabledata', CoreUtils::dataSourceParser($duplicates)) - ->render(); \ No newline at end of file + ->addVariable('tablescript', 'myradio.datatable.default') + ->addVariable('title', 'Duplicate Tracks') + ->addVariable('tabledata', CoreUtils::dataSourceParser($duplicates)) + ->render(); diff --git a/src/Controllers/Library/findMissing.php b/src/Controllers/Library/findMissing.php index 4b7f022ed..12b6f69e6 100644 --- a/src/Controllers/Library/findMissing.php +++ b/src/Controllers/Library/findMissing.php @@ -1,31 +1,31 @@ - * @version 24042013 - * @package MyRadio_Library + * Scan music library, filling in blanks or changing default values. */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Track; $tracks = MyRadio_Track::getAllDigitised(); -$missing = array(); +$missing = []; foreach ($tracks as $track) { - if (!$track->checkForAudioFile()) { - $missing[] = $track; - if (isset($_GET['fix'])) { - $track->setDigitised(false); + if (!$track->checkForAudioFile()) { + $missing[] = $track; + if (isset($_GET['fix'])) { + $track->setDigitised(false); + } } - } } - CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('tablescript', 'myury.datatable.default') - ->addVariable('title', 'Missing Track Files') - ->addVariable('tabledata', CoreUtils::dataSourceParser($missing)) - ->addInfo('Please ensure the information below seems correct, then
          click here to mark these files - as undigitised.', - 'wrench') - ->render(); \ No newline at end of file + ->addVariable('tablescript', 'myradio.library.findMissing') + ->addVariable('title', 'Missing Track Files') + ->addVariable('tabledata', CoreUtils::dataSourceParser($missing)) + ->addInfo( + 'Please ensure the information below seems correct, then click here to mark these files as undigitised.', + 'wrench' + )->render(); diff --git a/src/Controllers/Library/findWrong.php b/src/Controllers/Library/findWrong.php index ed566f0b6..cbac1bffa 100644 --- a/src/Controllers/Library/findWrong.php +++ b/src/Controllers/Library/findWrong.php @@ -1,46 +1,50 @@ - * @version 24042013 - * @package MyRadio_Library + * Scan music library, finding tracks that don't seem to be where they should be. */ +use \MyRadio\Config; +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Track; -$wrong = array(); +$wrong = []; foreach (Config::$music_central_db_exts as $ext) { - foreach (glob(Config::$music_central_db_path.'/records/*/*.'.$ext) as $file) { - $recordid = preg_replace('/^.*\/([0-9]+)\/[0-9]+\.'.$ext.'$/', '$1', $file); - $trackid = preg_replace('/^.*\/([0-9]+)\.'.$ext.'$/', '$1', $file); + foreach (glob(Config::$music_central_db_path.'/records/*/*.'.$ext) as $file) { + $recordid = preg_replace('/^.*\/([0-9]+)\/[0-9]+\.'.$ext.'$/', '$1', $file); + $trackid = preg_replace('/^.*\/([0-9]+)\.'.$ext.'$/', '$1', $file); - try { - $track = MyRadio_Track::getInstance($trackid); - if ($track->getAlbum()->getID() != $recordid) { - $wrong[] = array($file, $track->getAlbum()->getID()); + try { + $track = MyRadio_Track::getInstance($trackid); + if ($track->getAlbum()->getID() != $recordid) { + $wrong[] = [$file, $track->getAlbum()->getID()]; - if (isset($_GET['fix'])) { - if (!is_dir(Config::$music_central_db_path.'/records/'.$track->getAlbum()->getID())) - mkdir(Config::$music_central_db_path.'/records/'.$track->getAlbum()->getID()); - if (copy($file, $track->getPath($ext))) { - unlink($file); - } - } - } + if (isset($_GET['fix'])) { + if (!is_dir(Config::$music_central_db_path.'/records/'.$track->getAlbum()->getID())) { + mkdir(Config::$music_central_db_path.'/records/'.$track->getAlbum()->getID()); + } + if (copy($file, $track->getPath($ext))) { + unlink($file); + } + } + } - $track->removeInstance(); - unset($track); - } catch (MyRadioException $e) { - $wrong[] = array($file, 0); + $track->removeInstance(); + unset($track); + } catch (MyRadioException $e) { + $wrong[] = [$file, 0]; + } } - } } CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('tablescript', 'myury.datatable.default') - ->addVariable('title', 'Misplaced Tracks') - ->addVariable('tabledata', CoreUtils::dataSourceParser($wrong)) - ->addInfo('Please ensure the information below seems correct, then click here to auto-move files that - have a guessed correct location.', - 'wrench') - ->render(); \ No newline at end of file + ->addVariable('tablescript', 'myradio.datatable.default') + ->addVariable('title', 'Misplaced Tracks') + ->addVariable('tabledata', CoreUtils::dataSourceParser($wrong)) + ->addInfo( + 'Please ensure the information below seems correct, then click here to auto-move files that have a guessed correct location.', + 'wrench' + )->render(); diff --git a/src/Controllers/Library/gapFiller.php b/src/Controllers/Library/gapFiller.php index 2e954bade..02686352f 100644 --- a/src/Controllers/Library/gapFiller.php +++ b/src/Controllers/Library/gapFiller.php @@ -1,37 +1,44 @@ - * @version 21042013 - * @package MyRadio_Library + * Scan music library, filling in blanks or changing default values. */ +use \MyRadio\Config; +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_Album; $albums = MyRadio_Album::findByName(Config::$short_name, 10); -$cacher = APCProvider::getInstance(); +$cache = Config::$cache_provider; +$cacher = $cache::getInstance(); $checked = $cacher->get('myradioLibraryGapFillerCheckedTracks'); -if (!is_array($checked)) $checked = array(); +if (!is_array($checked)) { + $checked = []; +} $limit = 150; -$updated = array(); +$updated = []; foreach ($albums as $album) { - $tracks = $album->getTracks(); - foreach ($tracks as $track) { - if ($limit <= 0) break; - if (in_array($track->getID(), $checked)) continue; - $track->updateInfoFromLastfm(); - $updated[] = $track; - $checked[] = $track->getID(); - usleep(200000); - $limit--; - } + $tracks = $album->getTracks(); + foreach ($tracks as $track) { + if ($limit <= 0) { + break; + } + if (in_array($track->getID(), $checked)) { + continue; + } + $track->updateInfoFromLastfm(); + $updated[] = $track; + $checked[] = $track->getID(); + usleep(200000); + --$limit; + } } $cacher->set('myradioLibraryGapFillerCheckedTracks', $checked); CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('tablescript', 'myury.library.gapfiller') - ->addVariable('title', 'Updated Tracks') - ->addVariable('tabledata', CoreUtils::dataSourceParser($updated)) - ->render(); \ No newline at end of file + ->addVariable('tablescript', 'myradio.library.gapfiller') + ->addVariable('title', 'Updated Tracks') + ->addVariable('tabledata', CoreUtils::dataSourceParser($updated)) + ->render(); diff --git a/src/Controllers/Library/rejectTrackCorrection.php b/src/Controllers/Library/rejectTrackCorrection.php index b37e54c91..bdcf2ab4a 100644 --- a/src/Controllers/Library/rejectTrackCorrection.php +++ b/src/Controllers/Library/rejectTrackCorrection.php @@ -1,21 +1,20 @@ - * @version 20130722 - * @package MyRadio_Library */ +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_TrackCorrection; if (isset($_REQUEST['correctionid'])) { - $correction = MyRadio_TrackCorrection::getInstance($_REQUEST['correctionid']); + $correction = MyRadio_TrackCorrection::getInstance($_REQUEST['correctionid']); } else { - throw new MyRadioException('Correctionid is required!', 400); + throw new MyRadioException('Correctionid is required!', 400); } -$correction->reject(empty($_REQUEST['permanent']) ? false : (bool)$_REQUEST['permanent']); +$correction->reject(empty($_REQUEST['permanent']) ? false : (bool) $_REQUEST['permanent']); -CoreUtils::backWithMessage('The correction was applied succesfully!'); \ No newline at end of file +URLUtils::backWithMessage('The correction was applied successfully!'); diff --git a/src/Controllers/Library/search.php b/src/Controllers/Library/search.php index 9c825fce8..6ccb1aaab 100644 --- a/src/Controllers/Library/search.php +++ b/src/Controllers/Library/search.php @@ -1,3 +1,58 @@ addInfo('SEARCH PAGE GOES HERE') - ->render(); +/** + * Allows URY Librarians to search for Tracks. + */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Track; +use \MyRadio\MyRadio\MyRadioForm; +use \MyRadio\MyRadio\MyRadioFormField; + +$form = new MyRadioForm( + 'lib_search', + 'Library', + 'search', + [ + 'title' => 'Library Search', + ] +); + +$form->addField( + new MyRadioFormField( + 'title', + MyRadioFormField::TYPE_TEXT, + ['required' => false, 'label' => 'Title', 'placeholder' => 'Filter by track title...'] + ) +)->addField( + new MyRadioFormField( + 'artist', + MyRadioFormField::TYPE_TEXT, + ['required' => false, 'label' => 'Artist', 'placeholder' => 'Filter by artist name...'] + ) +); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = $form->readValues(); + + if (isset($data['title']) || isset($data['artist'])) { + $tracks = MyRadio_Track::findByOptions( + [ + 'title' => isset($data['title']) ? $data['title'] : '', + 'artist' => isset($data['artist']) ? $data['artist'] : '', + 'limit' => 30 + ] + ); + } else { + $tracks = null; + } +} else { + $tracks = null; +} +$tableData = CoreUtils::dataSourceParser($tracks); + +$form->setTemplate('Library/search.twig') + ->render([ + 'tabledata' => $tableData, + 'tablescript' => 'myradio.library.search', + ]); diff --git a/src/Controllers/Library/viewTrackCorrection.php b/src/Controllers/Library/viewTrackCorrection.php index 95410ee90..87903cc81 100644 --- a/src/Controllers/Library/viewTrackCorrection.php +++ b/src/Controllers/Library/viewTrackCorrection.php @@ -1,26 +1,24 @@ - * @version 20130720 - * @package MyRadio_Library */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_TrackCorrection; if (isset($_REQUEST['correctionid']) && is_numeric($_REQUEST['correctionid'])) { - $correction = MyRadio_TrackCorrection::getInstance($_REQUEST['correctionid']); + $correction = MyRadio_TrackCorrection::getInstance($_REQUEST['correctionid']); } else { - $correction = MyRadio_TrackCorrection::getRandom(); + $correction = MyRadio_TrackCorrection::getRandom(); } if (empty($correction)) { - CoreUtils::getTemplateObject()->setTemplate('MyRadio/text.twig') - ->addVariable('title', 'Central Database Metadata Correction Proposal Review') - ->addVariable('text', 'There are no proposals to review right now.') - ->render(); + CoreUtils::getTemplateObject()->setTemplate('MyRadio/text.twig') + ->addVariable('title', 'Central Database Metadata Correction Proposal Review') + ->addVariable('text', 'There are no proposals to review right now.') + ->render(); } else { - CoreUtils::getTemplateObject()->setTemplate('Library/viewTrackCorrection.twig') - ->addVariable('title', 'Central Database Metadata Correction Proposal Review') - ->addVariable('correction', $correction->toDataSource()) - ->render(); -} \ No newline at end of file + CoreUtils::getTemplateObject()->setTemplate('Library/viewTrackCorrection.twig') + ->addVariable('title', 'Central Database Metadata Correction Proposal Review') + ->addVariable('correction', $correction->toDataSource()) + ->render(); +} diff --git a/src/Controllers/Mail/archive.php b/src/Controllers/Mail/archive.php index b1fa619ea..7d94b4d0c 100644 --- a/src/Controllers/Mail/archive.php +++ b/src/Controllers/Mail/archive.php @@ -1,21 +1,30 @@ - * @version 20130828 - * @package MyRadio_Mail + * Lists the archive for a Mailing List. */ +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_List; +use \MyRadio\ServiceAPI\MyRadio_User; $list = MyRadio_List::getInstance($_REQUEST['list']); -if (!$list->isMember(MyRadio_User::getInstance())) { - throw new MyRadioException('You can only view archives for Lists you are a' - . ' member of.', 403); +if (!$list->isMember(MyRadio_User::getInstance()->getID())) { + URLUtils::backWithMessage( + 'You can only view archives for Lists you are a' + .' member of.' + ); +} + +$archive = CoreUtils::dataSourceParser($list->getArchive()); + +foreach ($archive as $key => $value) { + $archive[$key]['timestamp'] = date('Y/m/d H:i', $archive[$key]['timestamp']); } CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('tablescript', 'myury.datatable.default') - ->addVariable('title', $list->getName().' Archive') - ->addVariable('tabledata', CoreUtils::dataSourceParser($list->getArchive(), false)) - ->render(); \ No newline at end of file + ->addVariable('tablescript', 'myradio.mail.archive') + ->addVariable('title', $list->getName().' Archive') + ->addVariable('tabledata', $archive) + ->render(); diff --git a/src/Controllers/Mail/default.php b/src/Controllers/Mail/default.php index fa053332d..c404da777 100644 --- a/src/Controllers/Mail/default.php +++ b/src/Controllers/Mail/default.php @@ -1,17 +1,29 @@ - * @version 20130526 - * @package MyRadio_Mail + * Lists all mailing lists. + * + * @todo Datatable niceness */ +use \MyRadio\Config; +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_List; +use \MyRadio\ServiceAPI\MyRadio_User; CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('tablescript', 'myury.mail.default') - ->addVariable('title', 'All Mailing Lists') - ->addVariable('tabledata', CoreUtils::dataSourceParser(MyRadio_List::getAllLists())) - ->addInfo('You will only get any messages from '.Config::$short_name.' if you are set to "Receive Email" on your profile.') - ->render(); + ->addVariable('tablescript', 'myradio.mail.default') + ->addVariable('title', 'All Mailing Lists') + ->addVariable( + 'tabledata', + CoreUtils::dataSourceParser( + MyRadio_List::getAllLists(!MyRadio_User::getCurrentUser()->isOfficer()), + ['actions'] + ) + ) + ->addInfo( + 'You will only get any messages from ' + .Config::$short_name + .' if you are set to "Receive Email" on your profile.' + )->render(); diff --git a/src/Controllers/Mail/doSend.php b/src/Controllers/Mail/doSend.php deleted file mode 100644 index 25a706c4a..000000000 --- a/src/Controllers/Mail/doSend.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @version 20130526 - * @package MyRadio_Mail - */ - -$info = MyRadio_JsonFormLoader::loadFromModule( - $module, 'send', 'doSend' -)->readValues(); - -MyRadioEmail::sendEmailToList(MyRadio_List::getInstance($info['list']), $info['subject'], $info['body'], MyRadio_User::getInstance()); - -CoreUtils::backWithMessage('Message sent!'); diff --git a/src/Controllers/Mail/optin.php b/src/Controllers/Mail/optin.php index e988176a8..e10617669 100644 --- a/src/Controllers/Mail/optin.php +++ b/src/Controllers/Mail/optin.php @@ -1,24 +1,30 @@ - * @version 20130526 - * @package MyRadio_Mail + * Opt in to a mailing list. */ +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\AuthUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_List; +use \MyRadio\ServiceAPI\MyRadio_User; -if (!isset($_REQUEST['list'])) throw new MyRadioException('List ID was not provided!', 400); +if (!isset($_REQUEST['list'])) { + throw new MyRadioException('List ID was not provided!', 400); +} if (isset($_REQUEST['memberid'])) { - CoreUtils::requirePermission(AUTH_EDITANYPROFILE); - $user = $_REQUEST['memberid']; + AuthUtils::requirePermission(AUTH_EDITANYPROFILE); + $user = $_REQUEST['memberid']; } else { - $user = -1; + $user = -1; } $list = MyRadio_List::getInstance($_REQUEST['list']); -if ($list->optin(MyRadio_User::getInstance($user))) { - CoreUtils::backWithMessage('You are now subscribed to '.$list->getName().'.'); +if ($list->optin(MyRadio_User::getInstance($user)->getID())) { + URLUtils::backWithMessage('You are now subscribed to '.$list->getName().'.'); } else { - CoreUtils::backWithMessage('You could not be subscribed at this time. You may already have opted-in or it may not be an open mailing list.'); -} \ No newline at end of file + URLUtils::backWithMessage( + 'You could not be subscribed at this time. ' + .'You may already have opted-in or it may not be an open mailing list.' + ); +} diff --git a/src/Controllers/Mail/optout.php b/src/Controllers/Mail/optout.php index e693618d4..05c0abb6f 100644 --- a/src/Controllers/Mail/optout.php +++ b/src/Controllers/Mail/optout.php @@ -1,24 +1,27 @@ - * @version 20130527 - * @package MyRadio_Mail + * Opt out of a mailing list. */ +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\AuthUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_List; +use \MyRadio\ServiceAPI\MyRadio_User; -if (!isset($_REQUEST['list'])) throw new MyRadioException('List ID was not provided!', 400); +if (!isset($_REQUEST['list'])) { + throw new MyRadioException('List ID was not provided!', 400); +} if (isset($_REQUEST['memberid'])) { - CoreUtils::requirePermission(AUTH_EDITANYPROFILE); - $user = $_REQUEST['memberid']; + AuthUtils::requirePermission(AUTH_EDITANYPROFILE); + $user = $_REQUEST['memberid']; } else { - $user = -1; + $user = -1; } $list = MyRadio_List::getInstance($_REQUEST['list']); -if ($list->optout(MyRadio_User::getInstance($user))) { - CoreUtils::backWithMessage('You are now opted-out of '.$list->getName().'.'); +if ($list->optout(MyRadio_User::getInstance($user)->getID())) { + URLUtils::backWithMessage('You are now opted-out of '.$list->getName().'.'); } else { - CoreUtils::backWithMessage('You could not be opted-out at this time. You may already have opted-out.'); -} \ No newline at end of file + URLUtils::backWithMessage('You could not be opted-out at this time. You may already have opted-out.'); +} diff --git a/src/Controllers/Mail/send.php b/src/Controllers/Mail/send.php index 94181aa19..34a937184 100644 --- a/src/Controllers/Mail/send.php +++ b/src/Controllers/Mail/send.php @@ -1,25 +1,77 @@ - * @version 20130526 - * @package MyRadio_Mail + * Send an email to a mailing list. */ +use \MyRadio\Config; +use \MyRadio\MyRadioException; +use \MyRadio\MyRadioEmail; +use \MyRadio\MyRadio\AuthUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\MyRadio\MyRadioForm; +use \MyRadio\MyRadio\MyRadioFormField; +use \MyRadio\ServiceAPI\MyRadio_List; +use \MyRadio\ServiceAPI\MyRadio_User; -if (!isset($_REQUEST['list'])) { - throw new MyRadioException('List ID was not provided!', 400); -} -if (!MyRadio_List::getInstance($_REQUEST['list'])->isPublic()) { - CoreUtils::requirePermission(AUTH_MAILALLMEMBERS); -} - -MyRadio_JsonFormLoader::loadFromModule( - $module, 'send', 'doSend' -)->setFieldValue( - 'list', $_REQUEST['list'] -)->setTemplate( - 'Mail/send.twig' -)->render( - ['rcpt_str' => MyRadio_List::getInstance($_REQUEST['list'])->getName()] +$form = ( + new MyRadioForm( + 'mail_send', + $module, + $action, + [ + 'debug' => true, + 'title' => 'Send Email', + ] + ) +)->addField( + new MyRadioFormField( + 'subject', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Subject', + 'placeholder' => 'Subject (['.Config::$short_name.'] is prefixed automatically)', + ] + ) +)->addField( + new MyRadioFormField( + 'body', + MyRadioFormField::TYPE_BLOCKTEXT, + ['label' => 'Message Body',] + ) +)->addField( + new MyRadioFormField( + 'list', + MyRadioFormField::TYPE_HIDDEN, + [] + ) ); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = $form->readValues(); + + MyRadioEmail::sendEmailToList( + MyRadio_List::getInstance($data['list']), + $data['subject'], + $data['body'], + MyRadio_User::getInstance() + ); + + URLUtils::backWithMessage('Message sent!'); +} else { + //Not Submitted + if (!isset($_REQUEST['list'])) { + throw new MyRadioException('List ID was not provided!', 400); + } + if (!MyRadio_List::getInstance($_REQUEST['list'])->isPublic()) { + AuthUtils::requirePermission(AUTH_MAILALLMEMBERS); + } + + $form->setFieldValue( + 'list', + $_REQUEST['list'] + )->setTemplate( + 'Mail/send.twig' + )->render( + ['rcpt_str' => MyRadio_List::getInstance($_REQUEST['list'])->getName()] + ); +} diff --git a/src/Controllers/Mail/view.php b/src/Controllers/Mail/view.php index e869d21aa..8d488c012 100644 --- a/src/Controllers/Mail/view.php +++ b/src/Controllers/Mail/view.php @@ -1,21 +1,24 @@ - * @version 20130828 - * @package MyRadio_Mail - * @todo Uses unsanitised HTTP_REFERER + * Lists the archive for a Mailing List. + * + * @todo Uses unsanitised HTTP_REFERER */ +use \MyRadio\MyRadioException; +use \MyRadio\MyRadioEmail; +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_User; $email = MyRadioEmail::getInstance($_REQUEST['emailid']); if (!$email->isRecipient(MyRadio_User::getInstance())) { - throw new MyRadioException('You can only view emails you are a recipient of.', 403); + throw new MyRadioException('You can only view emails you are a recipient of.', 403); } CoreUtils::getTemplateObject()->setTemplate('MyRadio/text.twig') - ->addVariable('title', $email->getSubject()) - ->addVariable('text', 'Back
          '. - $email->getViewableBody()) - ->render(); \ No newline at end of file + ->addVariable('title', $email->getSubject()) + ->addVariable( + 'text', + 'Back
          ' + .$email->getViewableBody() + )->render(); diff --git a/src/Controllers/MyRadio/StaticProxy.php b/src/Controllers/MyRadio/StaticProxy.php index e2cd6bf3f..19c2ccee7 100644 --- a/src/Controllers/MyRadio/StaticProxy.php +++ b/src/Controllers/MyRadio/StaticProxy.php @@ -2,40 +2,37 @@ /** * For versions other than the default, static content is not linked directly to the web. * This provides access to these, at the cost of a substantial overhead. - * - * @author Lloyd Wallis - * @version 20130712 - * @package MyRadio_Core */ - if (empty($_GET[0])) { - require 'default.php'; - exit; + require 'default.php'; + exit; } //For config.js, this is a Controller in this module. if ($_GET[0] === 'config.js') { - require __DIR__.'/config.js.php'; - exit; + require __DIR__.'/config.js.php'; + exit; } $prefix = __DIR__.'/../../Public/'; -foreach (array(__DIR__.'/../../Public/', __DIR__.'/../../Public/js/skins/lightgray/') as $p) { - if (file_exists($p.$_GET[0])) { - $prefix = $p; - break; - } +foreach ([__DIR__.'/../../Public/', __DIR__.'/../../Public/js/vendor/skins/lightgray/'] as $p) { + if (file_exists($p.$_GET[0])) { + $prefix = $p; + break; + } } -if (strstr($_GET[0], '..') !== false) exit; +if (strstr($_GET[0], '..') !== false) { + exit; +} if (strtolower(substr($_GET[0], -3)) == 'css') { - $type = 'text/css'; + $type = 'text/css'; } elseif (strtolower(substr($_GET[0], -2)) == 'js') { - $type = 'text/javascript'; + $type = 'text/javascript'; } else { - $type = mime_content_type($prefix.$_GET[0]); + $type = mime_content_type($prefix.$_GET[0]); } header('Content-Type: '.$type); -echo file_get_contents($prefix.$_GET[0]); \ No newline at end of file +echo file_get_contents($prefix.$_GET[0]); diff --git a/src/Controllers/MyRadio/a-endjoyride.php b/src/Controllers/MyRadio/a-endjoyride.php index 4719ff9e3..8f9288e1a 100644 --- a/src/Controllers/MyRadio/a-endjoyride.php +++ b/src/Controllers/MyRadio/a-endjoyride.php @@ -1,12 +1,9 @@ - * @version 20130722 - * @package MyRadio_Core */ +use \MyRadio\MyRadio\URLUtils; unset($_SESSION['joyride']); -require 'Views/MyRadio/nocontent.php'; \ No newline at end of file +URLUtils::nocontent(); diff --git a/src/Controllers/MyRadio/a-findalbum.php b/src/Controllers/MyRadio/a-findalbum.php index 7029d368f..2f77b2599 100644 --- a/src/Controllers/MyRadio/a-findalbum.php +++ b/src/Controllers/MyRadio/a-findalbum.php @@ -2,18 +2,21 @@ /** * This is pretty much what every Controller should look like. * Some might include more than one model etc.... - * - * @todo Proper Documentation - * @author Lloyd Wallis - * @version 20130722 - * @package MyRadio_Core + * + * @todo Proper Documentation */ +use \MyRadio\Config; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Album; + if (isset($_REQUEST['id'])) { - $data = MyRadio_Album::getInstance($_REQUEST['id']); + $data = MyRadio_Album::getInstance($_REQUEST['id']); } elseif (!isset($_REQUEST['term'])) { - $data = array(); + $data = []; } else { - $data = MyRadio_Album::findByName($_REQUEST['term'], - isset($_REQUEST['limit']) ? intval($_REQUEST['limit']) : Config::$ajax_limit_default); + $data = MyRadio_Album::findByName( + $_REQUEST['term'], + isset($_REQUEST['limit']) ? intval($_REQUEST['limit']) : Config::$ajax_limit_default + ); } -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file +URLUtils::dataToJSON($data); diff --git a/src/Controllers/MyRadio/a-findartist.php b/src/Controllers/MyRadio/a-findartist.php index a276b0142..722cd2d22 100644 --- a/src/Controllers/MyRadio/a-findartist.php +++ b/src/Controllers/MyRadio/a-findartist.php @@ -2,13 +2,19 @@ /** * This is pretty much what every Controller should look like. * Some might include more than one model etc.... - * - * @todo Proper Documentation - * @author Lloyd Wallis - * @version 20130629 - * @package MyRadio_Core + * + * @todo Proper Documentation */ -if (!isset($_REQUEST['term'])) $data = array(); else { - $data = Artist::findByName($_REQUEST['term'], isset($_REQUEST['limit']) ? intval($_REQUEST['limit']) : Config::$ajax_limit_default); +use \MyRadio\Config; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Artist; + +if (!isset($_REQUEST['term'])) { + $data = []; +} else { + $data = MyRadio_Artist::findByName( + $_REQUEST['term'], + isset($_REQUEST['limit']) ? intval($_REQUEST['limit']) : Config::$ajax_limit_default + ); } -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file +URLUtils::dataToJSON($data); diff --git a/src/Controllers/MyRadio/a-findmember.php b/src/Controllers/MyRadio/a-findmember.php index 75906a62c..93a558e6a 100644 --- a/src/Controllers/MyRadio/a-findmember.php +++ b/src/Controllers/MyRadio/a-findmember.php @@ -1,14 +1,22 @@ - * @version 21072012 - * @package MyRadio_Core + * Some might include more than one model etc.... + * + * @todo Proper documentation */ -if (!isset($_REQUEST['term'])) throw new MyRadioException('Parameter \'term\' is required but was not provided'); +use \MyRadio\Config; +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_User; -$data = MyRadio_User::findByName($_REQUEST['term'], isset($_REQUEST['limit']) ? intval($_REQUEST['limit']) : Config::$ajax_limit_default); -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file +if (!isset($_REQUEST['term'])) { + throw new MyRadioException('Parameter \'term\' is required but was not provided'); +} + +$data = MyRadio_User::findByName( + $_REQUEST['term'], + isset($_REQUEST['limit']) ? intval($_REQUEST['limit']) : Config::$ajax_limit_default +); + +URLUtils::dataToJSON($data); diff --git a/src/Controllers/MyRadio/a-findtrack.php b/src/Controllers/MyRadio/a-findtrack.php index c30c6f3d6..910ff79f4 100644 --- a/src/Controllers/MyRadio/a-findtrack.php +++ b/src/Controllers/MyRadio/a-findtrack.php @@ -1,29 +1,30 @@ - * @version 20130626 - * @package MyRadio_Core */ +use \MyRadio\Config; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Track; + if (isset($_REQUEST['id'])) { $data = MyRadio_Track::getInstance((int) $_REQUEST['id']); } else { - - $data = MyRadio_Track::findByOptions(array( + $data = MyRadio_Track::findByOptions( + [ 'title' => isset($_REQUEST['term']) ? $_REQUEST['term'] : '', 'artist' => isset($_REQUEST['artist']) ? $_REQUEST['artist'] : '', 'limit' => isset($_REQUEST['limit']) ? intval($_REQUEST['limit']) : Config::$ajax_limit_default, 'digitised' => isset($_REQUEST['require_digitised']) ? (bool) $_REQUEST['require_digitised'] : false, - 'itonesplaylistid' => isset($_REQUEST['itonesplaylistid']) ? $_REQUEST['itonesplaylistid'] : '' - )); + 'itonesplaylistid' => isset($_REQUEST['itonesplaylistid']) ? $_REQUEST['itonesplaylistid'] : '', + ] + ); } -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file +URLUtils::dataToJSON($data); diff --git a/src/Controllers/MyRadio/a-getuploadprogress.php b/src/Controllers/MyRadio/a-getuploadprogress.php index 022e0ccd1..8ca159300 100644 --- a/src/Controllers/MyRadio/a-getuploadprogress.php +++ b/src/Controllers/MyRadio/a-getuploadprogress.php @@ -1,17 +1,15 @@ - * @version 20130816 - * @package MyRadio_Core + * Returns the APC upload progress data for the given upload ID. */ -if (function_exists("uploadprogress_get_info")) { - $data = uploadprogress_get_info($_REQUEST['id']); +use \MyRadio\MyRadio\URLUtils; + +if (function_exists('uploadprogress_get_info')) { + $data = uploadprogress_get_info($_REQUEST['id']); } else { - trigger_error('uploadprogress PECL extension is not installed.'); - $data = false; + trigger_error('uploadprogress PECL extension is not installed.'); + $data = false; } -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file +URLUtils::dataToJSON($data); diff --git a/src/Controllers/MyRadio/a-membernamefromid.php b/src/Controllers/MyRadio/a-membernamefromid.php index eb0b570e8..b82d5621c 100644 --- a/src/Controllers/MyRadio/a-membernamefromid.php +++ b/src/Controllers/MyRadio/a-membernamefromid.php @@ -1,14 +1,17 @@ - * @version 02082012 - * @package MyRadio_Core + * Some might include more than one model etc.... + * + * @todo Proper documentation */ -if (!isset($_REQUEST['term'])) throw new MyRadioException('Parameter \'term\' is required but was not provided'); +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_User; -$data = MyRadio_User::getInstance((int)$_REQUEST['term'])->getName(); -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file +if (!isset($_REQUEST['term'])) { + throw new MyRadioException('Parameter \'term\' is required but was not provided'); +} + +$data = MyRadio_User::getInstance((int) $_REQUEST['term'])->getName(); +URLUtils::dataToJSON($data); diff --git a/src/Controllers/MyRadio/a-readnews.php b/src/Controllers/MyRadio/a-readnews.php index 3815c06e7..1eaa4447f 100644 --- a/src/Controllers/MyRadio/a-readnews.php +++ b/src/Controllers/MyRadio/a-readnews.php @@ -1,13 +1,13 @@ - * @version 20130718 - * @package MyRadio_Core */ -MyRadioNews::markNewsAsRead((int)$_REQUEST['newsentryid'], MyRadio_User::getInstance()); -require 'Views/MyRadio/nocontent.php'; \ No newline at end of file +use \MyRadio\MyRadio\MyRadioNews; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_User; + +MyRadioNews::markNewsAsRead((int) $_REQUEST['newsentryid'], MyRadio_User::getInstance()); +URLUtils::nocontent(); diff --git a/src/Controllers/MyRadio/a-timeslotSignin.php b/src/Controllers/MyRadio/a-timeslotSignin.php index 064e34d4e..a75013b3a 100644 --- a/src/Controllers/MyRadio/a-timeslotSignin.php +++ b/src/Controllers/MyRadio/a-timeslotSignin.php @@ -1,18 +1,21 @@ - * @version 20140102 - * @package MyRadio_Core + * (if the user has access to this data). */ +use \MyRadio\MyRadio\AuthUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Timeslot; $ts = MyRadio_Timeslot::getInstance($_REQUEST['timeslotid']); if ($ts->getSeason()->getShow()->isCurrentUserAnOwner() - or CoreUtils::hasPermission(AUTH_EDITSHOWS)) { + || AuthUtils::hasPermission(AUTH_EDITSHOWS) +) { $data = $ts->getSigninInfo(); - require_once 'Views/MyRadio/datatojson.php'; + URLUtils::dataToJSON(array_map(function ($x) { + unset($x['guest_info']); + return $x; + }, $data)); } else { require_once 'Controllers/Errors/403.php'; -} \ No newline at end of file +} diff --git a/src/Controllers/MyRadio/actionPermissions.php b/src/Controllers/MyRadio/actionPermissions.php index 73c72f386..dc7c585e9 100644 --- a/src/Controllers/MyRadio/actionPermissions.php +++ b/src/Controllers/MyRadio/actionPermissions.php @@ -1,21 +1,111 @@ - * @package MyRadio_Core + * Provides a tool to manage permissions for MyRadio Service/Module/Action systems. */ +use \MyRadio\MyRadio\AuthUtils; +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\MyRadio\MyRadioForm; +use \MyRadio\MyRadio\MyRadioFormField; -/** - * Include the current permissions. This will be rendered in a DataTable. - */ -$data = CoreUtils::getAllActionPermissions(); -/** - * Include a form definition for adding permissions. +/* + * Form definition for adding permissions. */ -require 'Models/MyRadio/actionPermissionsForm.php'; -/** - * Pass it over to the actionPermissions view for output. - */ -require 'Views/MyRadio/actionPermissions.php'; \ No newline at end of file +$form = new MyRadioForm( + 'assign_action_permissions', + $module, + $action, + [ + 'title' => 'Permissions', + 'subtitle' => 'Assign Action Permissions', + ] +); + +$form->addField( + new MyRadioFormField( + 'module', + MyRadioFormField::TYPE_TEXT, + [ + 'explanation' => 'Type a Module to apply permissions to', + 'label' => 'Module', + ] + ) +)->addField( + new MyRadioFormField( + 'action', + MyRadioFormField::TYPE_TEXT, + [ + 'explanation' => 'Type an Action within that Module to apply permissions to. ' + .'Leave blank to apply it to all Actions.', + 'label' => 'Action', + 'required' => false, + ] + ) +)->addField( + new MyRadioFormField( + 'permission', + MyRadioFormField::TYPE_SELECT, + [ + 'explanation' => 'Select a permission that you want to add which when granted ' + .'allows a user to perform this Action. These use boolean OR, not AND so may not ' + .'stack as you would like depending on circumstances. Leave blank to allow global ' + .'access.', + 'label' => 'Permission', + 'required' => false, + 'options' => array_merge( + [ + [ + 'value' => null, + 'text' => 'GLOBAL ACCESS', + ], + ], + AuthUtils::getAllPermissions() + ), + ] + ) +); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = $form->readValues(); + + $setModule = CoreUtils::getModuleId($data['module']); + $setAction = CoreUtils::getActionId($setModule, $data['action']); + $permission = $data['permission']; + if (empty($setAction)) { + $setAction = null; + } + if (empty($permission)) { + $permission = null; + } + + AuthUtils::addActionPermission($setModule, $setAction, $permission); + + URLUtils::backWithMessage('The action permission has been updated.'); +} else { + //Not Submitted + //Include the current permissions. This will be rendered in a DataTable. + $data = AuthUtils::getAllActionPermissions(); + + /* + * Pass it over to the actionPermissions view for output. + */ + for ($i = 0; $i < sizeof($data); ++$i) { + $data[$i]['del'] = [ + 'display' => 'text', + 'url' => URLUtils::makeURL( + 'MyRadio', + 'removeActionPermission', + ['permissionid' => $data[$i]['actpermissionid']] + ), + 'value' => 'Delete', + ]; + } + $form->setTemplate('MyRadio/actionPermissions.twig') + ->render( + [ + 'tabledata' => $data, + 'tablescript' => 'myradio.core.actionPermissions', + ] + ); +} diff --git a/src/Controllers/MyRadio/addActionPermission.php b/src/Controllers/MyRadio/addActionPermission.php deleted file mode 100644 index 57fbcedcd..000000000 --- a/src/Controllers/MyRadio/addActionPermission.php +++ /dev/null @@ -1,12 +0,0 @@ - - * @version 24072012 - */ - -require 'Models/MyRadio/actionPermissionsForm.php'; -$data = $form->readValues(); -require 'Models/MyRadio/addActionPermission.php'; -require 'Views/MyRadio/back.php'; \ No newline at end of file diff --git a/src/Controllers/MyRadio/addNews.php b/src/Controllers/MyRadio/addNews.php old mode 100755 new mode 100644 index c06200d66..e1b6f9d6c --- a/src/Controllers/MyRadio/addNews.php +++ b/src/Controllers/MyRadio/addNews.php @@ -1,32 +1,23 @@ 'Add news item' - ) - ))->addField( - new MyRadioFormField('body', MyRadioFormField::TYPE_BLOCKTEXT, array( - 'explanation' => '', - 'label' => 'Content' - )) - )->addField( - new MyRadioFormField('feedid', MyRadioFormField::TYPE_HIDDEN) -); +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\MyRadio\MyRadioNews; if ($_SERVER['REQUEST_METHOD'] === 'POST') { //Submitted - $data = $form->readValues(); + $data = MyRadioNews::getForm()->readValues(); + MyRadioNews::addItem($data['feedid'], $data['body']); - header('Location: '.CoreUtils::makeURL('MyRadio', 'news', ['feed' =>$data['feedid']])); + + URLUtils::backWithMessage('News Updated!'); } else { //Not Submitted - $form->setFieldValue('feedid', $_REQUEST['feed'])->render(); -} \ No newline at end of file + MyRadioNews::getForm() + ->setFieldValue('feedid', $_REQUEST['feed']) + ->setFieldValue('body', MyRadioNews::getLatestNewsItem($_REQUEST['feed'])['content']) + ->render(); +} diff --git a/src/Controllers/MyRadio/addPermission.php b/src/Controllers/MyRadio/addPermission.php new file mode 100644 index 000000000..8df4a81eb --- /dev/null +++ b/src/Controllers/MyRadio/addPermission.php @@ -0,0 +1,60 @@ + 'Permissions', + 'subtitle' => 'New Permission', + ] +); +$form->addField( + new MyRadioFormField( + 'constant', + MyRadioFormField::TYPE_TEXT, + [ + 'explanation' => 'Type a constant name (AUTH_NAME) for the permission.', + 'label' => 'Name', + 'required' => true, + ] + ) +)->addField( + new MyRadioFormField( + 'description', + MyRadioFormField::TYPE_TEXT, + [ + 'explanation' => 'Type a description for what this permission allows.', + 'label' => 'Description', + 'required' => true, + ] + ) +); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = $form->readValues(); + + $constant = $data['constant']; + if (substr($constant, 0, 5) == 'AUTH_' && strlen($constant) >= 6) { + AuthUtils::addPermission($data['description'], $constant); + $message = 'The permission "'. $constant .'" has been added successfully.'; + URLUtils::redirectWithMessage('MyRadio', 'listPermissions', $message); + } else { + URLUtils::backWithMessage('The permission name should be a constant starting in "AUTH_".'); + } +} else { + //Not submitted + $form->render(); +} diff --git a/src/Controllers/MyRadio/chooseAuth.php b/src/Controllers/MyRadio/chooseAuth.php old mode 100755 new mode 100644 index ce9c521ad..df63ec2b1 --- a/src/Controllers/MyRadio/chooseAuth.php +++ b/src/Controllers/MyRadio/chooseAuth.php @@ -1,14 +1,18 @@ setAuthProvider($_REQUEST['authenticator']); -//If it's not the Default authenticator, delete the password -if ($_REQUEST['authenticator'] !== 'MyRadioDefaultAuthenticator') { +//If it's not the Default authenticator, delete the password and make require password change false +if ($_REQUEST['authenticator'] !== '\MyRadio\MyRadio\MyRadioDefaultAuthenticator') { (new MyRadioDefaultAuthenticator())->removePassword($_SESSION['memberid']); + MyRadio_User::getInstance()->setRequirePasswordChange(false); } //Remove the lock on Session access $_SESSION['auth_use_locked'] = false; -header('Location: '.$_REQUEST['next']); \ No newline at end of file +header('Location: '.$_REQUEST['next']); diff --git a/src/Controllers/MyRadio/config.js.php b/src/Controllers/MyRadio/config.js.php index 891c29905..5578703de 100644 --- a/src/Controllers/MyRadio/config.js.php +++ b/src/Controllers/MyRadio/config.js.php @@ -1,11 +1,12 @@ - * @version 20130525 - * @package MyRadio_Core + * Provides a JS file with configuration options useful to the client. */ +use \MyRadio\Config; -$conf = Config::getPublicConfig(); -require 'Views/MyRadio/config.js.php'; \ No newline at end of file +header('Content-Type: text/javascript'); +header('Cache-Control: max-age=86400, must-revalidate'); +header('Expires: ', date('r', time() + 86400)); +header('HTTP/1.1 200 OK'); + +echo 'window.mConfig='.json_encode(Config::getPublicConfig()).';'; diff --git a/src/Controllers/MyRadio/default.php b/src/Controllers/MyRadio/default.php index 581f39f16..19b370a26 100644 --- a/src/Controllers/MyRadio/default.php +++ b/src/Controllers/MyRadio/default.php @@ -1,35 +1,43 @@ - * @version 20130809 - * @package MyRadio_Core */ +use \MyRadio\Config; +use \MyRadio\MyRadio\CoreUtils; +use MyRadio\ServiceAPI\MyRadio_Event; +use \MyRadio\ServiceAPI\MyRadio_User; +use \MyRadio\MyRadio\MyRadioMenu; +use \MyRadio\MyRadio\MyRadioNews; +/** @var MyRadio_User $user */ $user = MyRadio_User::getInstance(); -$menu = (new MyRadioMenu())->getMenuForUser($user); +$menu = (new MyRadioMenu())->getMenuForUser(); $news = MyRadioNews::getLatestNewsItem(Config::$news_feed, $user); +$news_clickthrough = Config::$members_news_enable && empty($news['seen']); + +$events = MyRadio_Event::getNext(3); $twig = CoreUtils::getTemplateObject()->setTemplate('MyRadio/menu.twig') - ->addVariable('title', 'Menu') + ->addVariable('title', 'Welcome to '.Config::$short_name.', '. $user->getFName(). '!') ->addVariable('menu', $menu) - ->addVariable('news_clickthrough', empty($news['seen'])) - /** + ->addVariable('news_clickthrough', $news_clickthrough) + /* * This is some bonus stuff for the Get On Air item */ ->addVariable('studio_trained', $user->isStudioTrained()) ->addVariable('studio_demoed', $user->isStudioDemoed()) ->addVariable('is_trainer', $user->isTrainer()) ->addVariable('has_show', $user->hasShow()) - ->addVariable('paid', $user->isCurrentlyPaid()); + ->addVariable('paid', $user->isCurrentlyPaid()) + ->addVariable('contract_signed', $user->hasSignedContract()) + ->addVariable('events', $events); if (Config::$members_news_enable) { $twig->addVariable('news', $news); } -$twig->render(); \ No newline at end of file +$twig->render(); diff --git a/src/Controllers/MyRadio/errorReport.php b/src/Controllers/MyRadio/errorReport.php old mode 100755 new mode 100644 index 58333f99c..4bccd6e33 --- a/src/Controllers/MyRadio/errorReport.php +++ b/src/Controllers/MyRadio/errorReport.php @@ -1,10 +1,11 @@ - * @package MyRadio_Core + * Emails data to Computing. */ +use \MyRadio\MyRadioEmail; +use \MyRadio\MyRadio\CoreUtils; -MyRadioEmail::sendEmailToComputing('Error Report', - CoreUtils::getRequestInfo() . "\n" . print_r($_SESSION, true)); \ No newline at end of file +MyRadioEmail::sendEmailToComputing( + 'Error Report', + CoreUtils::getRequestInfo()."\n".print_r($_SESSION, true) +); diff --git a/src/Controllers/MyRadio/impersonate.php b/src/Controllers/MyRadio/impersonate.php old mode 100755 new mode 100644 index 1dd11e267..2b740fdb9 --- a/src/Controllers/MyRadio/impersonate.php +++ b/src/Controllers/MyRadio/impersonate.php @@ -2,42 +2,56 @@ /** * Impersonate a user, convincing systems that you *are* them. - * - * @author Lloyd Wallis - * @data 20140102 - * @package MyRadio_Core + * + * @data 20140102 */ +use \MyRadio\Database; +use \MyRadio\Config; +use \MyRadio\MyRadio\AuthUtils; +use \MyRadio\ServiceAPI\MyRadio_User; +use \MyRadio\ServiceAPI\MyRadio_Timeslot; if (isset($_REQUEST['memberid'])) { //Impersonate $impersonatee = MyRadio_User::getInstance($_REQUEST['memberid']); - if ((!CoreUtils::hasPermission(AUTH_IMPERSONATE)) || - ($impersonatee->hasAuth(AUTH_BLOCKIMPERSONATE) && - !CoreUtils::hasPermission(AUTH_IMPERSONATE_BLOCKED_USERS))) { + if ((!AuthUtils::hasPermission(AUTH_IMPERSONATE)) + || ($impersonatee->hasAuth(AUTH_BLOCKIMPERSONATE) + && !AuthUtils::hasPermission(AUTH_IMPERSONATE_BLOCKED_USERS)) + ) { require_once 'Controllers/Errors/403.php'; } else { - $_SESSION['myradio-impersonating'] = $_SESSION; + // Yes, this temporary variable is necessary, otherwise recursion happens. + // I don't even. + $old_sess = $_SESSION; + $_SESSION['myradio-impersonating'] = $old_sess; $_SESSION['memberid'] = $impersonatee->getID(); - $ip_auth = Database::getInstance()->fetch_column('SELECT typeid FROM auth_subnet WHERE subnet >>= $1', [$_SERVER['REMOTE_ADDR']]); + $ip_auth = Database::getInstance()->fetchColumn( + 'SELECT typeid FROM auth_subnet WHERE subnet >>= $1', + [$_SERVER['REMOTE_ADDR']] + ); $_SESSION['member_permissions'] = array_merge($ip_auth, $impersonatee->getPermissions()); $_SESSION['name'] = $impersonatee->getName(); $_SESSION['email'] = $impersonatee->getEmail(); $_SESSION['auth_use_locked'] = false; + + // Now to reset the timeslot if we should have it once impersonated. + $timeslot = MyRadio_Timeslot::getUserSelectedTimeslot(); + if ($timeslot) { + //Can the user access this timeslot? + if (!($timeslot->isCurrentUserAnOwner() || AuthUtils::hasPermission(AUTH_EDITSHOWS))) { + MyRadio_Timeslot::setUserSelectedTimeslot(); // Don't have perms, reset it. + } + } } -} else { - /** - * For some reason I sometimes have to unimpersonate 3 or more times before - * the impersonating key actually gets reset... - */ - while(isset($_SESSION['myradio-impersonating'])) { - //Unimpersonate - $impersonate = $_SESSION['myradio-impersonating']; - $_SESSION = $impersonate; - } +} elseif (isset($_SESSION['myradio-impersonating'])) { + //Unimpersonate + $impersonate = $_SESSION['myradio-impersonating']; + // This will jump back the selected timeslot back too. + $_SESSION = $impersonate; } if (isset($_REQUEST['next'])) { header('Location: '.$_REQUEST['next']); } else { header('Location: '.Config::$base_url); -} \ No newline at end of file +} diff --git a/src/Controllers/MyRadio/jwt.php b/src/Controllers/MyRadio/jwt.php new file mode 100644 index 000000000..c50c9acf5 --- /dev/null +++ b/src/Controllers/MyRadio/jwt.php @@ -0,0 +1,25 @@ + time(), + 'uid' => $_SESSION['memberid'], + 'name' => MyRadio_User::getCurrentUser()->getName(), + 'exp' => time() + 3600 * 3, + 'iss' => Config::$base_url + ]; + + header("Location: " . $_GET["redirectto"] . "?jwt=" . Token::customPayload($payload, Config::$jwt_signing_secret)); + +} else { + throw new MyRadioException('redirectto must be provided', 400); +} diff --git a/src/Controllers/MyRadio/listPermissions.php b/src/Controllers/MyRadio/listPermissions.php old mode 100755 new mode 100644 index 2049c2af9..0adb4ae38 --- a/src/Controllers/MyRadio/listPermissions.php +++ b/src/Controllers/MyRadio/listPermissions.php @@ -1,28 +1,34 @@ 'text', 'value' => 'Usage', - 'url' => CoreUtils::makeURL('MyRadio', 'permissionUsage', ['typeid' => $x['value']]) - ]; - $x['assigned'] = [ + 'url' => URLUtils::makeURL('MyRadio', 'permissionUsage', ['typeid' => $x['value']]), + ]; + $x['assigned'] = [ 'display' => 'text', 'value' => 'Assigned To', - 'url' => CoreUtils::makeURL('MyRadio', 'permissionAssigned', ['typeid' => $x['value']]) - ]; - return $x; -}, CoreUtils::getAllPermissions()); + 'url' => URLUtils::makeURL('MyRadio', 'permissionAssignedTo', ['typeid' => $x['value']]), + ]; + + return $x; + }, + AuthUtils::getAllPermissions() +); CoreUtils::getTemplateObject()->setTemplate('MyRadio/listPermissions.twig') - ->addVariable('title', 'Available Permissions') + ->addVariable('title', 'Permissions') + ->addVariable('subtitle', 'Available Permissions') ->addVariable('tabledata', $data) ->addVariable('tablescript', 'myradio.listPermissions') ->render(); diff --git a/src/Controllers/MyRadio/login.php b/src/Controllers/MyRadio/login.php old mode 100755 new mode 100644 index c706189f1..5398f08d8 --- a/src/Controllers/MyRadio/login.php +++ b/src/Controllers/MyRadio/login.php @@ -1,127 +1,182 @@ 'Login' - ) - ))->addField( - new MyRadioFormField('user', MyRadioFormField::TYPE_TEXT, array( - 'explanation' => '', - 'label' => 'Username:', - 'options' => ['placeholder' => 'abc123'] - )) - )->addField( - new MyRadioFormField('password', MyRadioFormField::TYPE_PASSWORD, array( - 'explanation' => '', - 'label' => 'Password:' - )) - )->addField( - new MyRadioFormField('next', MyRadioFormField::TYPE_HIDDEN, array( - 'value' => isset($_REQUEST['next']) ? $_REQUEST['next'] : Config::$base_url - )) - )->setTemplate('MyRadio/login.twig'); - -if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['myradio_login-user'])) { - //Submitted - $status = null; - $data = $form->readValues(); - $raw_uname = str_replace('@' . Config::$eduroam_domain, '', $data['user']); +if (isset($_SESSION['memberid'])) { + isset($_GET["next"]) ? URLUtils::redirectURI($_GET["next"]) : URLUtils::redirect(Config::$default_module); +} else { + $form = ( + new MyRadioForm( + 'myradio_login', + 'MyRadio', + 'login', + [ + 'title' => 'Login', + ] + ) + )->addField( + new MyRadioFormField( + 'user', + MyRadioFormField::TYPE_TEXT, + [ + 'label' => 'Username:', + 'options' => [ + 'placeholder' => 'abc123', + 'autofocus' => true, + ], + ] + ) + )->addField( + new MyRadioFormField( + 'password', + MyRadioFormField::TYPE_PASSWORD, + [ + 'explanation' => '', + 'label' => 'Password:', + ] + ) + )->addField( + new MyRadioFormField( + 'next', + MyRadioFormField::TYPE_HIDDEN, + [ + 'value' => isset($_REQUEST['next']) ? $_REQUEST['next'] : Config::$base_url, + ] + ) + )->setTemplate('MyRadio/login.twig'); - $authenticators = []; - foreach (Config::$authenticators as $i) { - $authenticator = new $i(); - $user = $authenticator->validateCredentials($raw_uname, $data['password']); + if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['myradio_login-user'])) { +//Submitted + $status = null; + $data = $form->readValues(); - if ($user) { - if ($user->getAccountLocked()) { - //The user's account is disabled - $status = 'locked'; - break; - } elseif (Config::$single_authenticator && - $user->getAuthProvider() != null && $user->getAuthProvider() != $i) { - //They can only authenticate with the right provider once they've set one - //(if they haven't yet, we'll ask them to choose one) - $status = 'wrongAuthProvider'; - } else { - $_SESSION['memberid'] = (int)$user->getID(); - /** - * Add in permissions granted by the remote IP - * Contains or equals: >>= - */ - $ip_auth = Database::getInstance()->fetch_column('SELECT typeid FROM auth_subnet WHERE subnet >>= $1', [$_SERVER['REMOTE_ADDR']]); - $_SESSION['member_permissions'] = array_map(function($x){return (int)$x;}, - array_merge($ip_auth, $user->getPermissions(), $authenticator->getPermissions($raw_uname))); - $_SESSION['name'] = $user->getName(); - $_SESSION['email'] = $user->getEmail(); - /* - * If anything other than false, the user will be kicked out if - * they try to access anything other than pages with AUTH_NOACCESS - */ - $_SESSION['auth_use_locked'] = false; - $user->updateLastLogin(); - $status = 'success'; - $authenticators[$i] = true; - if ($user->getRequirePasswordChange()) { - //The user needs to change their password - $_SESSION['auth_use_locked'] = 'changePassword'; - $status = 'change'; - } + $raw_uname = str_replace('@'.Config::$eduroam_domain, '', $data['user']); + $raw_uname = strtolower($raw_uname); - //If the user needs to specify an auth provider, go through all login mechanisms - if (Config::$single_authenticator && !$user->getAuthProvider()) { - $_SESSION['auth_use_locked'] = 'chooseAuth'; - $status = 'choose'; - } else { + $authenticators = []; + foreach (Config::$authenticators as $i) { + $authenticator = new $i(); + $user = $authenticator->validateCredentials($raw_uname, $data['password']); + if ($user) { + if ($user->getAccountLocked()) { + //The user's account is disabled + $status = 'locked'; break; + } elseif (Config::$single_authenticator + && $user->getAuthProvider() != null && $user->getAuthProvider() != $i + ) { + //They can only authenticate with the right provider once they've set one + //(if they haven't yet, we'll ask them to choose one) + $status = 'wrongAuthProvider'; + } else { + $_SESSION['memberid'] = (int) $user->getID(); + /* + * Add in permissions granted by the remote IP + * Contains or equals: >>= + */ + $ip_auth = Database::getInstance()->fetchColumn( + 'SELECT typeid FROM auth_subnet WHERE subnet >>= $1', + [$_SERVER['REMOTE_ADDR']] + ); + $_SESSION['member_permissions'] = array_map( + function ($x) { + return (int) $x; + }, + array_merge($ip_auth, $user->getPermissions(), $authenticator->getPermissions($raw_uname)) + ); + $_SESSION['name'] = $user->getName(); + $_SESSION['email'] = $user->getEmail(); + /* + * If anything other than false, the user will be kicked out if + * they try to access anything other than pages with AUTH_NOACCESS + */ + $_SESSION['auth_use_locked'] = false; + if (!$user->isActiveMemberForYear()) { + $user->activateMemberThisYear(); + } + $user->updateLastLogin(); + $status = 'success'; + if (!$user->isGDPRSigned()){ + $status = 'gdpr'; + } + $authenticators[$i] = true; + if ($user->getRequirePasswordChange()) { + //The user needs to change their password + $_SESSION['auth_use_locked'] = 'changePassword'; + $status = 'change'; + } + + //If the user needs to specify an auth provider, go through all login mechanisms + if (Config::$single_authenticator && !$user->getAuthProvider()) { + $_SESSION['auth_use_locked'] = 'chooseAuth'; + $status = 'choose'; + } else { + break; + } } + } else { + $authenticators[$i] = false; } - } else { - $authenticators[$i] = false; } - } - - if ($status === 'choose') { - //The user needs to set a login provider - $twig = CoreUtils::getTemplateObject()->setTemplate('MyRadio/chooseAuth.twig') + if ($status === 'choose') { + //The user needs to set a login provider + $twig = CoreUtils::getTemplateObject()->setTemplate('MyRadio/chooseAuth.twig') ->addVariable('title', 'Choose Login Method'); - $options = []; - $chosen_default = false; - foreach ($authenticators as $authenticator => $success) { - $a = new $authenticator(); - $option = ['value' => $authenticator, - 'name' => $a->getFriendlyName(), - 'description' => $a->getDescription(), - 'different' => !$success, - 'default' => false]; - if ($success && !$chosen_default) { - $option['default'] = true; - $chosen_default = true; + $options = []; + $chosen_default = false; + foreach ($authenticators as $authenticator => $success) { + $a = new $authenticator(); + $option = [ + 'value' => $authenticator, + 'name' => $a->getFriendlyName(), + 'description' => $a->getDescription(), + 'different' => !$success, + 'default' => false, + ]; + if ($success && !$chosen_default) { + $option['default'] = true; + $chosen_default = true; + } + $options[] = $option; } - $options[] = $option; - } - $twig->addVariable('methods', $options) - ->addVariable('next', isset($data['next']) ? $data['next'] : CoreUtils::makeURL(Config::$default_module)) + $twig->addVariable('methods', $options) + ->addVariable( + 'next', + isset($data['next']) ? $data['next'] : URLUtils::makeURL(Config::$default_module) + ) ->render(); - } elseif ($status === 'change') { - header('Location: '.CoreUtils::makeURL('MyRadio', 'pwChange')); - } elseif ($status !== 'success') { - $form->render(['error' => true]); - } else { - if (isset($data['next'])) { - header('Location: ' . $data['next']); + } elseif ($status === 'change') { + URLUtils::redirect('MyRadio', 'pwChange'); + } elseif ($status == 'gdpr'){ + URLUtils::redirect('MyRadio', 'privacystatement'); + } elseif ($status !== 'success') { + $form->setFieldValue( + 'next', + isset($data['next']) ? $data['next'] : URLUtils::makeURL(Config::$default_module) + ) + ->render(['error' => true]); } else { - header('Location: ' . CoreUtils::makeURL(Config::$default_module)); + if (isset($data['next'])) { + header('Location: '.$data['next']); + } else { + URLUtils::redirect(Config::$default_module); + } } + } else { + //Not Submitted + $form->render(['logout' => isset($_REQUEST['logout'])]); } -} else { - //Not Submitted - $form->render(['logout' => isset($_REQUEST['logout'])]); } diff --git a/src/Controllers/MyRadio/logout.php b/src/Controllers/MyRadio/logout.php old mode 100755 new mode 100644 index d37d2cb4a..7fc91d844 --- a/src/Controllers/MyRadio/logout.php +++ b/src/Controllers/MyRadio/logout.php @@ -1,13 +1,12 @@ 1])); \ No newline at end of file +URLUtils::redirect('MyRadio', 'login', ['logout' => 1]); diff --git a/src/Controllers/MyRadio/myr2Handoff.php b/src/Controllers/MyRadio/myr2Handoff.php new file mode 100644 index 000000000..a1e1bd35a --- /dev/null +++ b/src/Controllers/MyRadio/myr2Handoff.php @@ -0,0 +1,6 @@ +setTemplate('MyRadio/myr2Handoff.twig') + ->render(); diff --git a/src/Controllers/MyRadio/news.php b/src/Controllers/MyRadio/news.php old mode 100755 new mode 100644 index 26a521a46..1aaa876c0 --- a/src/Controllers/MyRadio/news.php +++ b/src/Controllers/MyRadio/news.php @@ -1,13 +1,12 @@ setTemplate('MyRadio/news.twig') ->addVariable('title', 'News Feed') diff --git a/src/Controllers/MyRadio/permissionAssignedTo.php b/src/Controllers/MyRadio/permissionAssignedTo.php new file mode 100644 index 000000000..04df8bd0e --- /dev/null +++ b/src/Controllers/MyRadio/permissionAssignedTo.php @@ -0,0 +1,16 @@ +setTemplate('MyRadio/permissionAssignedTo.twig') + ->addVariable('title', 'Permissions') + ->addVariable('subtitle', 'Users Assigned "'.AuthUtils::getAuthDescription($_REQUEST['typeid']).'"') + ->addVariable('assignedTo', $assignedTo) + ->render(); diff --git a/src/Controllers/MyRadio/permissionUsage.php b/src/Controllers/MyRadio/permissionUsage.php old mode 100755 new mode 100644 index 1fc56e57f..199fdbd40 --- a/src/Controllers/MyRadio/permissionUsage.php +++ b/src/Controllers/MyRadio/permissionUsage.php @@ -1,16 +1,16 @@ setTemplate('MyRadio/permissionUsage.twig') - ->addVariable('title', 'Permission Usage | '.CoreUtils::getAuthDescription($_REQUEST['typeid'])) + ->addVariable('title', 'Permissions') + ->addVariable('subtitle', '"'.AuthUtils::getAuthDescription($_REQUEST['typeid']).'" Usage') ->addVariable('usage', $usage) ->render(); diff --git a/src/Controllers/MyRadio/privacystatement.php b/src/Controllers/MyRadio/privacystatement.php new file mode 100644 index 000000000..a6830d4fe --- /dev/null +++ b/src/Controllers/MyRadio/privacystatement.php @@ -0,0 +1,33 @@ + 'Privacy Statement' + ] + ) + )->setTemplate('MyRadio/privacy.twig'); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + + $user = MyRadio_User::getInstance($_SESSION['memberid']); + $user->setSignedGDPR(true); + URLUtils::redirect(Config::$default_module); + +} else { + //Not Submitted + $form->render(['logout' => isset($_REQUEST['logout'])]); +} \ No newline at end of file diff --git a/src/Controllers/MyRadio/pwChange.php b/src/Controllers/MyRadio/pwChange.php old mode 100755 new mode 100644 index ecbe4e44d..262754fdd --- a/src/Controllers/MyRadio/pwChange.php +++ b/src/Controllers/MyRadio/pwChange.php @@ -3,55 +3,90 @@ /** * Enables a user to change their password, either whilst logged in or by * using a password reset token that has been emailed to them. - * - * @author Lloyd Wallis - * @data 20140121 - * @package MyRadio_Core + * + * @data 20140121 */ -$form = (new MyRadioForm('myradio_pwChange', 'MyRadio', 'pwChange', array( - 'title' => 'Password Change' - ) - ))->addField( - new MyRadioFormField('pw1', MyRadioFormField::TYPE_PASSWORD, array( +use \MyRadio\Config; +use \MyRadio\Database; +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\AuthUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\MyRadio\MyRadioForm; +use \MyRadio\MyRadio\MyRadioFormField; +use \MyRadio\MyRadio\MyRadioDefaultAuthenticator; +use \MyRadio\ServiceAPI\MyRadio_User; + +$form = ( + new MyRadioForm( + 'myradio_pwChange', + 'MyRadio', + 'pwChange', + [ + 'title' => 'Password Change', + ] + ) +)->addField( + new MyRadioFormField( + 'pw1', + MyRadioFormField::TYPE_PASSWORD, + [ 'explanation' => '', - 'label' => 'New Password:' - )) - )->addField( - new MyRadioFormField('pw2', MyRadioFormField::TYPE_PASSWORD, array( + 'label' => 'New Password:', + ] + ) +)->addField( + new MyRadioFormField( + 'pw2', + MyRadioFormField::TYPE_PASSWORD, + [ 'explanation' => '', - 'label' => 'Confirm New Password:' - )) - )->setTemplate('MyRadio/pwReset.twig'); + 'label' => 'Confirm New Password:', + ] + ) +)->setTemplate('MyRadio/pwReset.twig'); -/** +/* * If the user is logged in, we're changing their password. Ask them to verify * their existing one. If they aren't logged in, then they should be following * a password reset link, in which case we verify the reset token. */ if (isset($_SESSION['memberid'])) { $form->addField( - new MyRadioFormField('pwold', MyRadioFormField::TYPE_PASSWORD, array( - 'explanation' => '', - 'label' => 'Current Password:' - )) + new MyRadioFormField( + 'pwold', + MyRadioFormField::TYPE_PASSWORD, + [ + 'explanation' => '', + 'label' => 'Current Password:', + ] + ) ); } else { $var = $_SERVER['REQUEST_METHOD'] === 'POST' ? 'myradio_pwChange-token' : 'token'; - + if (!isset($_REQUEST[$var])) { throw new MyRadioException('Password reset token required.', 400); } else { $db = Database::getInstance(); - $token = $db->fetch_one('SELECT * FROM myury.password_reset_token' - . ' WHERE token=$1 AND expires > NOW() AND used IS NULL', [$_REQUEST[$var]]); + $tokenStr = $_REQUEST[$var]; + $tokenStr = str_replace(["\r", "\n"], ['',''], $tokenStr); + $token = $db->fetchOne( + 'SELECT * FROM myury.password_reset_token + WHERE token=$1 AND expires > NOW() AND used IS NULL', + [$tokenStr] + ); if (empty($token)) { - throw new MyRadioException('Password reset token invalid. It may have expired or already been used.', 400); + throw new MyRadioException('Password reset token invalid. It may have expired or already been used. Alternatively, try removing the %0D%0A from the URL. If you\'re joining computing team, maybe you could help us solve that bug', 400); } else { $form->addField( - new MyRadioFormField('token', MyRadioFormField::TYPE_HIDDEN, array( - 'value' => $token['token'] - )) + new MyRadioFormField( + 'token', + MyRadioFormField::TYPE_HIDDEN, + [ + 'value' => $token['token'], + ] + ) ); } } @@ -63,15 +98,15 @@ if ($data['pw1'] !== $data['pw2']) { //Passwords do not match - $form->render(['messages' => 'Your new passwords did not match.']); + $form->render(['error' => 'Your new passwords did not match.']); + exit; } //Logged in user change? if (isset($data['pwold'])) { //Is the old password correct? - if (CoreUtils::testCredentials(MyRadio_User::getInstance()->getEmail(), - $data['pwold']) === false) { - $form->render(['messages' => 'Your old password was invalid.']); + if (AuthUtils::testCredentials(MyRadio_User::getInstance()->getEmail(), $data['pwold']) === false) { + $form->render(['error' => 'Your old password was invalid.']); exit; } $user = MyRadio_User::getInstance(); @@ -79,35 +114,39 @@ //Token initialised in form definition above. $user = MyRadio_User::getInstance($token['memberid']); } - + //Right, let's update the password - /** + /* * Only works with MyRadioDefaultAuthenticator. Should it allow * others to plug in? I think not. */ $authenticator = new MyRadioDefaultAuthenticator(); $authenticator->setPassword($user, $data['pw1']); unset($data); - + //Reset the User's authenticator preferences - they may be locking them out $user->setAuthProvider(null); - + //Set the token as used if (isset($token)) { - $db->query('UPDATE myury.password_reset_token SET used=NOW()' - . ' WHERE token=$1', [$token['token']]); + $db->query( + 'UPDATE myury.password_reset_token SET used=NOW() + WHERE token=$1', + [$token['token']] + ); } - + //If the user was locked out for a password change, unlock them - if (isset($_SESSION['auth_use_locked']) && - $_SESSION['auth_use_locked'] === 'chooseAuth') { + if (isset($_SESSION['auth_use_locked']) + && $_SESSION['auth_use_locked'] === 'chooseAuth' + ) { unset($_SESSION['auth_use_locked']); } - header('Location: '.CoreUtils::makeURL('MyRadio', 'login')); + URLUtils::redirect('MyRadio', 'login'); } else { foreach (Config::$authenticators as $authenticator) { - $auth = new $authenticator; + $auth = new $authenticator(); $messages[] = $auth->getResetFormMessage(); } diff --git a/src/Controllers/MyRadio/pwReset.php b/src/Controllers/MyRadio/pwReset.php old mode 100755 new mode 100644 index 4e267a8ee..1ee05a670 --- a/src/Controllers/MyRadio/pwReset.php +++ b/src/Controllers/MyRadio/pwReset.php @@ -1,46 +1,65 @@ 'Password Reset', - 'captcha' => true - ) - ))->addField( - new MyRadioFormField('user', MyRadioFormField::TYPE_TEXT, array( +use \MyRadio\Config; +use \MyRadio\MyRadio\MyRadioForm; +use \MyRadio\MyRadio\MyRadioFormField; + +$form = ( + new MyRadioForm( + 'myradio_pwReset', + 'MyRadio', + 'pwReset', + [ + 'title' => 'Password Reset', + 'captcha' => true, + ] + ) +)->addField( + new MyRadioFormField( + 'user', + MyRadioFormField::TYPE_TEXT, + [ 'explanation' => '', 'label' => 'Username:', - 'options' => ['placeholder' => 'abc123'] - )) - )->setTemplate('MyRadio/pwReset.twig'); + 'options' => ['placeholder' => 'abc123'], + ] + ) +)->setTemplate('MyRadio/pwReset.twig'); if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_REQUEST['myradio_pwReset-user'])) { //Submitted $data = $form->readValues(); - + if (!$data) { //Invalid captcha - $form->render(['messages' => ['
          Please verify the captcha input and try again.
          ']]); + $form->render( + ['messages' => ['
          Please verify the captcha input and try again.
          ']] + ); } else { foreach (Config::$authenticators as $i) { - $authenticator = new $i; + $authenticator = new $i(); if ($authenticator->resetAccount($data['user'])) { break; } } - - $form->render(['messages' => ['
          Please check your email to finish resetting your password.
          ']]); + + $form->render( + [ + 'messages' => [ + '
          Please check your email to finish resetting your password.
          ' + ] + ] + ); } } else { foreach (Config::$authenticators as $authenticator) { - $auth = new $authenticator; + $auth = new $authenticator(); $messages[] = $auth->getResetFormMessage(); } - + //Not Submitted $form->render(['messages' => $messages]); } diff --git a/src/Controllers/MyRadio/removeActionPermission.php b/src/Controllers/MyRadio/removeActionPermission.php new file mode 100644 index 000000000..3c8f967ef --- /dev/null +++ b/src/Controllers/MyRadio/removeActionPermission.php @@ -0,0 +1,97 @@ + 'Permissions', + 'subtitle' => 'Delete Action Permission', + ] +); +$form->addField( + new MyRadioFormField( + 'permissionid', + MyRadioFormField::TYPE_HIDDEN, + [ + 'required' => true + ] + ) +)->addField( + new MyRadioFormField( + 'module', + MyRadioFormField::TYPE_TEXT, + [ + 'enabled' => false + ] + ) +)->addField( + new MyRadioFormField( + 'action', + MyRadioFormField::TYPE_TEXT, + [ + 'enabled' => false + ] + ) +)->addField( + new MyRadioFormField( + 'permission', + MyRadioFormField::TYPE_TEXT, + [ + 'enabled' => false + ] + ) +)->addField( + new MyRadioFormField( + 'confirm', + MyRadioFormField::TYPE_CHECK, + [ + 'explanation' => 'I confirm that deleting this action permission is + safe and won\'t break things.', + 'label' => 'Confirm Deletion', + ] + ) +); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = $form->readValues(); + + $actPermissionID = $data['permissionid']; + $confirm = $data['confirm']; + + if ($confirm) { + AuthUtils::removeActionPermission($actPermissionID); + $message = 'The action permission has been removed.'; + } else { + $message = 'The action permission has been not been altered.'; + } + URLUtils::redirectWithMessage('MyRadio', 'actionPermissions', $message); +} else { + //Not Submitted + if (isset($_REQUEST['permissionid'])) { + $actionPermission = AuthUtils::getActionPermission($_REQUEST['permissionid']); + $form->editMode( + $_REQUEST['permissionid'], + [ + 'permissionid' => $_REQUEST['permissionid'], + 'module' => $actionPermission['module'], + 'action' => $actionPermission['action'], + 'permission' => $actionPermission['permission'], + ] + )->render(); + } else { + throw new MyRadioException('An PermissionID to delete has not been provided, please try again.', 400); + } +} diff --git a/src/Controllers/MyRadio/timeslot.php b/src/Controllers/MyRadio/timeslot.php old mode 100755 new mode 100644 index 1fc88e695..6c41969f7 --- a/src/Controllers/MyRadio/timeslot.php +++ b/src/Controllers/MyRadio/timeslot.php @@ -1,54 +1,98 @@ getSeason()->getShow()->isCurrentUserAnOwner() or CoreUtils::hasPermission(AUTH_EDITSHOWS))) { + if (!($timeslot->isCurrentUserAnOwner() || AuthUtils::hasPermission(AUTH_EDITSHOWS))) { + $message = "You don't have permission to view this show"; require_once 'Controllers/Errors/403.php'; } else { - $_SESSION['timeslotid'] = $timeslot->getID(); - $_SESSION['timeslotname'] = CoreUtils::happyTime($timeslot->getStartTime()); + MyRadio_Timeslot::setUserSelectedTimeslot($timeslot); //Handle sign-ins - foreach ($_REQUEST['signin'] as $memberid) { - $timeslot->signIn(MyRadio_User::getInstance($memberid)); + foreach (($_REQUEST['signin'] ?? []) as $memberid) { + if (!isset($_REQUEST["location"]) || $_REQUEST["location"] == "unselected") { + URLUtils::backWithMessage("You must select where you are doing your show"); + return; + } + $timeslot->signIn(MyRadio_User::getInstance($memberid), $_REQUEST['location']); + } + if (!empty($_REQUEST['guest_info'])) { + $timeslot->signInGuests($_REQUEST['guest_info'], $_REQUEST['location']); } - header('Location: '.($_POST['next'] !== '' ? $_POST['next'] : Config::$base_url)); + header('Location: ' . ($_REQUEST['next'] !== '' ? $_REQUEST['next'] : Config::$base_url)); } +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + if (isset($_POST['timeslotid'])) { + setupTimeslot(MyRadio_Timeslot::getInstance($_POST['timeslotid'])); + } else { + URLUtils::backWithMessage('Cannot select empty timeslot'); + } +} elseif (isset($_GET['current']) && $_GET['current'] && AuthUtils::hasPermission(AUTH_EDITSHOWS)) { + //Submitted Current + setupTimeslot(MyRadio_Timeslot::getCurrentTimeslot()); +} elseif (!empty(Config::$contract_uri) && !MyRadio_User::getInstance()->hasSignedContract()) { + $message = "You need to have signed the Presenter's Contract to view this. You'll find it in the main menu."; + require_once 'Controllers/Errors/403.php'; } else { //Not Submitted $twig = CoreUtils::getTemplateObject()->setTemplate('MyRadio/timeslot.twig') - ->addVariable('title', 'Timeslot Select') - ->addVariable('allTimeslots', 'unavailable') - ->addVariable('next', $_GET['next']); + ->addVariable('title', 'Timeslot Select') + ->addVariable('allTimeslots', 'unavailable') + ->addVariable('next', $_GET['next']); $data = []; - /** + /* * People with AUTH_EDITSHOWS can see all timeslots here */ $shows = MyRadio_User::getInstance()->getShows(); - if (CoreUtils::hasPermission(AUTH_EDITSHOWS)) { + if (AuthUtils::hasPermission(AUTH_EDITSHOWS)) { if (isset($_GET['all'])) { $shows = MyRadio_Show::getAllShows(); $twig->addVariable('allTimeslots', 'on'); } else { $twig->addVariable('allTimeslots', 'off'); } + + if (!is_null(MyRadio_Timeslot::getCurrentTimeslot())) { + $twig->addVariable('currentAvaliable', 'true'); + } } foreach ($shows as $show) { foreach ($show->getAllSeasons() as $season) { - $data[$show->getMeta('title')][] = array_map(function($x) { - return [$x->getID(), $x->getStartTime(), $x->getEndTime()]; - }, $season->getAllTimeslots()); + $data[$show->getMeta('title')][] = array_map( + function ($x) { + return [$x->getID(), $x->getStartTime(), $x->getEndTime()]; + }, + $season->getAllTimeslots() + ); } } - $twig->addVariable('timeslots', $data)->render(); -} \ No newline at end of file + $twig + ->addVariable('timeslots', $data) + ->addVariable('locations', MyRadio_Scheduler::getLocations()) + ->render(); +} diff --git a/src/Controllers/MyRadio/webstudio.php b/src/Controllers/MyRadio/webstudio.php new file mode 100644 index 000000000..32860ced4 --- /dev/null +++ b/src/Controllers/MyRadio/webstudio.php @@ -0,0 +1,11 @@ + - * @version 20130509 - * @package MyRadio_NIPSWeb + * Confirms upload of a ManagedItem. */ +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\NIPSWeb\NIPSWeb_ManagedItem; -if (!isset($_REQUEST['fileid']) or !isset($_REQUEST['title']) or !isset($_REQUEST['expires']) or !isset($_REQUEST['auxid'])) { - header('HTTP/1.1 400 Bad Request'); - exit; +if (!isset($_REQUEST['fileid']) + or !isset($_REQUEST['title']) + or !isset($_REQUEST['expires']) + or !isset($_REQUEST['auxid']) +) { + header('HTTP/1.1 400 Bad Request'); + exit; } $data = NIPSWeb_ManagedItem::storeItem($_REQUEST['fileid'], $_REQUEST['title']); $data['fileid'] = $_REQUEST['fileid']; -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file +URLUtils::dataToJSON($data); diff --git a/src/Controllers/NIPSWeb/confirm_central_upload.php b/src/Controllers/NIPSWeb/confirm_central_upload.php index defd74296..d804c1f19 100644 --- a/src/Controllers/NIPSWeb/confirm_central_upload.php +++ b/src/Controllers/NIPSWeb/confirm_central_upload.php @@ -1,12 +1,18 @@ - * @version 18042013 - * @package MyRadio_NIPSWeb + * Saves a cached upload into the URY Central Database. */ -$data = MyRadio_Track::identifyAndStoreTrack($_REQUEST['fileid'], $_REQUEST['title'], $_REQUEST['artist']); +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Track; + +$data = MyRadio_Track::identifyAndStoreTrack( + $_REQUEST['fileid'], + $_REQUEST['title'], + $_REQUEST['artist'], + $_REQUEST['album'], + $_REQUEST['position'], + filter_var($_REQUEST['explicit'], FILTER_VALIDATE_BOOLEAN) ? true : null +); $data['fileid'] = $_REQUEST['fileid']; -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file +URLUtils::dataToJSON($data); diff --git a/src/Controllers/NIPSWeb/create_token.php b/src/Controllers/NIPSWeb/create_token.php index 6694f74c2..9da175588 100644 --- a/src/Controllers/NIPSWeb/create_token.php +++ b/src/Controllers/NIPSWeb/create_token.php @@ -1,11 +1,10 @@ - * @version 17032013 - * @package MyRadio_NIPSWeb + * Creates a NIPSWeb Play Token for the Current User and the given trackid. */ +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\NIPSWeb\NIPSWeb_Token; + NIPSWeb_Token::createToken($_REQUEST['trackid']); -require 'Views/MyRadio/nocontent.php'; \ No newline at end of file +URLUtils::nocontent(); diff --git a/src/Controllers/NIPSWeb/default.php b/src/Controllers/NIPSWeb/default.php index ce54bb086..16d724e8a 100644 --- a/src/Controllers/NIPSWeb/default.php +++ b/src/Controllers/NIPSWeb/default.php @@ -1,31 +1,34 @@ - * @version 20130824 - * @package MyRadio_NIPSWeb + * Main renderer for NIPSWeb. */ +use \MyRadio\MyRadio\AuthUtils; +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_Timeslot; +use \MyRadio\iTones\iTones_Playlist; +use \MyRadio\NIPSWeb\NIPSWeb_AutoPlaylist; +use \MyRadio\NIPSWeb\NIPSWeb_ManagedPlaylist; +use \MyRadio\NIPSWeb\NIPSWeb_ManagedUserPlaylist; +use \MyRadio\ServiceAPI\MyRadio_User; CoreUtils::requireTimeslot(); -if (isset($_REQUEST['readonly'])) { - $template = 'NIPSWeb/readonly.twig'; - $title = MyRadio_Timeslot::getInstance($_SESSION['timeslotid'])->getMeta('title'); - $reslists = []; -} else { - $template = 'NIPSWeb/main.twig'; - $title = 'Show Planner'; - $reslists = CoreUtils::dataSourceParser(array( - 'managed' => iTones_Playlist::getAlliTonesPlaylists(), - 'auto' => NIPSWeb_AutoPlaylist::getAllAutoPlaylists(), - 'aux' => NIPSWeb_ManagedPlaylist::getAllManagedPlaylists(), - 'user' => NIPSWeb_ManagedUserPlaylist::getAllManagedUserPlaylistsFor(MyRadio_User::getInstance()) - )); -} +$show_title = MyRadio_Timeslot::getInstance($_SESSION['timeslotid'])->getMeta('title'); + +$template = 'NIPSWeb/main.twig'; +$reslists = CoreUtils::dataSourceParser( + [ + 'managed' => iTones_Playlist::getAlliTonesPlaylists(), + 'auto' => NIPSWeb_AutoPlaylist::getAllAutoPlaylists(), + 'aux' => NIPSWeb_ManagedPlaylist::getAllManagedPlaylists(), + 'user' => NIPSWeb_ManagedUserPlaylist::getAllManagedUserPlaylists(), + ] +); CoreUtils::getTemplateObject()->setTemplate($template) - ->addVariable('title', $title) - ->addVariable('tracks', MyRadio_Timeslot::getInstance($_SESSION['timeslotid'])->getShowPlan()) - ->addVariable('reslists', $reslists) - ->render(); \ No newline at end of file + ->addVariable('title', "Show Planner") + ->addVariable('show_title', $show_title) + ->addVariable('tracks', MyRadio_Timeslot::getInstance($_SESSION['timeslotid'])->getShowPlan()) + ->addVariable('reslists', $reslists) + ->addVariable('auth_edit_tracks', AuthUtils::hasPermission(AUTH_EDITMUSIC)) + ->render(); diff --git a/src/Controllers/NIPSWeb/get_client_token.php b/src/Controllers/NIPSWeb/get_client_token.php deleted file mode 100644 index a8755bd74..000000000 --- a/src/Controllers/NIPSWeb/get_client_token.php +++ /dev/null @@ -1,11 +0,0 @@ - - * @version 20130608 - * @package MyRadio_NIPSWeb - */ -$data = array('token' => NIPSWeb_Token::getEditToken()); - -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file diff --git a/src/Controllers/NIPSWeb/import.php b/src/Controllers/NIPSWeb/import.php new file mode 100644 index 000000000..50918dc3d --- /dev/null +++ b/src/Controllers/NIPSWeb/import.php @@ -0,0 +1,11 @@ +setTemplate($template) + ->render(); diff --git a/src/Controllers/NIPSWeb/live.php b/src/Controllers/NIPSWeb/live.php index 0871fc95b..0d833bfae 100644 --- a/src/Controllers/NIPSWeb/live.php +++ b/src/Controllers/NIPSWeb/live.php @@ -2,22 +2,27 @@ /** * Main renderer for NIPSWeb in LIVE mode. - * - * @author Lloyd Wallis - * @version 20130907 - * @package MyRadio_NIPSWeb */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_User; +use \MyRadio\iTones\iTones_Playlist; +use \MyRadio\NIPSWeb\NIPSWeb_AutoPlaylist; +use \MyRadio\NIPSWeb\NIPSWeb_ManagedPlaylist; +use \MyRadio\NIPSWeb\NIPSWeb_ManagedUserPlaylist; + $template = 'NIPSWeb/live.twig'; $title = 'Broadcasting and Presenting Suite'; -$reslists = CoreUtils::dataSourceParser(array( - 'managed' => iTones_Playlist::getAlliTonesPlaylists(), - 'auto' => NIPSWeb_AutoPlaylist::getAllAutoPlaylists(), - 'aux' => NIPSWeb_ManagedPlaylist::getAllManagedPlaylists(), - 'user' => NIPSWeb_ManagedUserPlaylist::getAllManagedUserPlaylistsFor(MyRadio_User::getInstance()) - )); +$reslists = CoreUtils::dataSourceParser( + [ + 'managed' => iTones_Playlist::getAlliTonesPlaylists(), + 'auto' => NIPSWeb_AutoPlaylist::getAllAutoPlaylists(), + 'aux' => NIPSWeb_ManagedPlaylist::getAllManagedPlaylists(), + 'user' => NIPSWeb_ManagedUserPlaylist::getAllManagedUserPlaylists(), + ] +); CoreUtils::getTemplateObject()->setTemplate($template) - ->addVariable('title', $title) - ->addVariable('tracks', [])//(new BRA_Utils())->getAllChannelInfo()) - ->addVariable('reslists', $reslists) - ->render(); \ No newline at end of file + ->addVariable('title', $title) + ->addVariable('tracks', [])//(new BRA_Utils())->getAllChannelInfo()) + ->addVariable('reslists', $reslists) + ->render(); diff --git a/src/Controllers/NIPSWeb/load_auto_managed.php b/src/Controllers/NIPSWeb/load_auto_managed.php index 41dd5e10a..a6337144f 100644 --- a/src/Controllers/NIPSWeb/load_auto_managed.php +++ b/src/Controllers/NIPSWeb/load_auto_managed.php @@ -1,14 +1,12 @@ - * @version 20130508 - * @package MyRadio_NIPSWeb + * Loads a NIPSWeb Auto playlist. */ +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\NIPSWeb\NIPSWeb_AutoPlaylist; - $playlistid = str_replace('auto-','',$_REQUEST['playlistid']); +$playlistid = str_replace('auto-', '', $_REQUEST['playlistid']); $data = NIPSWeb_AutoPlaylist::getInstance($playlistid)->getTracks(); -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file +URLUtils::dataToJSON($data); diff --git a/src/Controllers/NIPSWeb/load_aux_lib.php b/src/Controllers/NIPSWeb/load_aux_lib.php index 7f617d970..99b617e47 100644 --- a/src/Controllers/NIPSWeb/load_aux_lib.php +++ b/src/Controllers/NIPSWeb/load_aux_lib.php @@ -1,20 +1,24 @@ - * @author Andy Durant - * @version 20130512 - * @package MyRadio_NIPSWeb + * Loads a NIPSWeb Auxillary playlist. */ +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\NIPSWeb\NIPSWeb_ManagedPlaylist; +use \MyRadio\NIPSWeb\NIPSWeb_ManagedUserPlaylist; + +if (isset($_SERVER['HTTP_ORIGIN'])) { + header("Access-Control-Allow-Origin: " . $_SERVER["HTTP_ORIGIN"]); +} else { + header("Access-Control-Allow-Origin: *"); +} +header("Access-Control-Allow-Credentials: true"); if (preg_match('/^aux-.*$/', $_REQUEST['libraryid']) === 1) { - $libraryid = str_replace('aux-','',$_REQUEST['libraryid']); - $data = NIPSWeb_ManagedPlaylist::getInstance($libraryid)->getItems(); -} -else { - $libraryid = str_replace('user-','',$_REQUEST['libraryid']); - $data = NIPSWeb_ManagedUserPlaylist::getInstance($libraryid)->getItems(); + $libraryid = str_replace('aux-', '', $_REQUEST['libraryid']); + $data = NIPSWeb_ManagedPlaylist::getInstance($libraryid)->getItems(); +} else { + $libraryid = str_replace('user-', '', $_REQUEST['libraryid']); + $data = NIPSWeb_ManagedUserPlaylist::getInstance($libraryid)->getItems(); } -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file +URLUtils::dataToJSON($data); diff --git a/src/Controllers/NIPSWeb/manage_library.php b/src/Controllers/NIPSWeb/manage_library.php index baa4b7150..0ba5a947b 100644 --- a/src/Controllers/NIPSWeb/manage_library.php +++ b/src/Controllers/NIPSWeb/manage_library.php @@ -1,15 +1,30 @@ - * @version 20130525 - * @package MyRadio_NIPSWeb + * Main renderer for NIPSWeb. */ -CoreUtils::getTemplateObject()->setTemplate('NIPSWeb/manage_library.twig') - ->addVariable('reslists', CoreUtils::dataSourceParser(array( - 'managed' => array(), - 'aux' => NIPSWeb_ManagedPlaylist::getAllManagedPlaylists(true), - 'user' => NIPSWeb_ManagedUserPlaylist::getAllManagedUserPlaylistsFor(MyRadio_User::getInstance()) - ))) - ->render(); \ No newline at end of file +use \MyRadio\MyRadio\AuthUtils; +use \MyRadio\MyRadio\CoreUtils; +use MyRadio\MyRadioException; +use \MyRadio\ServiceAPI\MyRadio_User; +use \MyRadio\NIPSWeb\NIPSWeb_ManagedPlaylist; +use \MyRadio\NIPSWeb\NIPSWeb_ManagedUserPlaylist; + +if (!AuthUtils::hasPermission(AUTH_UPLOADMUSICMANUAL)) { + throw new MyRadioException( + 'You must have been Manual Upload trained before accessing the uploader. + If you need to get trained, please fill out the form on the front page of MyRadio.', + 403 + ); +} + +CoreUtils::getTemplateObject()->setTemplate('NIPSWeb/manage_library_manual.twig') + ->addVariable( + 'reslists', + CoreUtils::dataSourceParser( + [ + 'managed' => [], + 'aux' => NIPSWeb_ManagedPlaylist::getAllManagedPlaylists(true), + 'user' => NIPSWeb_ManagedUserPlaylist::getAllManagedUserPlaylists(), + ] + ) + )->render(); diff --git a/src/Controllers/NIPSWeb/managed_play.php b/src/Controllers/NIPSWeb/managed_play.php index 797e4489c..05414e683 100644 --- a/src/Controllers/NIPSWeb/managed_play.php +++ b/src/Controllers/NIPSWeb/managed_play.php @@ -1,14 +1,21 @@ - * @version 30032013 - * @package MyRadio_NIPSWeb + * Streams a managed database item (jingles, beds etc). */ -if (!isset($_REQUEST['managedid'])) { - throw new MyRadioException('Bad Request - managedid required.', 400); -} -$managedid = (int)$_REQUEST['managedid']; +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\AuthUtils; +use \MyRadio\NIPSWeb\NIPSWeb_Views; +use \MyRadio\NIPSWeb\NIPSWeb_ManagedItem; + +header("Access-Control-Allow-Origin: " . $_SERVER["HTTP_ORIGIN"]); +header("Access-Control-Allow-Credentials: true"); -NIPSWeb_Views::serveMP3(NIPSWeb_ManagedItem::getInstance($managedid)->getPath()); \ No newline at end of file +if (AuthUtils::hasPermission(AUTH_USENIPSWEB) || AuthUtils::hasPermission(AUTH_DOWNLOAD_LIBRARY)) { + if (!isset($_REQUEST['managedid'])) { + throw new MyRadioException('Bad Request - managedid required.', 400); + } + $managedid = (int) $_REQUEST['managedid']; + NIPSWeb_Views::serveMP3(NIPSWeb_ManagedItem::getInstance($managedid)->getPath()); +} else { + throw new MyRadioException('You do not have access to this endpoint.', 403); +} diff --git a/src/Controllers/NIPSWeb/playout.php b/src/Controllers/NIPSWeb/playout.php new file mode 100644 index 000000000..2303c5117 --- /dev/null +++ b/src/Controllers/NIPSWeb/playout.php @@ -0,0 +1,11 @@ +setTemplate($template) + ->render(); diff --git a/src/Controllers/NIPSWeb/recv_ops.php b/src/Controllers/NIPSWeb/recv_ops.php deleted file mode 100644 index 453b5fa9b..000000000 --- a/src/Controllers/NIPSWeb/recv_ops.php +++ /dev/null @@ -1,15 +0,0 @@ - - * @version 16042013 - * @package MyRadio_NIPSWeb - */ - -if (!isset($_POST['clientid'])) - throw new MyRadioException('ClientID Required', 400); - -$data = MyRadio_Timeslot::getInstance(NIPSWeb_Token::getEditTokenTimeslot($_POST['clientid']))->updateShowPlan($_POST); - -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file diff --git a/src/Controllers/NIPSWeb/secure_play.php b/src/Controllers/NIPSWeb/secure_play.php index b83c0d44a..927dd85aa 100644 --- a/src/Controllers/NIPSWeb/secure_play.php +++ b/src/Controllers/NIPSWeb/secure_play.php @@ -1,28 +1,37 @@ - * @version 17032013 - * @package MyRadio_NIPSWeb + * Streams a central database track if the user has the correct permissions. */ -if (!isset($_REQUEST['trackid']) or !isset($_REQUEST['recordid'])) { - throw new MyRadioException('Bad Request - trackid and recordid required.', 400); -} -$recordid = (int)$_REQUEST['recordid']; -$trackid = (int) $_REQUEST['trackid']; +use \MyRadio\MyRadio\AuthUtils; +use \MyRadio\Config; +use \MyRadio\MyRadioException; +use \MyRadio\NIPSWeb\NIPSWeb_Views; +use \MyRadio\ServiceAPI\MyRadio_Track; + +header("Access-Control-Allow-Origin: " . $_SERVER["HTTP_ORIGIN"]); +header("Access-Control-Allow-Credentials: true"); + +if (AuthUtils::hasPermission(AUTH_USENIPSWEB) || AuthUtils::hasPermission(AUTH_DOWNLOAD_LIBRARY)) { + if (!isset($_REQUEST['trackid'])) { + throw new MyRadioException('Bad Request - trackid required.', 400); + } + + $trackid = (int) $_REQUEST['trackid']; -if (NIPSWeb_Token::hasToken($trackid)) { - //Yes, clear the current play session and read the track - $path = Config::$music_central_db_path."/records/$recordid/$trackid"; - - if (isset($_REQUEST['ogg'])) { - $path .= '.ogg'; - NIPSWeb_Views::serveOGG($path); - } else { - $path .= '.mp3'; - NIPSWeb_Views::serveMP3($path); - } + + $track = MyRadio_Track::getInstance($trackid); + + if ($track) { + $path = Config::$music_central_db_path."/records/".$track->getAlbum()->getID()."/".$trackid; + + if (isset($_REQUEST['ogg'])) { + $path .= '.ogg'; + NIPSWeb_Views::serveOGG($path); + } else { + $path .= '.mp3'; + NIPSWeb_Views::serveMP3($path); + } + } } else { - throw new MyRadioException('Invalid Play Token!', 403); -} \ No newline at end of file + throw new MyRadioException('You do not have access to this endpoint.', 403); +} diff --git a/src/Controllers/NIPSWeb/upload_aux.php b/src/Controllers/NIPSWeb/upload_aux.php index a33b40357..b6489c5aa 100644 --- a/src/Controllers/NIPSWeb/upload_aux.php +++ b/src/Controllers/NIPSWeb/upload_aux.php @@ -1,11 +1,10 @@ - * @version 20130509 - * @package MyRadio_NIPSWeb + * Uploads a ManagedItem. */ +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\NIPSWeb\NIPSWeb_ManagedItem; + $data = NIPSWeb_ManagedItem::cacheItem($_FILES['audio']['tmp_name']); -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file +URLUtils::dataToJSON($data); diff --git a/src/Controllers/NIPSWeb/upload_central.php b/src/Controllers/NIPSWeb/upload_central.php index 1a8bd634c..0a4900edf 100644 --- a/src/Controllers/NIPSWeb/upload_central.php +++ b/src/Controllers/NIPSWeb/upload_central.php @@ -1,11 +1,19 @@ - * @version 20130517 - * @package MyRadio_NIPSWeb + * Caches an uploaded track and attempts to identify it. */ +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Track; + +if (empty($_FILES)) { + throw new MyRadioException('Failed to receive uploaded files. Is your POST max size big enough?', 500); +} + +if (isset($_FILES['audio']['error']) && $_FILES['audio']['error'] !== 0) { + throw new MyRadioException('File upload failed with code '.$_FILES['audio']['error'], 500); +} + $data = MyRadio_Track::cacheAndIdentifyUploadedTrack($_FILES['audio']['tmp_name']); -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file +URLUtils::dataToJSON($data); diff --git a/src/Controllers/Podcast/allPodcast.php b/src/Controllers/Podcast/allPodcast.php new file mode 100644 index 000000000..87a782d40 --- /dev/null +++ b/src/Controllers/Podcast/allPodcast.php @@ -0,0 +1,16 @@ +setTemplate('table.twig') + ->addVariable('tablescript', 'myradio.podcasts') + ->addVariable('title', 'Podcasts') + ->addVariable('subtitle', 'All Podcasts') + ->addVariable( + 'tabledata', + CoreUtils::setToDataSource(MyRadio_Podcast::getAllPodcasts($include_suspended = true, $include_pending = true)) + )->render(); diff --git a/src/Controllers/Podcast/common.php b/src/Controllers/Podcast/common.php deleted file mode 100644 index 9648c402c..000000000 --- a/src/Controllers/Podcast/common.php +++ /dev/null @@ -1,67 +0,0 @@ - - * @version 20140117 - * @package MyRadio_Podcast - */ - -/** - * Loads the podcast cover form. - * - * @return MyRadioForm The form. - */ -function podcastCoverForm() { - return MyRadio_JsonFormLoader::loadFromModule( - 'Podcast', 'setCover', 'doSetCover' - ); -} - -/** - * Gets the Podcast this form concerns. - * - * @param array $source The parameters array; $_REQUEST by default. - * - * @return MyRadio_Podcast The podcast. - */ -function currentPodcast($source = null) { - if ($source === null) { - $source = $_REQUEST; - } - - if (!array_key_exists('podcastid', $source)) { - throw new MyRadioException('Podcast ID was not provided.', 400); - } - - return MyRadio_Podcast::getInstance($source['podcastid']); -} - -/** - * Requires extra permissions if the current user cannot directly edit the - * given podcast. - * - * @param MyRadio_Podcast $podcast The podcast. - */ -function raisePermissionsIfCannotEdit($podcast) { - if (!currentUserCanEditPodcast($podcast)) { - CoreUtils::requirePermission(AUTH_PODCASTANYSHOW); - } -} - -/** - * Decides if the current user has edit rights to a podcast. - * - * @param MyRadio_Podcast $podcast The podcast to query. - * - * @return boolean True if the user can edit this podcast; false otherwise. - */ -function currentUserCanEditPodcast($podcast) { - // Doing this by ID saves having to query for all of the user's podcasts. - $user_podcast_ids = MyRadio_Podcast::getPodcastIDsAttachedToUser(); - return in_array($podcast->getID(), $user_podcast_ids); -} - - -?> diff --git a/src/Controllers/Podcast/createPodcast.php b/src/Controllers/Podcast/createPodcast.php deleted file mode 100644 index f0183b9d3..000000000 --- a/src/Controllers/Podcast/createPodcast.php +++ /dev/null @@ -1,9 +0,0 @@ - - * @version 20130815 - * @package MyRadio_Podcast - */ - -MyRadio_Podcast::getCreateForm()->render(); \ No newline at end of file diff --git a/src/Controllers/Podcast/default.php b/src/Controllers/Podcast/default.php index 5e450951f..021889321 100644 --- a/src/Controllers/Podcast/default.php +++ b/src/Controllers/Podcast/default.php @@ -1,15 +1,15 @@ - * @version 20130815 - * @package MyRadio_Podcast + * List a User's Podcasts. */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_Podcast; CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('tablescript', 'myury.datatable.default') - ->addVariable('title', 'My Podcasts') - ->addVariable('tabledata', - ServiceAPI::setToDataSource( - MyRadio_Podcast::getPodcastsAttachedToUser(), false)) - ->render(); \ No newline at end of file + ->addVariable('tablescript', 'myradio.podcasts') + ->addVariable('title', 'Podcasts') + ->addVariable('subtitle', 'My Podcasts') + ->addVariable( + 'tabledata', + CoreUtils::setToDataSource(MyRadio_Podcast::getPodcastsAttachedToUser()) + )->render(); diff --git a/src/Controllers/Podcast/doCreatePodcast.php b/src/Controllers/Podcast/doCreatePodcast.php deleted file mode 100644 index 3746ee413..000000000 --- a/src/Controllers/Podcast/doCreatePodcast.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @version 20130815 - * @package MyRadio_Podcast - */ - -$data = MyRadio_Podcast::getCreateForm()->readValues(); - -MyRadio_Podcast::create($data['title'], - $data['description'], - explode(' ', $data['tags']), - $data['file']['tmp_name'], - empty($data['show']) ? null: MyRadio_Show::getInstance($data['show']), - $data['credits']); - -header('Location: '.CoreUtils::makeURL('Podcast', 'default')); \ No newline at end of file diff --git a/src/Controllers/Podcast/doSetCover.php b/src/Controllers/Podcast/doSetCover.php deleted file mode 100644 index 4ea0174c9..000000000 --- a/src/Controllers/Podcast/doSetCover.php +++ /dev/null @@ -1,94 +0,0 @@ - - * @version 20140117 - * @package MyRadio_Podcasts - */ - -require_once 'common.php'; - -$values = podcastCoverForm()->readValues(); -$podcast = currentPodcast($values); -raisePermissionsIfCannotEdit($podcast); - -switch($values['cover_method']) { -case 'existing': - existingCoverFile($podcast, $values); - break; -case 'new': - uploadNewCoverFile($podcast, $values); - break; -default: - throw new MyRadioException('Unknown cover upload method.', 400); -} - - -// -// Helper functions -// - -function existingCoverFile($podcast, $values) { - $podcast->setCover($values['existing_cover']); -} - -function uploadNewCoverFile($podcast, $values) { - $temporary = $values['new_cover']['tmp_name']; - if (empty($temporary)) { - throw new MyRadioException('No new cover file uploaded.', 400); - } - - $url = moveCoverFile($podcast, $temporary); - - $podcast->setCover($url); -} - -function moveCoverFile($podcast, $temporary_file) { - $path = makeCoverFilePath($podcast, $temporary_file); - $file_path = Config::$public_media_path . $path; - checkCoverFileUnique($file_path); - moveCoverFileTo($file_path, $temporary_file); - return $path; -} - -function makeCoverFilePath($podcast, $temporary_file) { - return ( - coverFileDirectory() . - 'podcast' . - $podcast->getID() . - '-' . - time() . - '.' . - coverFileFormat($temporary_file) - ); -} - -function coverFileDirectory() { - return '/image_meta/MyRadioImageMetadata/'; -} - -function coverFileFormat($temporary_file) { - return explode( - '/', - finfo_file(finfo_open(FILEINFO_MIME_TYPE), $temporary_file) - )[1]; -} - -function checkCoverFileUnique($path) { - if (file_exists($path)) { - throw new MyRadioException('The cover filename chosen already exists.', 500); - } -} - -function moveCoverFileTo($path, $temporary_file) { - move_uploaded_file($temporary_file, $path); - if (!file_exists($path)) { - throw new MyRadioException('File move failed.', 500); - } -} - -coreUtils::backWithMessage('Cover set.'); - -?> diff --git a/src/Controllers/Podcast/editPodcast.php b/src/Controllers/Podcast/editPodcast.php new file mode 100644 index 000000000..9faf81a0e --- /dev/null +++ b/src/Controllers/Podcast/editPodcast.php @@ -0,0 +1,78 @@ +readValues(); + + if (empty($data['existing_cover']) && !is_uploaded_file($data['new_cover']['tmp_name'])) { + throw new MyRadioException('You must provide either an existing or new cover photo.', 400); + } + + if (empty($data['id'])) { + //create new + $podcast = MyRadio_Podcast::create( + $data['title'], + $data['description'], + $data['tags'], + $data['file']['tmp_name'], + empty($data['show']) ? null : MyRadio_Show::getInstance($data['show']), + $data['credits'] + ); + $return_message = "New Podcast Created"; + } else { + //submit edit + $podcast = MyRadio_Podcast::getInstance($data['id']); + + // Check if user can edit this podcast + if (!in_array($podcast->getID(), MyRadio_Podcast::getPodcastIDsAttachedToUser())) { + AuthUtils::requirePermission(AUTH_PODCASTANYSHOW); + } + + $podcast->setMeta('title', $data['title']) + ->setMeta('description', $data['description']) + ->setMeta('tag', CoreUtils::explodeTags($data['tags'])) + ->setCredits($data['credits']['member'], $data['credits']['credittype']); + + if (!empty($data['show'])) { + $podcast->setShow(MyRadio_Show::getInstance($data['show'])); + } else { + $podcast->setShow(null); + } + $return_message = "Podcast Updated"; + } + + if (!empty($data['existing_cover'])) { + $podcast->setCover($data['existing_cover']); + } elseif (is_uploaded_file($data['new_cover']['tmp_name'])) { + $podcast->createCover($data['new_cover']['tmp_name']); + } + + URLUtils::redirectWithMessage("Podcast", "default", $return_message); +} else { + //Not Submitted + if (isset($_REQUEST['podcast_id'])) { + //edit form + $podcast = MyRadio_Podcast::getInstance($_REQUEST['podcast_id']); + + // Check if user can edit this podcast + if (!in_array($podcast->getID(), MyRadio_Podcast::getPodcastIDsAttachedToUser())) { + AuthUtils::requirePermission(AUTH_EDITANYPODCAST); + } + + $podcast + ->getEditForm() + ->render(); + } else { + //create form + MyRadio_Podcast::getForm()->render(); + } +} diff --git a/src/Controllers/Podcast/setCover.php b/src/Controllers/Podcast/setCover.php deleted file mode 100644 index 5b2249f8f..000000000 --- a/src/Controllers/Podcast/setCover.php +++ /dev/null @@ -1,23 +0,0 @@ - - * @version 20140117 - * @package MyRadio_Podcasts - */ - -require_once 'common.php'; - -$podcast = currentPodcast(); -raisePermissionsIfCannotEdit($podcast); - -podcastCoverForm( -)->setFieldValue( - 'podcastid', $podcast->getID() -)->setFieldValue( - 'existing_cover', $podcast->getCover() -)->render(); - -?> diff --git a/src/Controllers/Podcast/suspendPodcast.php b/src/Controllers/Podcast/suspendPodcast.php new file mode 100644 index 000000000..da642a636 --- /dev/null +++ b/src/Controllers/Podcast/suspendPodcast.php @@ -0,0 +1,69 @@ +readValues(); + } catch (MyRadioException $e) { + try { + $data = MyRadio_Podcast::getUnsuspendForm()->readValues(); + } catch (MyRadioException $e) { + throw new MyRadioException("Can't read suspend/unsuspend form values."); + } + } + + $podcast = MyRadio_Podcast::getInstance($data["podcast_id"]); + + // Check if the user can edit this podcast + if (!in_array($podcast->getID(), MyRadio_Podcast::getPodcastIDsAttachedToUser())) { + AuthUtils::requirePermission(AUTH_PODCASTANYSHOW); + } + + // Request unsuspension or suspend podcast + if ($podcast->isSuspended()) { + if ($data["reason"] != "") { + $podcast->requestUnsuspend($data["reason"]); + $return_message = "Request Sent"; + } else { + $return_message = "You need to provide a reason for requesting unsuspension"; + } + } else { + if (!$data["confirm"]) { + $return_message = "You need to confirm the suspension."; + } else { + $podcast->setSuspended(true); + $return_message = "Podcast Suspended"; + } + } + + URLUtils::redirectWithMessage("Podcast", "default", $return_message); +} else { + //Not Submitted + if (isset($_REQUEST['podcast_id'])) { + $podcast = MyRadio_Podcast::getInstance($_REQUEST['podcast_id']); + + // Check if user can suspend this podcast + if (!in_array($podcast->getID(), MyRadio_Podcast::getPodcastIDsAttachedToUser())) { + AuthUtils::requirePermission(AUTH_EDITANYPODCAST); + } + + if (!$podcast->isSuspended()) { + $podcast->getSuspendForm()->render(); + } else { + $podcast->getUnsuspendForm()->render(); + } + } else { + throw new MyRadioException("Podcast ID needs specifying."); + } +} diff --git a/src/Controllers/Profile/addTrainingStatus.php b/src/Controllers/Profile/addTrainingStatus.php index 6672b2a69..d87ecd080 100644 --- a/src/Controllers/Profile/addTrainingStatus.php +++ b/src/Controllers/Profile/addTrainingStatus.php @@ -1,13 +1,15 @@ - * @version 20130825 - * @package MyRadio_Profile + * Gives a User a Training Status. */ +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_User; +use \MyRadio\ServiceAPI\MyRadio_TrainingStatus; +use \MyRadio\ServiceAPI\MyRadio_UserTrainingStatus; + MyRadio_UserTrainingStatus::create( - MyRadio_TrainingStatus::getInstance($_POST['status_id']), - MyRadio_User::getInstance($_POST['memberid'])); + MyRadio_TrainingStatus::getInstance($_POST['status_id']), + MyRadio_User::getInstance($_POST['memberid']) +); -CoreUtils::backWithMessage('Training data updated'); \ No newline at end of file +URLUtils::backWithMessage('Training data updated'); diff --git a/src/Controllers/Profile/assignOfficer.php b/src/Controllers/Profile/assignOfficer.php new file mode 100644 index 000000000..80f401e4c --- /dev/null +++ b/src/Controllers/Profile/assignOfficer.php @@ -0,0 +1,42 @@ +readValues(); + + $officer = MyRadio_Officer::getInstance($data['id']); + + if ($data['member']->isCurrentlyPaid()) { + $officer->assignOfficer($data['member']->getID()); + URLUtils::backWithMessage('Officership Assigned!'); + } else { + throw new MyRadioException('Member is not paid!', 400); + } +} else { + //Not Submitted + + if (isset($_REQUEST['officerid'])) { + //assign form + $officer = MyRadio_Officer::getInstance($_REQUEST['officerid']); + if ($officer->getStatus() == "h") { + throw new MyRadioException("Officer is historical.", 400); + } + + MyRadio_Officer::getAssignForm() + ->editMode( + $officer->getID(), + [] + ) + ->setTitle('Assign Officer - '.$officer->getName()) + ->render(); + } else { + // Error + throw new MyRadioException('Officer ID must be provided.', 400); + } +} diff --git a/src/Controllers/Profile/bulkAdd.php b/src/Controllers/Profile/bulkAdd.php index cdfd3996c..a09a1de18 100644 --- a/src/Controllers/Profile/bulkAdd.php +++ b/src/Controllers/Profile/bulkAdd.php @@ -1,9 +1,40 @@ - * @version 20130717 - * @package MyRadio_Profile */ -MyRadio_User::getBulkAddForm()->render(); \ No newline at end of file +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_User; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = MyRadio_User::getBulkAddForm()->readValues(); + $template = CoreUtils::getTemplateObject(); + + for ($i = 0; $i < sizeof($data['bulkaddrepeater']['fname']); ++$i) { + $params = []; + foreach ($data['bulkaddrepeater'] as $key => $v) { + $params[$key] = $data['bulkaddrepeater'][$key][$i]; + } + try { + $user = MyRadio_User::createOrActivate( + $params['fname'], + $params['sname'], + $params['eduroam'], + $params['collegeid'] + ); + if ($user === null) { + $template->addInfo($params['fname'] . ' ' . $params['sname'] . ' already has an account!'); + } else { + $template->addInfo('Added Member with ID '.$user->getID()); + } + } catch (MyRadioException $e) { + $template->addError('Could not add '.$params['eduroam'].': '.$e->getMessage()); + } + } + + $template->setTemplate('MyRadio/text.twig')->render(); +} else { + //Not Submitted + MyRadio_User::getBulkAddForm()->render(); +} diff --git a/src/Controllers/Profile/default.php b/src/Controllers/Profile/default.php index 7c48c28fd..71b6655f3 100644 --- a/src/Controllers/Profile/default.php +++ b/src/Controllers/Profile/default.php @@ -1,10 +1,6 @@ - * @version 21072012 - * @package MyRadio_Profile */ require 'Controllers/Profile/view.php'; diff --git a/src/Controllers/Profile/doBulkAdd.php b/src/Controllers/Profile/doBulkAdd.php deleted file mode 100644 index 4d764a8f5..000000000 --- a/src/Controllers/Profile/doBulkAdd.php +++ /dev/null @@ -1,26 +0,0 @@ - - * @version 20130717 - * @package MyRadio_Profile - */ -$data = MyRadio_User::getBulkAddForm()->readValues(); -$template = CoreUtils::getTemplateObject(); - -for ($i = 0; $i < sizeof($data['bulkaddrepeater']['fname']); $i++) { - $params = array(); - foreach ($data['bulkaddrepeater'] as $key => $v) { - $params[$key] = $data['bulkaddrepeater'][$key][$i]; - } - try { - $user = MyRadio_User::create($params['fname'], $params['sname'], $params['eduroam'], - $params['sex'], $params['collegeid']); - $template->addInfo('Added Member with ID '.$user->getID()); - } catch (MyRadioException $e) { - $template->addError('Could not add '.$params['eduroam'].': '.$e->getMessage()); - } -} - -$template->setTemplate('MyRadio/text.twig')->render(); \ No newline at end of file diff --git a/src/Controllers/Profile/doEdit.php b/src/Controllers/Profile/doEdit.php deleted file mode 100644 index 58892c96c..000000000 --- a/src/Controllers/Profile/doEdit.php +++ /dev/null @@ -1,38 +0,0 @@ - - * @version 20130731 - * @package MyRadio_Profile - */ -// Set if trying to view another member's profile page -if (isset($_REQUEST['profileedit-memberid']) && MyRadio_User::getInstance()->hasAuth(AUTH_EDITANYPROFILE)) { - $user = MyRadio_User::getInstance($_REQUEST['profileedit-memberid']); -} else { - $user = MyRadio_User::getInstance(); -} - -$data = $user->getEditForm()->readValues(); - -$user->setFName($data['fname']) - ->setSName($data['sname']) - ->setSex($data['sex']) - ->setCollegeID($data['collegeid']) - ->setPhone($data['phone']) - ->setEmail($data['email']) - ->setReceiveEmail($data['receive_email']) - ->setEduroam($data['eduroam']) - ->setBio($data['bio']); - -if (!empty($data['photo']['tmp_name'])) { - $user->setProfilePhoto(MyRadio_Photo::create($data['photo']['tmp_name'])); -} - -if (isset($data['local_name'])) { - $user->setLocalName($data['local_name']) - ->setLocalAlias($data['local_alias']); -} - -header('Location: ' . CoreUtils::makeURL('Profile', 'view', array('memberid' => $data['memberid']))); \ No newline at end of file diff --git a/src/Controllers/Profile/doQuickAdd.php b/src/Controllers/Profile/doQuickAdd.php deleted file mode 100644 index 7fec09952..000000000 --- a/src/Controllers/Profile/doQuickAdd.php +++ /dev/null @@ -1,13 +0,0 @@ - - * @version 20130717 - * @package MyRadio_Profile - */ -$params = MyRadio_User::getQuickAddForm()->readValues(); -$user = MyRadio_User::create($params['fname'], $params['sname'], $params['eduroam'], - $params['sex'], $params['collegeid'], null, $params['phone']); - -CoreUtils::backWithMessage('New Member has been created with ID '.$user->getID()); \ No newline at end of file diff --git a/src/Controllers/Profile/edit.php b/src/Controllers/Profile/edit.php index 72957c9ec..6a688da0e 100644 --- a/src/Controllers/Profile/edit.php +++ b/src/Controllers/Profile/edit.php @@ -1,17 +1,57 @@ - * @version 20130715 - * @package MyRadio_Profile */ +use \MyRadio\Config; +use \MyRadio\MyRadio\AuthUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_User; +use \MyRadio\ServiceAPI\MyRadio_Photo; // Set if trying to view another member's profile page -if (isset($_REQUEST['memberid']) && MyRadio_User::getInstance()->hasAuth(AUTH_EDITANYPROFILE)) { - $user = MyRadio_User::getInstance($_REQUEST['memberid']); +if (isset($_REQUEST['profileedit-memberid']) && AuthUtils::hasPermission(AUTH_EDITANYPROFILE)) { + $user = MyRadio_User::getInstance($_REQUEST['profileedit-memberid']); +} elseif (isset($_REQUEST['memberid']) && AuthUtils::hasPermission(AUTH_EDITANYPROFILE)) { + $user = MyRadio_User::getInstance($_REQUEST['memberid']); } else { - $user = MyRadio_User::getInstance(); + $user = MyRadio_User::getInstance(); } -$user->getEditForm()->render(); \ No newline at end of file +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = $user->getEditForm()->readValues(); + + $user->setFName($data['fname']) + ->setSName($data['sname']) + ->setCollegeID($data['collegeid']) + ->setPhone($data['phone']) + ->setEmail($data['email']) + ->setReceiveEmail($data['receive_email']) + ->setEduroam($data['eduroam']) + ->setBio($data['bio']) + ->setHideProfile($data['hide']); + + if ($data['data_removal']) { + $user->setDataRemoval('optout'); + } else { + $user->setDataRemoval('default'); + } + + if (!empty(Config::$contract_uri)) { + $user->setContractSigned($data['contract']); + } + + if (!empty($data['photo']['tmp_name'])) { + $user->setProfilePhoto(MyRadio_Photo::create($data['photo']['tmp_name'])); + } + + if (isset($data['local_name'])) { + $user->setLocalName($data['local_name']) + ->setLocalAlias($data['local_alias']); + } + + URLUtils::redirectWithMessage('Profile', 'view', 'User Updated'); +} else { + //Not Submitted + $user->getEditForm()->render(); +} diff --git a/src/Controllers/Profile/editOfficer.php b/src/Controllers/Profile/editOfficer.php index 270071eaa..15b8e3a1f 100644 --- a/src/Controllers/Profile/editOfficer.php +++ b/src/Controllers/Profile/editOfficer.php @@ -1,16 +1,81 @@ - * @version 20130809 - * @package MyRadio_Profile + * Edit an Officer. */ +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Officer; +use MyRadio\ServiceAPI\MyRadio_Team; -$officer = MyRadio_Officer::getInstance($_REQUEST['officerid']); +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = MyRadio_Officer::getForm()->readValues(); -CoreUtils::getTemplateObject() - ->setTemplate('Profile/officer.twig') - ->addVariable('title', $officer->getName()) - ->addVariable('officer', $officer->toDataSource(true)) - ->render(); \ No newline at end of file + if (empty($data['id'])) { + $team = MyRadio_Team::getInstance($data['team']); + //create new + $officer = MyRadio_Officer::createOfficer( + $data['name'], + $data['description'], + $data['alias'], + $data['ordering'], + $team, + $data['type'] + ); + } else { + //submit edit + $officer = MyRadio_Officer::getInstance($data['id']); + + // update officer + $officer + ->setName($data['name']) + ->setDescription($data['description']) + ->setAlias($data['alias']) + ->setOrdering($data['ordering']) + ->setTeam($data['team']) + ->setType($data['type']) + ->setStatus($data['status']); + + // remove empty permissions values + $data['permissions'] = array_filter($data['permissions']['permission']) ?: []; + + // get IDs of current officer permissions + $currentPerms = []; + $officerPerms = $officer->getPermissions(); + foreach ($officerPerms as $perm) { + $currentPerms[] = (int) $perm['value']; + } + + // Get permissions to add or remove + $addPerms = array_diff($data['permissions'], $currentPerms); + $remPerms = array_diff($currentPerms, $data['permissions']); + + // Add permissions + if (!empty($addPerms)) { + foreach ($addPerms as $perm) { + $officer->addPermission($perm); + } + } + // Remove permissions + if (!empty($remPerms)) { + foreach ($remPerms as $perm) { + $officer->revokePermission($perm); + } + } + } + + URLUtils::backWithMessage('Officer Updated!'); +} else { + //Not Submitted + + if (isset($_REQUEST['officerid'])) { + //edit form + $officer = MyRadio_Officer::getInstance($_REQUEST['officerid']); + + $officer + ->getEditForm() + ->render(); + } else { + //create form + MyRadio_Officer::getForm()->render(); + } +} diff --git a/src/Controllers/Profile/list.php b/src/Controllers/Profile/list.php index e0ce808fd..89c8bf095 100644 --- a/src/Controllers/Profile/list.php +++ b/src/Controllers/Profile/list.php @@ -1,25 +1,27 @@ - * @version 20130516 - * @package MyRadio_Profile + * @todo Use Users better */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\Profile; $members = Profile::getThisYearsMembers(); foreach ($members as $k => $v) { - $members[$k]['name'] = array( - 'display' => 'text', - 'url' => CoreUtils::makeURL('Profile', 'view', array('memberid' => $v['memberid'])), - 'value' => $v['name'] - ); + $members[$k]['name'] = [ + 'display' => 'text', + 'url' => URLUtils::makeURL('Profile', 'view', ['memberid' => $v['memberid']]), + 'value' => $v['name'], + ]; + unset($members[$k]['email']); + unset($members[$k]['eduroam']); } CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('tablescript', 'myury.profile.list') - ->addVariable('title', 'Members List') - ->addVariable('tabledata', $members) - ->render(); \ No newline at end of file + ->addVariable('tablescript', 'myradio.profile.list') + ->addVariable('title', 'Members List') + ->addVariable('tabledata', $members) + ->render(); diff --git a/src/Controllers/Profile/listOfficers.php b/src/Controllers/Profile/listOfficers.php index f647f2ad0..601061f6b 100644 --- a/src/Controllers/Profile/listOfficers.php +++ b/src/Controllers/Profile/listOfficers.php @@ -1,33 +1,44 @@ - * @version 20130516 - * @package MyRadio_Profile */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\Profile; $officers = Profile::getOfficers(); foreach ($officers as $k => $v) { - if (!empty($officers[$k]['name'])) { - $officers[$k]['name'] = array( - 'display' => 'text', - 'url' => CoreUtils::makeURL('Profile', 'view', array('memberid' => $v['memberid'])), - 'value' => $v['name'] - ); - } - $officers[$k]['editlink'] = array( - 'display' => 'icon', - 'value' => 'wrench', + if (!empty($officers[$k]['name'])) { + $officers[$k]['name'] = [ + 'display' => 'text', + 'url' => URLUtils::makeURL('Profile', 'view', ['memberid' => $v['memberid']]), + 'value' => $v['name'], + ]; + } + $officers[$k]['viewlink'] = [ + 'display' => 'icon', + 'value' => 'user', + 'title' => 'View Officer', + 'url' => URLUtils::makeURL('Profile', 'officer', ['officerid' => $v['officerid']]), + ]; + $officers[$k]['editlink'] = [ + 'display' => 'icon', + 'value' => 'pencil', 'title' => 'Edit Officer', - 'url' => CoreUtils::makeURL('Profile', 'editOfficer', array('officerid' => $v['officerid'])), - ); + 'url' => URLUtils::makeURL('Profile', 'editOfficer', ['officerid' => $v['officerid']]), + ]; + $officers[$k]['assignlink'] = [ + 'display' => 'icon', + 'value' => 'plus', + 'title' => 'Assign Officer', + 'url' => URLUtils::makeURL('Profile', 'assignOfficer', ['officerid' => $v['officerid']]), + ]; } CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('tablescript', 'myury.profile.listOfficers') - ->addVariable('title', 'Officers List') - ->addVariable('tabledata', $officers) - ->render(); \ No newline at end of file + ->addVariable('tablescript', 'myradio.profile.listOfficers') + ->addVariable('title', 'Officers List') + ->addVariable('tabledata', $officers) + ->render(); diff --git a/src/Controllers/Profile/listPrevious.php b/src/Controllers/Profile/listPrevious.php new file mode 100644 index 000000000..d1a131d6b --- /dev/null +++ b/src/Controllers/Profile/listPrevious.php @@ -0,0 +1,27 @@ + $v) { + $members[$k]['name'] = [ + 'display' => 'text', + 'url' => URLUtils::makeURL('Profile', 'view', ['memberid' => $v['memberid']]), + 'value' => $v['name'], + ]; + unset($members[$k]['email']); + unset($members[$k]['eduroam']); +} + +CoreUtils::getTemplateObject()->setTemplate('table.twig') + ->addVariable('tablescript', 'myradio.profile.list') + ->addVariable('title', 'Last Year\'s Members List') + ->addVariable('tabledata', $members) + ->render(); diff --git a/src/Controllers/Profile/listTrainers.php b/src/Controllers/Profile/listTrainers.php index bdcad461e..2283a94d4 100644 --- a/src/Controllers/Profile/listTrainers.php +++ b/src/Controllers/Profile/listTrainers.php @@ -1,17 +1,18 @@ - * @version 20131014 - * @package MyRadio_Profile + * List all trainers. */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_TrainingStatus; -$officers = CoreUtils::dataSourceParser( - MyRadio_TrainingStatus::getInstance(3)->getAwardedTo()); +$trainers = CoreUtils::dataSourceParser(MyRadio_TrainingStatus::getInstance(3)->getAwardedTo()); + +foreach ($trainers as $key => $value) { + $trainers[$key]['awarded_time'] = date('Y/m/d', $trainers[$key]['awarded_time']); +} CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('tablescript', 'myury.datatable.default') - ->addVariable('title', 'Trainers List') - ->addVariable('tabledata', $officers) - ->render(); \ No newline at end of file + ->addVariable('tablescript', 'myradio.profile.listTrainers') + ->addVariable('title', 'Trainers List') + ->addVariable('tabledata', $trainers) + ->render(); diff --git a/src/Controllers/Profile/markPaid.php b/src/Controllers/Profile/markPaid.php index f331c939a..ec540389c 100644 --- a/src/Controllers/Profile/markPaid.php +++ b/src/Controllers/Profile/markPaid.php @@ -1,12 +1,12 @@ -* @version 20140129 -* @package MyRadio_Profile +* Adds a User payment. */ +use \MyRadio\Config; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_User; + $user = MyRadio_User::getInstance($_REQUEST['memberid']); $user->setPayment(Config::$membership_fee); -CoreUtils::backWithMessage('Payment data updated'); +URLUtils::backWithMessage('Payment data updated'); diff --git a/src/Controllers/Profile/officer.php b/src/Controllers/Profile/officer.php new file mode 100644 index 000000000..d33d6d5d3 --- /dev/null +++ b/src/Controllers/Profile/officer.php @@ -0,0 +1,14 @@ +setTemplate('Profile/officer.twig') + ->addVariable('title', $officer->getName()) + ->addVariable('officer', $officer->toDataSource(['history', 'permissions'])) + ->render(); diff --git a/src/Controllers/Profile/officers.php b/src/Controllers/Profile/officers.php index 482fb2d64..5726a5971 100644 --- a/src/Controllers/Profile/officers.php +++ b/src/Controllers/Profile/officers.php @@ -2,27 +2,29 @@ /** * This provides similar information to listOfficers, but in a far nicer format. - * - * @author Lloyd Wallis - * @version 20130802 - * @package MyRadio_Profile */ +use \MyRadio\Config; +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_User; +use \MyRadio\ServiceAPI\Profile; + $officers = Profile::getOfficers(); foreach ($officers as $k => $v) { - if (!empty($officers[$k]['name'])) { - $officers[$k]['url'] = CoreUtils::makeURL('Profile', 'view', array('memberid' => $v['memberid'])); - } - - if (!empty($officers[$k]['memberid'])) { - $image = MyRadio_User::getInstance($officers[$k]['memberid'])->getProfilePhoto(); - $officers[$k]['image'] = $image !== null ? $image->getURL() : Config::$default_person_uri; - } else { - $officers[$k]['image'] = Config::$vacant_officer_uri; - } + if (!empty($officers[$k]['name'])) { + $officers[$k]['url'] = URLUtils::makeURL('Profile', 'view', ['memberid' => $v['memberid']]); + } + + if (!empty($officers[$k]['memberid'])) { + $image = MyRadio_User::getInstance($officers[$k]['memberid'])->getProfilePhoto(); + $officers[$k]['image'] = $image !== null ? $image->getURL() : Config::$default_person_uri; + } else { + $officers[$k]['image'] = Config::$vacant_officer_uri; + } } CoreUtils::getTemplateObject()->setTemplate('Profile/officers.twig') - ->addVariable('title', Config::$short_name.' Committee') - ->addVariable('officers', $officers) - ->render(); \ No newline at end of file + ->addVariable('title', Config::$short_name.' Committee') + ->addVariable('officers', $officers) + ->render(); diff --git a/src/Controllers/Profile/quickAdd.php b/src/Controllers/Profile/quickAdd.php index 0a6d8f25e..e59f7141f 100644 --- a/src/Controllers/Profile/quickAdd.php +++ b/src/Controllers/Profile/quickAdd.php @@ -1,9 +1,29 @@ - * @version 20130717 - * @package MyRadio_Profile */ -MyRadio_User::getQuickAddForm()->render(); \ No newline at end of file +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_User; + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $params = MyRadio_User::getQuickAddForm()->readValues(); + $user = MyRadio_User::createOrActivate( + $params['fname'], + $params['sname'], + $params['eduroam'], + $params['collegeid'], + null, + $params['phone'] + ); + + if ($user === null) { + $msg = 'This member already has an account!'; + } else { + $msg = 'New Member has been created with ID '.$user->getID(); + } + URLUtils::backWithMessage($msg); +} else { + //Not Submitted + MyRadio_User::getQuickAddForm()->render(); +} diff --git a/src/Controllers/Profile/timeline.php b/src/Controllers/Profile/timeline.php old mode 100755 new mode 100644 index 37c82685d..01678851d --- a/src/Controllers/Profile/timeline.php +++ b/src/Controllers/Profile/timeline.php @@ -1,17 +1,16 @@ - * @version 20130803 - * @package MyRadio_Profile */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_User; + $user = MyRadio_User::getInstance(isset($_GET['memberid']) ? $_GET['memberid'] : $_SESSION['memberid']); $data = $user->getTimeline(); CoreUtils::getTemplateObject()->setTemplate('Profile/timeline.twig') - ->addVariable('title', 'Timeline') - ->addVariable('timeline', $data) - ->addVariable('profile_name', $user->getName()) - ->render(); \ No newline at end of file + ->addVariable('title', 'Timeline') + ->addVariable('timeline', $data) + ->addVariable('profile_name', $user->getName()) + ->render(); diff --git a/src/Controllers/Profile/view.php b/src/Controllers/Profile/view.php index dc05e8e12..cdc33d7d4 100644 --- a/src/Controllers/Profile/view.php +++ b/src/Controllers/Profile/view.php @@ -1,59 +1,84 @@ - * - Any member can view Name, Sex, College, Officership, Training status and photo of any other member + * - Any member can view Name, College, Officership, Training status and photo of any other member * - Any member can also view Phone & email alias of any committee member - * - Members with AUTH_VIEWOTHERMEMBERS can view eduroam/email/locked/last login/paid of any other member - * - * @author Andy Durant - * @version 20130717 - * @package MyRadio_Profile + * - Members with AUTH_VIEWOTHERMEMBERS can view eduroam/email/locked/last login/paid of any other member. */ +use \MyRadio\MyRadio\AuthUtils; +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_User; +use \MyRadio\ServiceAPI\MyRadio_TrainingStatus; + // Set if trying to view another member's profile page $user = MyRadio_User::getInstance(empty($_REQUEST['memberid']) ? -1 : $_REQUEST['memberid']); $visitor = MyRadio_User::getInstance(); //Add global user data -$userData = $user->toDataSource(); -$userData['training'] = CoreUtils::dataSourceParser($user->getAllTraining(true)); -$userData['training_avail'] = CoreUtils::dataSourceParser( - MyRadio_TrainingStatus::getAllAwardableTo($user)); +$mixins = []; -if ($user->isOfficer()) { - $userData['phone'] = $user->getPhone(); - $userData['email'] = $user->getPublicEmail(); +if ($user->getID() === $visitor->getID() || AuthUtils::hasPermission(AUTH_VIEWOTHERMEMBERS)) { + $mixins = ['personal_data', 'officerships', 'payment']; +} elseif ($user->isOfficer()) { + // A non-officer viewing an officer + $mixins = ['officerships']; } -if (CoreUtils::hasPermission(AUTH_VIEWOTHERMEMBERS)) { - $userData['email'] = $user->getEmail(); - $userData['eduroam'] = $user->getEduroam(); - $userData['local_alias'] = $user->getLocalAlias(); - $userData['local_name'] = $user->getLocalName(); - $userData['account_locked'] = $user->getAccountLocked(); - $userData['last_login'] = $user->getLastLogin(); - $userData['payment'] = $user->getAllPayments(); - $userData['receive_email'] = $user->getReceiveEmail(); +$userData = $user->toDataSource($mixins); + +$userData['training'] = CoreUtils::dataSourceParser($user->getAllTraining()); +$userData['training_avail'] = CoreUtils::dataSourceParser(MyRadio_TrainingStatus::getAllAwardableTo($user)); + +// A non-officer viewing an officer +if ($user->isOfficer()) { + $userData['phone'] = $user->getPhone(); } $template = CoreUtils::getTemplateObject()->setTemplate('Profile/user.twig') - ->addVariable('title', 'View Profile') - ->addVariable('user', $userData); + ->addVariable('title', 'View Profile') + ->addVariable('user', $userData); -if ($user->getID() === $visitor->getID() or $visitor->hasAuth(AUTH_EDITANYPROFILE)) { - $template->addVariable('editurl', 'Edit Profile'); +if ($user->getID() === $visitor->getID() || $visitor->hasAuth(AUTH_EDITANYPROFILE)) { + $template->addVariable( + 'editurl', + 'Edit Profile' + ); } -if (CoreUtils::hasPermission(AUTH_IMPERSONATE) && - ($user->hasAuth(AUTH_BLOCKIMPERSONATE) === false or CoreUtils::hasPermission(AUTH_IMPERSONATE_BLOCKED_USERS))) { - $template->addVariable('impersonateurl', - 'Impersonate User'); +if (AuthUtils::hasPermission(AUTH_IMPERSONATE) + && ($user->hasAuth(AUTH_BLOCKIMPERSONATE) === false + || AuthUtils::hasPermission(AUTH_IMPERSONATE_BLOCKED_USERS)) +) { + $template->addVariable( + 'impersonateurl', + 'Impersonate User' + ); } -if (CoreUtils::hasPermission(AUTH_LOCK)) { - $template->addVariable('lockurl', - 'Disable Account'); +if (AuthUtils::hasPermission(AUTH_LOCK)) { + $template->addVariable( + 'lockurl', + 'Disable Account' + ); } -if (CoreUtils::hasPermission(AUTH_MARKPAYMENT)) { - $template->addVariable('canmarkpayments', true); +if (AuthUtils::hasPermission(AUTH_MARKPAYMENT)) { + $template->addVariable('can_mark_payments', true); } -$template->render(); \ No newline at end of file +$template->render(); diff --git a/src/Controllers/Quotes/addQuote.php b/src/Controllers/Quotes/addQuote.php deleted file mode 100644 index 93f13f915..000000000 --- a/src/Controllers/Quotes/addQuote.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @package MyRadio_Quotes - */ - -MyRadio_JsonFormLoader::loadFromModule( - $module, - 'addQuote', - 'doAddQuote', - [] -)->render(); - -?> diff --git a/src/Controllers/Quotes/default.php b/src/Controllers/Quotes/default.php index c50c014dc..5df8a3388 100644 --- a/src/Controllers/Quotes/default.php +++ b/src/Controllers/Quotes/default.php @@ -1,15 +1,7 @@ setTemplate( - 'table.twig' -)->addVariable( - 'tablescript', - 'myury.datatable.default' -)->addVariable( - 'title', - 'Quotes' -)->addVariable( - 'tabledata', - ServiceAPI::setToDataSource(MyRadio_Quote::getAll()) -)->render(); -?> + +use \MyRadio\MyRadio\CoreUtils; + +CoreUtils::getTemplateObject()->setTemplate('Quotes/default.twig') + ->addVariable('title', 'Quotes') + ->render(); diff --git a/src/Controllers/Quotes/doAddQuote.php b/src/Controllers/Quotes/doAddQuote.php deleted file mode 100644 index b56ef1294..000000000 --- a/src/Controllers/Quotes/doAddQuote.php +++ /dev/null @@ -1,22 +0,0 @@ - - * @package MyRadio_Quotes - */ - -$data = MyRadio_JsonFormLoader::loadFromModule( - $module, - 'addQuote', - 'doAddQuote', - [] -)->readValues(); - -MyRadio_Quote::create($data); - -CoreUtils::backWithMessage('Quote added.'); - -?> diff --git a/src/Controllers/Quotes/doEditQuote.php b/src/Controllers/Quotes/doEditQuote.php deleted file mode 100644 index d91945cfb..000000000 --- a/src/Controllers/Quotes/doEditQuote.php +++ /dev/null @@ -1,49 +0,0 @@ - - * @package MyURY_Quote - */ - -/* - * Creates a new quote. - * - * @param $data The data hash from the quotes form. - * - * @return Nothing. This function writes directly to the database. - */ -function create_quote($data) { - MyURY_Quote::create($data); -} - -/* - * Edits the quote with the given ID. - * - * @param $id The ID of the quote to edit. - * @param $data The data hash from the quotes form. - * - * @return Nothing. This function writes directly to the database. - */ -function edit_quote($id, $data) { - $quote = MyURY_ChartRelease::getInstance($id); - $quote - ->setSource($data['source']) - ->setText($data['text']) - ->setDate($data['date']); -} - - -/* - * END OF HELPER FUNCTIONS - */ - -$form = MyURY_JsonFormLoader::loadFromModule( - $module, 'quotefrm', 'doEditQuote', [] -); - -$data = $form->readValues(); - -empty($data['id']) ? create_quote($data) : edit_quote($data['id'], $data); - -CoreUtils::redirect($module); diff --git a/src/Controllers/Quotes/editQuote.php b/src/Controllers/Quotes/editQuote.php deleted file mode 100644 index 79f1a75bd..000000000 --- a/src/Controllers/Quotes/editQuote.php +++ /dev/null @@ -1,35 +0,0 @@ - - * @package MyURY_Quotes - */ - -$form = MyURY_JsonFormLoader::loadFromModule( - $module, 'quotefrm', 'doEditQuote', - [] -); - -if ($_REQUEST['quote_id']) { - $quote = MyURY_ChartRelease::getInstance($_REQUEST['quote_id']); - - $form->editMode( - $quote->getID(), - array_merge( - [ - 'date' => CoreUtils::happyTime($quote->getDate(), false), - 'source' => $quote->getSource(), - 'text' => $quote->getText() - ], - $chart_rows_form - ) - ); - -} else { - $form->setTitle('Create Quote'); - $form->setFieldValue('date', CoreUtils::happyTime(time(), false)); -} - -$form->render(); -?> diff --git a/src/Controllers/SIS/default.php b/src/Controllers/SIS/default.php old mode 100755 new mode 100644 index 9c4abc539..64144480c --- a/src/Controllers/SIS/default.php +++ b/src/Controllers/SIS/default.php @@ -1,21 +1,16 @@ - * @version 20130923 - * @package MyRadio_SIS + * Main renderer for SIS. */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\SIS\SIS_Utils; CoreUtils::requireTimeslot(); - $template = 'SIS/main.twig'; - $title = 'SIS'; - $plugins = SIS_Utils::getPlugins(); - $tabs = SIS_Utils::getTabs(); +$template = 'SIS/main.twig'; +$title = 'SIS'; CoreUtils::getTemplateObject()->setTemplate($template) - ->addVariable('title', $title) - ->addVariable('plugins', $plugins) - ->addVariable('tabs', $tabs) - ->render(); \ No newline at end of file + ->addVariable('title', $title) + ->addVariable('modules', SIS_Utils::getModulesForUser()) + ->render(); diff --git a/src/Controllers/SIS/help.hide.php b/src/Controllers/SIS/help.hide.php old mode 100755 new mode 100644 index aba213f07..e0e836827 --- a/src/Controllers/SIS/help.hide.php +++ b/src/Controllers/SIS/help.hide.php @@ -1,11 +1,8 @@ - * @version 20131123 - * @package MyRadio_SIS + * Help Tab Hidder for SIS. */ +use \MyRadio\SIS\SIS_Utils; SIS_Utils::hideHelpTab($_SESSION['memberid']); -header('HTTP/1.1 204 No Content'); \ No newline at end of file +header('HTTP/1.1 204 No Content'); diff --git a/src/Controllers/SIS/messages.markread.php b/src/Controllers/SIS/messages.markread.php old mode 100755 new mode 100644 index a7de09069..d98843197 --- a/src/Controllers/SIS/messages.markread.php +++ b/src/Controllers/SIS/messages.markread.php @@ -1,11 +1,8 @@ - * @version 20131101 - * @package MyRadio_SIS + * Message Mark Reader for SIS. */ +use \MyRadio\SIS\SIS_Messages; SIS_Messages::setMessageStatus(intval($_GET['id']), SIS_Messages::MSG_STATUS_READ); -header('HTTP/1.1 204 No Content'); \ No newline at end of file +header('HTTP/1.1 204 No Content'); diff --git a/src/Controllers/SIS/news.php b/src/Controllers/SIS/news.php index 83b7a3ba9..c8509cb59 100644 --- a/src/Controllers/SIS/news.php +++ b/src/Controllers/SIS/news.php @@ -1,30 +1,27 @@ - * @version 20131102 - * @package MyRadio_SIS + * IRN Proxy for SIS. */ /* Proxy based on https://github.com/Alexxz/Simple-php-proxy-script */ +use \MyRadio\Config; +use \MyRadio\MyRadio\URLUtils; + $dest_host = Config::$news_provider; -$proxy_base_url = '/' . ltrim(str_replace($_SERVER['HTTP_HOST'], '', CoreUtils::makeURL('SIS', 'news')), '/'); +$proxy_base_url = '/'.ltrim(str_replace($_SERVER['HTTP_HOST'], '', URLUtils::makeURL('SIS', 'news')), '/'); $proxying_url = Config::$news_proxy; - -$proxied_headers = array('Set-Cookie', 'Content-Type', 'Cookie', 'Location'); +$proxied_headers = ['Set-Cookie', 'Content-Type', 'Cookie', 'Location']; //canonical trailing slash -$proxy_base_url_canonical = rtrim($proxy_base_url, '/ ') . '/'; +$proxy_base_url_canonical = rtrim($proxy_base_url, '/ ').'/'; //check if valid -if( strpos($_SERVER['REQUEST_URI'], $proxy_base_url) !== 0 ) -{ - die("The config paramter \$prox_base_url \"$proxy_base_url\" that you specified +if (strpos($_SERVER['REQUEST_URI'], $proxy_base_url) !== 0) { + die("The config paramter \$prox_base_url \"$proxy_base_url\" that you specified does not match the beginning of the request URI: ". $_SERVER['REQUEST_URI']); } @@ -33,8 +30,7 @@ $proxy_request_url = substr($_SERVER['REQUEST_URI'], strlen($proxy_base_url_canonical)); //final proxied request url -$request_url = rtrim($dest_host, '/ ') . '/' . $proxy_request_url; - +$request_url = rtrim($dest_host, '/ ').'/'.$proxy_request_url; /* Init CURL */ $ch = curl_init(); @@ -44,26 +40,23 @@ curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_HEADER, 1); curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']); -curl_setopt($ch, CURLOPT_HTTPHEADER, array('Expect:')); +curl_setopt($ch, CURLOPT_HTTPHEADER, ['Expect:']); curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_0); /* Collect and pass client request headers */ -if(isset($_SERVER['HTTP_COOKIE'])) -{ - $hdrs[]="Cookie: " . $_SERVER['HTTP_COOKIE']; +if (isset($_SERVER['HTTP_COOKIE'])) { + $hdrs[] = 'Cookie: '.$_SERVER['HTTP_COOKIE']; } -if(isset($_SERVER['HTTP_USER_AGENT'])) -{ - $hdrs[]="User-Agent: " . $_SERVER['HTTP_USER_AGENT']; +if (isset($_SERVER['HTTP_USER_AGENT'])) { + $hdrs[] = 'User-Agent: '.$_SERVER['HTTP_USER_AGENT']; } curl_setopt($ch, CURLOPT_HTTPHEADER, $hdrs); /* pass POST params */ -if( sizeof($_POST) > 0 ) -{ - curl_setopt($ch, CURLOPT_POSTFIELDS, $_POST); +if (sizeof($_POST) > 0) { + curl_setopt($ch, CURLOPT_POSTFIELDS, $_POST); } $res = curl_exec($ch); @@ -73,41 +66,33 @@ list($headers, $body) = explode("\r\n\r\n", $res, 2); $headers = explode("\r\n", $headers); -$hs = array(); +$hs = []; -foreach($headers as $header) -{ - if( false !== strpos($header, ':') ) - { +foreach ($headers as $header) { + if (false !== strpos($header, ':')) { list($h, $v) = explode(':', $header); $hs[$h][] = $v; - } - else - { - $header1 = $header; + } else { + $header1 = $header; } } /* set headers */ list($proto, $code, $text) = explode(' ', $header1); -header($_SERVER['SERVER_PROTOCOL'] . ' ' . $code . ' ' . $text); - -foreach($proxied_headers as $hname) -{ - if( isset($hs[$hname]) ) - { - foreach( $hs[$hname] as $v ) - { - if( $hname === 'Set-Cookie' ) - { - header($hname.": " . $v, false); - } - else - { - header($hname.": " . $v); +header($_SERVER['SERVER_PROTOCOL'].' '.$code.' '.$text); + +foreach ($proxied_headers as $hname) { + if (isset($hs[$hname])) { + foreach ($hs[$hname] as $v) { + if ($hname === 'Set-Cookie') { + header($hname.': '.$v, false); + } else { + header($hname.': '.$v); } } } } +$body = str_replace('"/IRNPortal', '"IRNPortal', $body); + die($body); diff --git a/src/Controllers/SIS/remote.php b/src/Controllers/SIS/remote.php old mode 100755 new mode 100644 index b5ac241f0..42689cbcb --- a/src/Controllers/SIS/remote.php +++ b/src/Controllers/SIS/remote.php @@ -1,32 +1,38 @@ - * @version 20131101 - * @package MyRadio_SIS + * Comet Server Handler for SIS. */ +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\SIS\SIS_Utils; +use \MyRadio\Config; //Allow Session writing from other requests $session = $_SESSION; session_write_close(); -$pollFuncs = SIS_Utils::readPolls(array_merge(SIS_Utils::getPlugins(), SIS_Utils::getTabs())); +// Session has ended up without a timeslot +// User has probably logged out (& in) on another tab, so ask them to pick another one (handled by JS) +if (!isset($session['timeslotid'])) { + header('HTTP/1.1 400 Bad Request'); + exit; +} + +$pollFuncs = SIS_Utils::readPolls(Config::$sis_modules); //Enter an infinite loop calling these functions, and enjoy the ride //Times out after 50 cycles to prevent infinites or something like that $count = 0; -$data = array(); +$data = []; do { - foreach ($pollFuncs as $function) { - $temp = call_user_func($function, $session); - if (!empty($temp)) { - $data = array_merge($data, $temp); - } - } - sleep(1); - $count++; + foreach ($pollFuncs as $function) { + $temp = call_user_func($function, $session); + if (!empty($temp)) { + $data = array_merge($data, $temp); + } + } + sleep(1); + ++$count; } while (empty($data) && $count < 50); //Return the response data -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file +URLUtils::dataToJSON($data); diff --git a/src/Controllers/SIS/schedule.get.php b/src/Controllers/SIS/schedule.get.php old mode 100755 new mode 100644 index 6250a5497..38800bb8c --- a/src/Controllers/SIS/schedule.get.php +++ b/src/Controllers/SIS/schedule.get.php @@ -1,12 +1,10 @@ - * @version 20131116 - * @package MyRadio_SIS + * Schedule Getter for SIS. */ +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Timeslot; $data = MyRadio_Timeslot::getCurrentAndNext(null, 10); -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file +URLUtils::dataToJSON($data); diff --git a/src/Controllers/SIS/selector.set.php b/src/Controllers/SIS/selector.set.php old mode 100755 new mode 100644 index 4cc72c455..36fbb7a0b --- a/src/Controllers/SIS/selector.set.php +++ b/src/Controllers/SIS/selector.set.php @@ -1,43 +1,34 @@ - * @version 20131117 - * @package MyRadio_SIS - * @todo Lots of duplication with MyRadio_Selector here + * Selector setter for SIS. + * + * @todo Lots of duplication with MyRadio_Selector here */ +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Selector; $src = (isset($_REQUEST['src'])) ? (int) $_REQUEST['src'] : 0; -$status = MyRadio_Selector::getStatusAtTime(time()); +$status = MyRadio_Selector::getStatusAtTime(); if (($src <= 0) || ($src > 8)) { - $data = ['error' => 'Invalid Selection']; - require 'Views/MyRadio/datatojson.php'; -} -elseif ($src == $status['studio']) { - $data = ['error' => 'Source '.$src.' already selected']; - require 'Views/MyRadio/datatojson.php'; -} -elseif ((($src == 1) && (!$status['s1power'])) || - (($src == 2) && (!$status['s2power'])) || - (($src == 4) && (!$status['s4power']))) { - $data = ['error' => 'Source '.$src.' not powered']; - require 'Views/MyRadio/datatojson.php'; -} -elseif ($status['lock'] != 0) { - $data = ['error' => 'locked']; - require 'Views/MyRadio/datatojson.php'; + $data = ['error' => 'Invalid selection.']; +} elseif ($src == $status['studio']) { + $data = ['error' => 'Source '.$src.' is already selected.']; +} elseif ((($src == 1) && (!$status['s1power'])) + || (($src == 2) && (!$status['s2power'])) + || (($src == 4) && (!$status['s4power'])) +) { + $data = ['error' => 'Source '.$src.' is not powered.']; +} elseif ($status['lock'] != 0) { + $data = ['error' => 'locked']; +} else { + $response = MyRadio_Selector::setStudio($src); + + if (!empty($response)) { + $data = $response; + } else { + $data = MyRadio_Selector::getStatusAtTime(); + } } -else { - $response = MyRadio_Selector::setStudio($src); - if (!empty($response)) { - $data = $response; - require 'Views/MyRadio/datatojson.php'; - } - else { - $data = MyRadio_Selector::getStatusAtTime(time()); - require 'Views/MyRadio/datatojson.php'; - } -} \ No newline at end of file +URLUtils::dataToJSON($data); diff --git a/src/Controllers/SIS/stats.graph.php b/src/Controllers/SIS/stats.graph.php old mode 100755 new mode 100644 index 1d8c2f89f..aceaeaca9 --- a/src/Controllers/SIS/stats.graph.php +++ b/src/Controllers/SIS/stats.graph.php @@ -1,21 +1,17 @@ - * @version 20131101 - * @package MyRadio_SIS + * Listener Stats Graph for SIS. */ - error_reporting(0); $fname = (isset($_REQUEST['f'])) ? $_REQUEST['f'] : 'https://ury.org.uk/sis2/streamstats-ury.txt'; $f = file($fname); $max = 0; foreach ($f as &$l) { - $l = explode(' ', $l); - if ((int) $l[1] > $max) - $max = (int) $l[1]; + $l = explode(' ', $l); + if ((int) $l[1] > $max) { + $max = (int) $l[1]; + } } $barwidth = 3.4; @@ -43,35 +39,47 @@ imageline($image, $left, $padding, $left, $imageheight - 2 * $padding, $coledge); imagestring($image, 4, 5, $padding - 5, $max, $coltext); imagestring($image, 4, 5, $imageheight - 2 * $padding - 5, '0', $coltext); -imagestring($image, 2, $left, $padding + $maxheight + 2, date("H:i", (int) $f[0][0]), $coltext); -imagestring($image, 2, $imagewidth - 2 * $padding - 30, $padding + $maxheight + 2, date("H:i", (int) $f[count($f) - 1][0]), $coltext); +imagestring($image, 2, $left, $padding + $maxheight + 2, date('H:i', (int) $f[0][0]), $coltext); +imagestring( + $image, + 2, + $imagewidth - 2 * $padding - 30, + $padding + $maxheight + 2, + date('H:i', (int) $f[count($f) - 1][0]), + $coltext +); -$points = array(); +$points = []; $points[] = $left; $points[] = $maxheight + $padding; -$maxleft = NULL; -$maxdate = NULL; +$maxleft = null; +$maxdate = null; foreach ($f as $p) { - $points[] = $left; - $points[] = (($maxheight * ($max - (int) $p[1])) / $max) + $padding; - if ((int) $p[1] == $max) { - $maxleft = $left; - $maxdate = $p[0]; - } - $left += $barwidth; + $points[] = $left; + $points[] = (($maxheight * ($max - (int) $p[1])) / $max) + $padding; + if ((int) $p[1] == $max) { + $maxleft = $left; + $maxdate = $p[0]; + } + $left += $barwidth; } $points[] = $imagewidth - 2 * $padding; $points[] = $maxheight + $padding; imagefilledpolygon($image, $points, count($points) / 2, $colfill); imagepolygon($image, $points, count($points) / 2, $coledge); -imagestring($image, 2, $maxleft - 14, $maxheight - 5, date("H:i", (int) $maxdate) . " ($max)", $coltext); +imagestring($image, 2, $maxleft - 14, $maxheight - 5, date('H:i', (int) $maxdate)." ($max)", $coltext); imageline($image, $maxleft, $padding, $maxleft, $maxheight + $padding, $coledge); -imagestring($image, 2, $imagewidth - 2 * $padding + 4, (($maxheight * ($max - (int) $f[count($f) - 1][1])) / $max) - 5 + $padding, $f[count($f) - 1][1], $coltext); +imagestring( + $image, + 2, + $imagewidth - 2 * $padding + 4, + (($maxheight * ($max - (int) $f[count($f) - 1][1])) / $max) - 5 + $padding, + $f[count($f) - 1][1], + $coltext +); header('Content-type: image/png'); imagepng($image); imagedestroy($image); - - diff --git a/src/Controllers/SIS/tracklist.checkTrack.php b/src/Controllers/SIS/tracklist.checkTrack.php old mode 100755 new mode 100644 index 0c5e5eb61..59b464ee6 --- a/src/Controllers/SIS/tracklist.checkTrack.php +++ b/src/Controllers/SIS/tracklist.checkTrack.php @@ -1,43 +1,30 @@ - * @version 20131101 - * @package MyRadio_SIS + * Tracklist Track Inserter for SIS. */ +use \MyRadio\SIS\SIS_Tracklist; +use \MyRadio\ServiceAPI\MyRadio_Track; +use \MyRadio\MyRadioException; -$artist = $_GET['artist']; -$album = $_GET['album']; -$tname = $_GET['tname']; -$where = $_GET['where']; +$artist = $_REQUEST['artist']; +$album = $_REQUEST['album']; +$tname = $_REQUEST['title']; +$trackid = $_REQUEST['trackid']; $timeslotid = $_SESSION['timeslotid']; -if ($where == "notrec"){ - SIS_Tracklist::insertTrackNoRec($tname, $artist, $album, time(), "m", $timeslotid); - header('HTTP/1.1 204 No Content'); -} - -else if($where == 'rec'){ - $result = SIS_Tracklist::checkTrackOK($artist, $album, $tname); - $numrow = sizeof($result); - $row = $result[0]; - $return = 0; - if ($numrow != 1){ - if($numrow == 0){ - $return = 1; - } - elseif($numrow >= 2){ - $return = 2; - } - } - elseif ($numrow == 1){ - SIS_Tracklist::insertTrackRec($row['trackid'], $row['recordid'], time(), "m", $timeslotid); - $return = 0; - } - - $data = array("return"=>$return, "result"=>$row); - //Return the response data - require 'Views/MyRadio/datatojson.php'; +if (empty($trackid)) { + if (empty($artist)) { + throw new MyRadioException('Artist is required', 400); + } + if (empty($album)) { + throw new MyRadioException('Album is required', 400); + } + if (empty($tname)) { + throw new MyRadioException('Title is required', 400); + } + SIS_Tracklist::insertTrackNoRec($tname, $artist, $album, 'm', $timeslotid); +} else { + $track = MyRadio_Track::getInstance($trackid); + SIS_Tracklist::insertTrackRec($track, 'm', $timeslotid); } diff --git a/src/Controllers/SIS/tracklist.delTrack.php b/src/Controllers/SIS/tracklist.delTrack.php old mode 100755 new mode 100644 index aa98a240d..d72e3b0d2 --- a/src/Controllers/SIS/tracklist.delTrack.php +++ b/src/Controllers/SIS/tracklist.delTrack.php @@ -1,11 +1,8 @@ - * @version 20131101 - * @package MyRadio_SIS + * Tracklist Track Deleter for SIS. */ +use \MyRadio\SIS\SIS_Tracklist; -SIS_Tracklist::markTrackDeleted($_GET['id']); -header('HTTP/1.1 204 No Content'); \ No newline at end of file +SIS_Tracklist::markTrackDeleted($_REQUEST['id']); +header('HTTP/1.1 204 No Content'); diff --git a/src/Controllers/SIS/tracklist.findTrack.php b/src/Controllers/SIS/tracklist.findTrack.php old mode 100755 new mode 100644 index 948d66d84..c9de3dc60 --- a/src/Controllers/SIS/tracklist.findTrack.php +++ b/src/Controllers/SIS/tracklist.findTrack.php @@ -1,53 +1,57 @@ - * @version 20131101 - * @package MyRadio_SIS + * Tracklist Track Finder for SIS. */ +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Artist; +use \MyRadio\ServiceAPI\MyRadio_Track; +use \MyRadio\ServiceAPI\MyRadio_Album; $artist = $_GET['artist']; $album = $_GET['album']; $tname = $_GET['tname']; $box = $_GET['box']; -$artistResult = Artist::findByOptions( - ['title' => $tname, - 'artist' => $artist, - 'album' => $album, - 'digitised' => false] - ); +$artistResult = MyRadio_Artist::findByOptions( + [ + 'title' => $tname, + 'artist' => $artist, + 'album' => $album, + 'digitised' => false, + ] +); $trackResult = MyRadio_Track::findByOptions( - ['title' => $tname, - 'artist' => $artist, - 'album' => $album, - 'digitised' => false] - ); + [ + 'title' => $tname, + 'artist' => $artist, + 'album' => $album, + 'digitised' => false, + ] +); $albumResult = MyRadio_Album::findByOptions( - ['title' => $tname, - 'artist' => $artist, - 'album' => $album, - 'digitised' => false] - ); + [ + 'title' => $tname, + 'artist' => $artist, + 'album' => $album, + 'digitised' => false, + ] +); -$dataout = array(); +$dataout = []; -if ($box == "artist"){ - foreach ($artistResult as $artist) { - $dataout[] = "{$artist['artist']}"; - } -} -else if ($box == "album"){ - foreach ($albumResult as $record) { - $dataout[] = "{$record->getTitle()}"; - } -} -else if ($box == "tname"){ - foreach ($trackResult as $track) { - $dataout[] = "{$track->getTitle()}"; - } +if ($box == 'artist') { + foreach ($artistResult as $artist) { + $dataout[] = "{$artist['artist']}"; + } +} elseif ($box == 'album') { + foreach ($albumResult as $record) { + $dataout[] = "{$record->getTitle()}"; + } +} elseif ($box == 'tname') { + foreach ($trackResult as $track) { + $dataout[] = "{$track->getTitle()}"; + } } $data = $dataout; -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file +URLUtils::dataToJSON($data); diff --git a/src/Controllers/SIS/webcam.set.php b/src/Controllers/SIS/webcam.set.php old mode 100755 new mode 100644 index 4346482fb..958864404 --- a/src/Controllers/SIS/webcam.set.php +++ b/src/Controllers/SIS/webcam.set.php @@ -1,15 +1,13 @@ - * @version 20131101 - * @package MyRadio_SIS + * Webcam Setter for SIS. */ +use \MyRadio\ServiceAPI\MyRadio_Webcam; -if (!isset($_REQUEST['src'])) - return; +if (!isset($_REQUEST['src'])) { + return; +} MyRadio_Webcam::setWebcam($_REQUEST['src']); -header('HTTP/1.1 204 No Content'); \ No newline at end of file +header('HTTP/1.1 204 No Content'); diff --git a/src/Controllers/Scheduler/a-findshowbytitle.php b/src/Controllers/Scheduler/a-findshowbytitle.php index ad2e7d8de..9cb2c983f 100644 --- a/src/Controllers/Scheduler/a-findshowbytitle.php +++ b/src/Controllers/Scheduler/a-findshowbytitle.php @@ -1,14 +1,21 @@ - * @version 16082012 - * @package MyRadio_Scheduler + * Some might include more than one model etc.... + * + * @todo Proper documentation */ -if (!isset($_REQUEST['term'])) throw new MyRadioException('Parameter \'term\' is required but was not provided'); +use \MyRadio\Config; +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Scheduler; -$data = MyRadio_Scheduler::findShowByTitle($_REQUEST['term'], isset($_REQUEST['limit']) ? intval($_REQUEST['limit']) : Config::$ajax_limit_default);; -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file +if (!isset($_REQUEST['term'])) { + throw new MyRadioException('Parameter \'term\' is required but was not provided'); +} + +$data = MyRadio_Scheduler::findShowByTitle( + $_REQUEST['term'], + isset($_REQUEST['limit']) ? intval($_REQUEST['limit']) : Config::$ajax_limit_default +); +URLUtils::dataToJSON($data); diff --git a/src/Controllers/Scheduler/addEpisode.php b/src/Controllers/Scheduler/addEpisode.php new file mode 100644 index 000000000..44142b4de --- /dev/null +++ b/src/Controllers/Scheduler/addEpisode.php @@ -0,0 +1,46 @@ +getAddEpisodeForm()->readValues(); + + if ($data['new_start_time'] === $data['new_end_time']) { + $message = 'You can\'t have an episode start and end at the same time.'; + URLUtils::backWithMessage($message); + } else { + // Move + $result = $season->addEpisode( + $data['new_start_time'], + $data['new_end_time'] + ); + + if ($result) { + $message = 'New episode created.'; + } else { + $message = 'Something didn\'t work! Please ping Computing.'; + } + + URLUtils::backWithMessage($message); + } +} else { + //Not Submitted + + if (!isset($_REQUEST['show_season_id'])) { + throw new MyRadioException('No seasonid provided.', 400); + } + + $season = MyRadio_Season::getInstance($_REQUEST['show_season_id']); + + $season->getAddEpisodeForm()->render(); +} diff --git a/src/Controllers/Scheduler/allShows.php b/src/Controllers/Scheduler/allShows.php deleted file mode 100644 index b8f7e1355..000000000 --- a/src/Controllers/Scheduler/allShows.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @version 21072012 - * @package MyRadio_Scheduler - */ - -$shows = MyRadio_Show::getAllShows(); -$twig = CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('title', 'All Shows') - ->addVariable('tabledata', ServiceAPI::setToDataSource($shows)) - ->addVariable('tablescript', 'myury.scheduler.showlist'); - -$twig->render(); \ No newline at end of file diff --git a/src/Controllers/Scheduler/allocate.php b/src/Controllers/Scheduler/allocate.php index 1157c9d3f..cda7900ec 100644 --- a/src/Controllers/Scheduler/allocate.php +++ b/src/Controllers/Scheduler/allocate.php @@ -1,19 +1,32 @@ - * @version 22092012 - * @package MyRadio_Scheduler */ +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Season; +use \MyRadio\ServiceAPI\MyRadio_Scheduler; +use \MyRadio\ServiceAPI\MyRadio_Term; +use \MyRadio\MyRadioException; -//Model: The Season to be allocated -$season = MyRadio_Season::getInstance((int)$_REQUEST['show_season_id']); -/** - * @todo WHY IS THIS IN THE SESSION - */ -$_SESSION['myury_working_with_season'] = $season->getID(); -//Model: The Form definition -require 'Models/Scheduler/allocatefrm.php'; -//View: The Form output with $season meta -$form->render($season->toDataSource()); \ No newline at end of file +$current_term_info = MyRadio_Term::getActiveApplicationTerm(); +$term_weeks = $current_term_info->getTermWeeks(); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + + // Note Slightly ugly hack to get the season ID from the submitted form + $season = MyRadio_Season::getInstance($_POST['sched_allocate-season_id']); + $data = $season->getAllocateForm()->readValues(); + $season->schedule($data, $term_weeks); + + URLUtils::redirectWithMessage('Scheduler', 'default', 'Season Allocated!'); +} else { + //Not Submitted + if (empty(MyRadio_Term::getActiveApplicationTerm())) { + throw new MyRadioException('There is not currently a term you can apply/schedule for.', 400); + } else { + $season = MyRadio_Season::getInstance($_REQUEST['show_season_id']); + $season->getAllocateForm() + ->render($season->toDataSource()); + } +} diff --git a/src/Controllers/Scheduler/attendDemo.php b/src/Controllers/Scheduler/attendDemo.php deleted file mode 100644 index f47e14643..000000000 --- a/src/Controllers/Scheduler/attendDemo.php +++ /dev/null @@ -1,11 +0,0 @@ - - * @version 24102012 - * @package MyRadio_Scheduler - */ - -$result = MyRadio_Demo::attend($_REQUEST['demoid']); -header('Location: '.CoreUtils::makeURL($module, 'listDemos', array('msg'=>$result))); \ No newline at end of file diff --git a/src/Controllers/Scheduler/attendance.php b/src/Controllers/Scheduler/attendance.php index 258b184c1..01cd1191d 100644 --- a/src/Controllers/Scheduler/attendance.php +++ b/src/Controllers/Scheduler/attendance.php @@ -2,25 +2,23 @@ /** * Shows statistics about members actually turning up for their shows. - * - * @author Lloyd Wallis - * @version 20130829 - * @package MyRadio_Scheduler */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_Season; + $data = []; foreach (MyRadio_Season::getAllSeasonsInLatestTerm() as $season) { - $info = $season->getAttendanceInfo(); - $data[] = [ - 'title' => $season->getMeta('title'), - 'percent' => (int)$info[0], - 'missed' => (int)$info[1] - ]; + $info = $season->getAttendanceInfo(); + $data[] = [ + 'title' => $season->getMeta('title'), + 'percent' => (int) $info[0], + 'missed' => (int) $info[1], + ]; } -$twig = CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('title', 'Show Attendence') - ->addVariable('tabledata', $data) - ->addVariable('tablescript', 'myury.datatable.default'); - -$twig->render(); \ No newline at end of file +CoreUtils::getTemplateObject()->setTemplate('table.twig') + ->addVariable('title', 'Show Attendence') + ->addVariable('tabledata', $data) + ->addVariable('tablescript', 'myradio.scheduler.attendance') + ->render(); diff --git a/src/Controllers/Scheduler/autoViz.php b/src/Controllers/Scheduler/autoViz.php new file mode 100644 index 000000000..194049d8f --- /dev/null +++ b/src/Controllers/Scheduler/autoViz.php @@ -0,0 +1,48 @@ +getID()); + $rows[] = [ + 'title' => $timeslot->getMeta('title'), + 'start_time' => CoreUtils::happyTime($timeslot->getStartTime()), + 'togglelink' => ($timeslot->getStartTime() < time()) ? [ + 'display' => 'icon', + 'value' => 'times', + 'title' => 'Show is in the past!', + 'url' => '#', + ] : (($timeslot->getAutoViz()) ? [ + 'display' => 'text', + 'value' => 'Enabled - Click here to disable', + 'title' => 'Disable Automatic Visualisation', + 'url' => URLUtils::makeURL('Scheduler', 'setAutoViz', ['timeslotid' => $timeslot->getID(), 'value' => 'false']), + ] : [ + 'display' => 'text', + 'value' => 'Disabled - Click here to enable', + 'title' => 'Enable Automatic Visualisation', + 'url' => URLUtils::makeURL('Scheduler', 'setAutoViz', ['timeslotid' => $timeslot->getID(), 'value' => 'true']), + ]), + 'clipslink' => empty($clips) ? 'No clips available' : [ + 'display' => 'text', + 'value' => 'Clips', + 'title' => 'Access all the clips from this show', + 'url' => URLUtils::makeURL('Scheduler', 'autoVizClips', ['timeslotid' => $timeslot->getID()]) + ] + ]; +} + +CoreUtils::getTemplateObject()->setTemplate('table.twig') + ->addVariable('tablescript', 'myradio.scheduler.autoViz') + ->addVariable('title', 'Automatically Visualised Shows') + ->addVariable('tabledata', $rows) + ->render(); diff --git a/src/Controllers/Scheduler/autoVizClips.php b/src/Controllers/Scheduler/autoVizClips.php new file mode 100644 index 000000000..4c49ba08f --- /dev/null +++ b/src/Controllers/Scheduler/autoVizClips.php @@ -0,0 +1,38 @@ +getSeason()->isCurrentUserAnOwner()) { + AuthUtils::requirePermission(AUTH_EDITSHOWS); +} + + +$clips = MyRadio_AutoVizClip::getClipsForTimeslot($timeslot->getID()); + +$rows = []; +foreach ($clips as $clip) { + $rows[] = [ + 'type' => $clip->getType() === 'full_show' ? 'Full Show' : 'Clip', + 'start_time' => CoreUtils::happyTime($clip->getStartTime()), + 'end_time' => CoreUtils::happyTime($clip->getEndTime()), + 'downloadlink' => [ + 'display' => 'text', + 'value' => 'Download', + 'title' => 'Download this clip', + 'url' => $clip->getPublicURL(), + ] + ]; +} + +CoreUtils::getTemplateObject()->setTemplate('table.twig') + ->addVariable('tablescript', 'myradio.scheduler.autoVizClips') + ->addVariable('title', 'Clips for ' . $timeslot->getMeta('title') . ' ' . CoreUtils::happyTime($timeslot->getStartTime())) + ->addVariable('tabledata', $rows) + ->render(); diff --git a/src/Controllers/Scheduler/cancelEpisode.php b/src/Controllers/Scheduler/cancelEpisode.php index 89d6b56d9..6608d20dc 100644 --- a/src/Controllers/Scheduler/cancelEpisode.php +++ b/src/Controllers/Scheduler/cancelEpisode.php @@ -1,16 +1,34 @@ - * @version 20131016 - * @package MyRadio_Scheduler + * Presents a form to the user to enable them to cancel an Episode. */ +use \MyRadio\Config; +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Timeslot; -if (!isset($_REQUEST['show_season_timeslot_id'])) { - throw new MyRadioException('No timeslotid provided.', 400); +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + //Get data + $data = MyRadio_Timeslot::getCancelForm()->readValues(); + //Cancel + $timeslot = MyRadio_Timeslot::getInstance($data['show_season_timeslot_id']); + $result = $timeslot->cancelTimeslot($data['reason']); + + if (!$result) { + $message = 'This episode is too close to its scheduled time to be automatically cancelled, ' + .'please contact programming@'.Config::$email_domain.' instead.'; + } else { + $message = 'Your cancellation request has been sent. You will receive an email informing you of updates.'; + } + + URLUtils::backWithMessage($message); +} else { + //Not Submitted + + if (!isset($_REQUEST['show_season_timeslot_id'])) { + throw new MyRadioException('No timeslotid provided.', 400); + } + + MyRadio_Timeslot::getCancelForm()->render(); } -//The Form definition -require 'Models/Scheduler/reasonfrm.php'; -//'tis a one line view -$form->render(); \ No newline at end of file diff --git a/src/Controllers/Scheduler/createDemo.php b/src/Controllers/Scheduler/createDemo.php deleted file mode 100644 index 0ecf14c85..000000000 --- a/src/Controllers/Scheduler/createDemo.php +++ /dev/null @@ -1,13 +0,0 @@ - - * @version 24102012 - * @package MyRadio_Scheduler - */ - -//The Form definition -require 'Models/Scheduler/demofrm.php'; -//'tis a one line view -$form->setTemplate('Scheduler/createDemo.twig')->render(); \ No newline at end of file diff --git a/src/Controllers/Scheduler/createSeason.php b/src/Controllers/Scheduler/createSeason.php deleted file mode 100644 index 3a8605815..000000000 --- a/src/Controllers/Scheduler/createSeason.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @version 23082012 - * @package MyRadio_Scheduler - */ - -//The Form definition -$current_term_info = MyRadio_Scheduler::getActiveApplicationTermInfo(); -$current_term = $current_term_info['descr']; -require 'Models/Scheduler/seasonfrm.php'; -$form->setFieldValue('show_id', (int)$_REQUEST['showid']) - ->setTemplate('Scheduler/createSeason.twig') - ->render(array('current_term' => $current_term)); \ No newline at end of file diff --git a/src/Controllers/Scheduler/createShow.php b/src/Controllers/Scheduler/createShow.php deleted file mode 100644 index f26a8ab2e..000000000 --- a/src/Controllers/Scheduler/createShow.php +++ /dev/null @@ -1,15 +0,0 @@ - - * @version 20130727 - * @package MyRadio_Scheduler - */ -//The Form definition -require 'Models/Scheduler/showfrm.php'; -$form->setFieldValue('credits.member', array(MyRadio_User::getInstance())) - ->setFieldValue('credits.credittype', array(1)) - ->setTemplate('Scheduler/createShow.twig') - ->render(); \ No newline at end of file diff --git a/src/Controllers/Scheduler/default.php b/src/Controllers/Scheduler/default.php index 1094ade68..a85872bd3 100644 --- a/src/Controllers/Scheduler/default.php +++ b/src/Controllers/Scheduler/default.php @@ -2,15 +2,15 @@ /** * The default page of the Scheduler module lists Season applications * that have not yet had timeslots assigned. - * - * @author Lloyd Wallis - * @version 20130923 - * @package MyRadio_Scheduler */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_Scheduler; CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('tablescript', 'myury.scheduler.pending') - ->addVariable('title', 'Scheduler') - ->addVariable('tabledata', CoreUtils::dataSourceParser( - MyRadio_Scheduler::getPendingAllocations(), false)) - ->render(); + ->addVariable('tablescript', 'myradio.scheduler.pending') + ->addVariable('title', 'Scheduler') + ->addVariable('subtitle', 'Pending Allocations') + ->addVariable( + 'tabledata', + CoreUtils::dataSourceParser(MyRadio_Scheduler::getPendingAllocations()) + )->render(); diff --git a/src/Controllers/Scheduler/doAllocate.php b/src/Controllers/Scheduler/doAllocate.php deleted file mode 100644 index 099dda8c1..000000000 --- a/src/Controllers/Scheduler/doAllocate.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @version 09102012 - * @package MyRadio_Scheduler - */ - -//The Form definition -$season = MyRadio_Season::getInstance($_SESSION['myury_working_with_season']); -require 'Models/Scheduler/allocatefrm.php'; -$season->schedule($form->readValues()); -require 'Controllers/Scheduler/default.php'; \ No newline at end of file diff --git a/src/Controllers/Scheduler/doCancelEpisode.php b/src/Controllers/Scheduler/doCancelEpisode.php deleted file mode 100644 index 04fb644ec..000000000 --- a/src/Controllers/Scheduler/doCancelEpisode.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @version 05012013 - * @package MyRadio_Scheduler - */ - -//The Form definition -require 'Models/Scheduler/reasonfrm.php'; -//Get data -$data = $form->readValues(); -//Cancel -$timeslot = MyRadio_Timeslot::getInstance($data['show_season_timeslot_id']); -$result = $timeslot->cancelTimeslot($data['reason']); - -if (!$result) { - $message = 'Your cancellation request could not be processed at this time. Please contact programming@ury.org.uk instead.'; -} else { - $message = 'Your cancellation request has been sent. You will receive an email informing you of updates.'; -} - -header('Location: '.CoreUtils::makeURL('Scheduler', 'listTimeslots', array( - 'show_season_id' => $timeslot->getSeason()->getID(), - 'message' => base64_encode($message)) - )); \ No newline at end of file diff --git a/src/Controllers/Scheduler/doDemo.php b/src/Controllers/Scheduler/doDemo.php deleted file mode 100644 index 7009c9fc2..000000000 --- a/src/Controllers/Scheduler/doDemo.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @version 21072012 - * @package MyRadio_Scheduler - */ - -//The Form definition -require 'Models/Scheduler/demofrm.php'; -/** - * @todo make this less horrific - */ -$demoinfo = $form->readValues(); -MyRadio_Demo::registerDemo($demoinfo['demo-datetime']); -header('Location: '.CoreUtils::makeURL('Scheduler','listDemos')); \ No newline at end of file diff --git a/src/Controllers/Scheduler/doEditSeason.php b/src/Controllers/Scheduler/doEditSeason.php deleted file mode 100644 index 04c1743fc..000000000 --- a/src/Controllers/Scheduler/doEditSeason.php +++ /dev/null @@ -1,34 +0,0 @@ - - * @version 20130923 - * @package MyRadio_Scheduler - */ - -//Get the Form data -require 'Models/Scheduler/showfrm.php'; -$data = $form->readValues(); - -//Check the user has permission to edit this show -$season = MyRadio_Season::getInstance($data['id']); -if (!$season->isCurrentUserAnOwner() && !CoreUtils::hasPermission(AUTH_EDITSHOWS)) { - $message = 'You must be a Creditor of the Show or be in the Programming Team to edit this season.'; - require 'Views/Errors/403.php'; -} - -$season->setMeta('title', $data['title']); -$season->setMeta('description', $data['description']); -$season->setMeta('tag', explode(' ', $data['tags'])); -$season->setCredits($data['credits']['member'], $data['credits']['credittype']); - -CoreUtils::redirect( - 'Scheduler', - 'listSeasons', - [ - 'showid' => $season->getShow()->getID(), - 'message' => base64_encode('Season updated') - ] -); diff --git a/src/Controllers/Scheduler/doEditShow.php b/src/Controllers/Scheduler/doEditShow.php deleted file mode 100644 index f10afa80b..000000000 --- a/src/Controllers/Scheduler/doEditShow.php +++ /dev/null @@ -1,44 +0,0 @@ - - * @version 20130810 - * @package MyRadio_Scheduler - */ - -//Get the Form data -require 'Models/Scheduler/showfrm.php'; -$data = $form->readValues(); - -//Check the user has permission to edit this show -$show = MyRadio_Show::getInstance($data['id']); -if (!$show->isCurrentUserAnOwner() && !CoreUtils::hasPermission(AUTH_EDITSHOWS)) { - $message = 'You must be a Creditor of a Show or be in the Programming Team to edit this show.'; - require 'Views/Errors/403.php'; -} - -$show->setMeta('title', $data['title']); -$show->setMeta('description', $data['description']); -// We want to handle the case when people delimit with commas, or commas and -// spaces, as well as handling extended spaces. -$show->setMeta( - 'tag', - preg_split('/[, ] */', $data['tags'], NULL, PREG_SPLIT_NO_EMPTY) -); -$show->setGenre($data['genres']); -$show->setCredits($data['credits']['member'], $data['credits']['credittype']); - -if ($data['mixclouder']) { - $show->setMeta('upload_state', 'Requested'); -} else { - $show->setMeta('upload_state', 'Opted Out'); -} - -CoreUtils::redirect( - 'Scheduler', - 'myShows', - [ - 'message' => base64_encode('Show updated') - ] -); diff --git a/src/Controllers/Scheduler/doReject.php b/src/Controllers/Scheduler/doReject.php deleted file mode 100644 index 4d2dbb3fc..000000000 --- a/src/Controllers/Scheduler/doReject.php +++ /dev/null @@ -1,15 +0,0 @@ - - * @version 20130728 - * @package MyRadio_Scheduler - */ - -//Model: The Form definition -require 'Models/Scheduler/rejectfrm.php'; -$data = $form->readValues(); - -MyRadio_Season::getInstance($data['season_id'])->reject($data['reason'], $data['notify_user']); - -header('Location: '.CoreUtils::makeURL('Scheduler', 'default')); \ No newline at end of file diff --git a/src/Controllers/Scheduler/doSeason.php b/src/Controllers/Scheduler/doSeason.php deleted file mode 100644 index 477f3c5d5..000000000 --- a/src/Controllers/Scheduler/doSeason.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @version 21072012 - * @package MyRadio_Scheduler - */ - -//The Form definition -require 'Models/Scheduler/seasonfrm.php'; - -try { - $values = $form->readValues(); - MyRadio_Season::apply($values); - header('Location: '.CoreUtils::makeURL('Scheduler', 'listSeasons', - array('msg' => 'seasonCreated', 'showid' => $values['show_id']))); -} catch (MyRadioException $e) { - require 'Views/Errors/500.php'; -} \ No newline at end of file diff --git a/src/Controllers/Scheduler/doShow.php b/src/Controllers/Scheduler/doShow.php deleted file mode 100644 index 3c71620ac..000000000 --- a/src/Controllers/Scheduler/doShow.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @version 20130728 - * @package MyRadio_Scheduler - */ - -//The Form definition -require 'Models/Scheduler/showfrm.php'; - -try { - MyRadio_Show::create($form->readValues()); -} catch (MyRadioException $e) { - require 'Views/Errors/500.php'; - exit; -} - -header('Location: '.CoreUtils::makeURL('Scheduler', 'myShows')); \ No newline at end of file diff --git a/src/Controllers/Scheduler/doShowPhoto.php b/src/Controllers/Scheduler/doShowPhoto.php deleted file mode 100644 index 9fca452c5..000000000 --- a/src/Controllers/Scheduler/doShowPhoto.php +++ /dev/null @@ -1,23 +0,0 @@ - - * @version 20130529 - * @package MyRadio_Scheduler - */ - -//The Form definition -require 'Models/Scheduler/showphotofrm.php'; - -$data = $form->readValues(); - -$show = MyRadio_Show::getInstance($data['show_id']); -//Require this is the user's show or the user can edit any show -if (!$show->isCurrentUserAnOwner()) { - CoreUtils::requirePermission(AUTH_EDITSHOWS); -} - -$show->setShowPhoto($data['image_file']['tmp_name']); - -CoreUtils::backWithMessage('Show Photo updated!'); \ No newline at end of file diff --git a/src/Controllers/Scheduler/editSeason.php b/src/Controllers/Scheduler/editSeason.php index 148c7bb91..3f5090690 100644 --- a/src/Controllers/Scheduler/editSeason.php +++ b/src/Controllers/Scheduler/editSeason.php @@ -1,36 +1,80 @@ - * @version 20130923 - * @package MyRadio_Scheduler */ +use \MyRadio\MyRadio\AuthUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Scheduler; +use \MyRadio\ServiceAPI\MyRadio_Season; +use \MyRadio\ServiceAPI\MyRadio_Show; +use \MyRadio\ServiceAPI\MyRadio_Term; -//Check the user has permission to edit this show -$season = MyRadio_Season::getInstance($_REQUEST['seasonid']); -if (!$season->getShow()->isCurrentUserAnOwner() && !CoreUtils::hasPermission(AUTH_EDITSHOWS)) { - $message = 'You must be a Creditor of the Show or be in the Programming Team to edit this season.'; - require 'Views/Errors/403.php'; -} +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = MyRadio_Season::getForm()->readValues(); + + if (empty($data['id'])) { + //create new + MyRadio_Season::create($data); + URLUtils::redirectWithMessage( + 'Scheduler', + 'myShows', + "Your new season has been created. You should recieve an email when it's scheduled!" + ); + } else { + //submit edit + $season = MyRadio_Season::getInstance($data['id']); + + //Check the user has permission to edit this show + if (!$season->getShow()->isCurrentUserAnOwner()) { + AuthUtils::requirePermission(AUTH_EDITSHOWS); + } + + $season->setMeta('title', $data['title']); + $season->setMeta('description', $data['description']); + $season->setMeta('tag', explode(' ', $data['tags'])); + $season->setCredits($data['credits']['member'], $data['credits']['credittype']); + if (!empty($data['subtype'])) { + $season->setSubtypeByName($data['subtype']); + } else { + $season->clearSubtype(); + } + + URLUtils::redirectWithMessage( + 'Scheduler', + 'listSeasons', + 'Season updated!', + ['showid' => $season->getShow()->getID()] + ); + } +} else { + //Not Submitted + $current_term_info = MyRadio_Term::getActiveApplicationTerm(); + $current_term = $current_term_info->getTermDescr(); -//The Form definition -require 'Models/Scheduler/showfrm.php'; - -$form->editMode($_REQUEST['seasonid'], array( - 'title' => $season->getMeta('title'), - 'description' => $season->getMeta('description'), - 'tags' => implode(' ', $season->getMeta('tag')), - 'credits.member' => array_map(function ($ar) { - return $ar['User']; - }, $season->getCredits()), - 'credits.credittype' => array_map(function ($ar) { - return $ar['type']; - }, $season->getCredits()) - ), - 'doEditSeason' - ) - ->setTitle('Edit Season of '.$season->getShow()->getMeta('title')) - ->render(); \ No newline at end of file + if (isset($_REQUEST['seasonid'])) { + //edit form + $season = MyRadio_Season::getInstance($_REQUEST['seasonid']); + + //Check the user has permission to edit this show + if (!$season->getShow()->isCurrentUserAnOwner()) { + AuthUtils::requirePermission(AUTH_EDITSHOWS); + } + + $season->getEditForm()->render(); + } else { + //create form + + MyRadio_Season::getForm() + ->setFieldValue('show_id', (int) $_REQUEST['showid']) + ->setTemplate('Scheduler/createSeason.twig') + ->render( + [ + 'current_term' => $current_term, + 'show_title' => MyRadio_Show::getInstance($_REQUEST['showid'])->getMeta('title'), + ] + ); + } +} diff --git a/src/Controllers/Scheduler/editShow.php b/src/Controllers/Scheduler/editShow.php index c2c32789c..229214c49 100644 --- a/src/Controllers/Scheduler/editShow.php +++ b/src/Controllers/Scheduler/editShow.php @@ -1,40 +1,86 @@ - * @version 20130728 - * @package MyRadio_Scheduler */ +use \MyRadio\MyRadio\AuthUtils; +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Show; +use \MyRadio\ServiceAPI\MyRadio_User; -//Check the user has permission to edit this show -$show = MyRadio_Show::getInstance($_REQUEST['showid']); -if (!$show->isCurrentUserAnOwner() && !CoreUtils::hasPermission(AUTH_EDITSHOWS)) { - $message = 'You must be a Creditor of a Show or be in the Programming Team to edit this show.'; - require 'Views/Errors/403.php'; -} - -//The Form definition -require 'Models/Scheduler/showfrm.php'; - -$meta = $show->getMeta('tag'); -if ($meta === null) { - $meta = array(); -} -$form->editMode($_REQUEST['showid'], array( - 'title' => $show->getMeta('title'), - 'description' => $show->getMeta('description'), - 'genres' => $show->getGenre(), - 'tags' => implode(' ', $meta), - 'credits.member' => array_map(function ($ar) { - return $ar['User']; - }, $show->getCredits()), - 'credits.credittype' => array_map(function ($ar) { - return $ar['type']; - }, $show->getCredits()), - 'mixclouder' => ($show->getMeta('upload_state') === 'Requested') - ), - 'doEditShow' - ) - ->render(); \ No newline at end of file +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = MyRadio_Show::getForm()->readValues(); + + if (empty($data['id'])) { + //create new + $show = MyRadio_Show::create($data); + URLUtils::redirectWithMessage( + 'Scheduler', + 'myShows', + 'Your show, ' . $show->getMeta('title') . ', has been created. Now create a new season for it!' + ); + } else { + //submit edit + /** @var MyRadio_Show $show */ + $show = MyRadio_Show::getInstance($data['id']); + + //Check the user has permission to edit this show + if (!$show->isCurrentUserAnOwner()) { + AuthUtils::requirePermission(AUTH_EDITSHOWS); + } + + $show->setMeta('title', $data['title']); + $show->setMeta('description', $data['description']); + + $show->setMeta( + 'tag', + CoreUtils::explodeTags($data['tags']) + ); + + $show->setGenre($data['genres']); + $show->setCredits($data['credits']['memberid'], $data['credits']['credittype']); + + if ($data['mixclouder']) { + $show->setMeta('upload_state', 'Requested'); + } else { + $show->setMeta('upload_state', 'Opted Out'); + } + + if ($data['podcast_explicit']) { + $show->setPodcastExplicit(true); + } else { + $show->setPodcastExplicit(false); + } + + $show->setSubtypeByName($data['subtype']); + + URLUtils::redirectWithMessage('Scheduler', 'myShows', 'Show Updated!'); + } +} else { + //Not Submitted + if (isset($_REQUEST['showid'])) { + //edit form + $show = MyRadio_Show::getInstance($_REQUEST['showid']); + + //Check the user has permission to edit this show + if (!$show->isCurrentUserAnOwner()) { + AuthUtils::requirePermission(AUTH_EDITSHOWS); + } + + $meta = $show->getMeta('tag'); + if ($meta === null) { + $meta = []; + } + $show->getEditForm()->render(); + } else { + //create form + MyRadio_Show::getForm() + ->setFieldValue('credits.memberid', [MyRadio_User::getInstance()]) + ->setFieldValue('credits.credittype', [1]) + ->setTemplate('Scheduler/createShow.twig') + ->render(); + } +} \ No newline at end of file diff --git a/src/Controllers/Scheduler/editTerm.php b/src/Controllers/Scheduler/editTerm.php new file mode 100644 index 000000000..14fc37a51 --- /dev/null +++ b/src/Controllers/Scheduler/editTerm.php @@ -0,0 +1,41 @@ +readValues(); + + if (empty($data['id'])) { + //create new + $term = MyRadio_Term::addTerm($data['start'], $data['descr'], $data['numweeks']); + if (is_numeric($term)) { + URLUtils::redirectWithMessage('Scheduler', 'listTerms', 'Term '.$data['descr'].', has been added.'); + } else { + throw new MyRadioException('Error creating term.', 500); + } + } else { + /* + * @todo + */ + throw new MyRadioException('Not Implemented'); + //submit edit + + URLUtils::backWithMessage('Show Updated!'); + } +} else { + //Not Submitted + if (isset($_REQUEST['termid'])) { + MyRadio_Term::getTermEditForm($_REQUEST['termid'])->render(); + } else { + //create form + MyRadio_Term::getTermForm() + ->render(); + } +} diff --git a/src/Controllers/Scheduler/listDemos.php b/src/Controllers/Scheduler/listDemos.php deleted file mode 100644 index 4df808941..000000000 --- a/src/Controllers/Scheduler/listDemos.php +++ /dev/null @@ -1,43 +0,0 @@ - - * @version 21072012 - * @package MyRadio_Scheduler - */ - -$demos = MyRadio_Demo::listDemos(); - -$twig = CoreUtils::getTemplateObject(); - -$tabledata = array(); -foreach ($demos as $demo) { - $demo['join'] = 'Join'; - $tabledata[] = $demo; -} - -if (empty($tabledata)) { - $tabledata = array(array('','','','','Error' => 'There are currently no demo slots available.')); -} - -//print_r($tabledata); -$twig->setTemplate('table.twig') - ->addVariable('title', 'Upcoming Demo Slots') - ->addVariable('tabledata', $tabledata) - ->addVariable('tablescript', 'myury.scheduler.demolist'); -if (isset($_REQUEST['msg'])) { - switch($_REQUEST['msg']) { - case 0: //joined - $twig->addInfo('You have successfully been added to this demo.'); - break; - case 1: //full - $twig->addError('Sorry, but a maximum two people can join a demo.'); - break; - case 2: //attending already - $twig->addError('You can only attend one demo at a time.'); - break; - } -} - -$twig->render(); \ No newline at end of file diff --git a/src/Controllers/Scheduler/listSeasons.php b/src/Controllers/Scheduler/listSeasons.php index 2ef6605db..2a642154d 100644 --- a/src/Controllers/Scheduler/listSeasons.php +++ b/src/Controllers/Scheduler/listSeasons.php @@ -1,22 +1,24 @@ - * @version 20130828 - * @package MyRadio_Scheduler + * Controller for outputting a Datatable of Seasons within the specified Show. + * * @todo This requires manual permission checks as it needs interesting things */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_Show; +use \MyRadio\ServiceAPI\MyRadio_User; $show = MyRadio_Show::getInstance($_REQUEST['showid']); $seasons = $show->getAllSeasons(); //This page is part of a joyride. We restart it if there's no seasons and this is their first Show. if (sizeof(MyRadio_User::getInstance()->getShows()) === 1 && sizeof($seasons) === 1) { - $_SESSION['joyride'] = 'first_show'; + $_SESSION['joyride'] = 'first_show'; } CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('tablescript', 'myury.scheduler.seasonlist') - ->addVariable('title', 'Seasons of '.$show->getMeta('title')) - ->addVariable('tabledata', ServiceAPI::setToDataSource($seasons)) - ->render(); \ No newline at end of file + ->addVariable('tablescript', 'myradio.scheduler.seasonlist') + ->addVariable('title', 'Scheduler') + ->addVariable('subtitle', 'Seasons of "'.$show->getMeta('title').'"') + ->addVariable('tabledata', CoreUtils::setToDataSource($seasons)) + ->render(); diff --git a/src/Controllers/Scheduler/listTerms.php b/src/Controllers/Scheduler/listTerms.php new file mode 100644 index 000000000..c2e8b94b6 --- /dev/null +++ b/src/Controllers/Scheduler/listTerms.php @@ -0,0 +1,23 @@ +toDataSource(); + $x['start'] = date('d/m/Y', $x['start']); + + return $x; + }, + MyRadio_Term::getAllTerms() +); + +CoreUtils::getTemplateObject()->setTemplate('Scheduler/listTerms.twig') + ->addVariable('title', 'Scheduler') + ->addVariable('subtitle', 'Manage Terms') + ->addVariable('tabledata', CoreUtils::dataSourceParser($terms)) + ->addVariable('tablescript', 'myradio.scheduler.termlist') + ->render(); diff --git a/src/Controllers/Scheduler/listTimeslots.php b/src/Controllers/Scheduler/listTimeslots.php index 491eadf5b..dafa02251 100644 --- a/src/Controllers/Scheduler/listTimeslots.php +++ b/src/Controllers/Scheduler/listTimeslots.php @@ -1,16 +1,17 @@ - * @version 26122012 - * @package MyRadio_Scheduler + * Controller for outputting a Datatable of Seasons within the specified Show. + * * @todo This requires manual permission checks as it needs interesting things */ +use MyRadio\MyRadio\CoreUtils; +use MyRadio\ServiceAPI\MyRadio_Season; $season = MyRadio_Season::getInstance($_GET['show_season_id']); CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('tablescript', 'myury.scheduler.timeslotlist') - ->addVariable('title', 'Episodes of '.$season->getMeta('title')) - ->addVariable('tabledata', ServiceAPI::setToDataSource($season->getAllTimeslots())) - ->render(); \ No newline at end of file + ->addVariable('tablescript', 'myradio.scheduler.timeslotlist') + ->addVariable('title', 'Scheduler') + ->addVariable('subtitle', 'Episodes of "'.$season->getMeta('title').'"') + ->addVariable('tabledata', CoreUtils::setToDataSource($season->getAllTimeslots())) + ->render(); diff --git a/src/Controllers/Scheduler/moveEpisode.php b/src/Controllers/Scheduler/moveEpisode.php new file mode 100644 index 000000000..24e7e8c79 --- /dev/null +++ b/src/Controllers/Scheduler/moveEpisode.php @@ -0,0 +1,46 @@ +getMoveForm()->readValues(); + + if ($data['new_start_time'] === $data['new_end_time']) { + $message = 'You can\'t have an episode start and end at the same time.'; + URLUtils::backWithMessage($message); + } else { + //Move + $result = $timeslot->moveTimeslot( + $data['new_start_time'], + $data['new_end_time'] + ); + + if ($result) { + $message = 'Move successful.'; + } else { + $message = 'Something didn\'t work! Please ping Computing.'; + } + + URLUtils::backWithMessage($message); + } +} else { + //Not Submitted + + if (!isset($_REQUEST['show_season_timeslot_id'])) { + throw new MyRadioException('No timeslotid provided.', 400); + } + + $timeslot = MyRadio_Timeslot::getInstance($_REQUEST['show_season_timeslot_id']); + + $timeslot->getMoveForm()->render(); +} diff --git a/src/Controllers/Scheduler/myShows.php b/src/Controllers/Scheduler/myShows.php index ba9d454c3..785119f6e 100644 --- a/src/Controllers/Scheduler/myShows.php +++ b/src/Controllers/Scheduler/myShows.php @@ -1,29 +1,29 @@ - * @version 20130828 - * @package MyRadio_Scheduler + * Lists Shows the User has. */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_User; $shows = MyRadio_User::getInstance()->getShows(); //This is a Joyride start point - if there are no shows, or it's their first season, run the first show joyride. if (empty($shows) or (sizeof($shows) === 1 and sizeof($shows[0]->getAllSeasons()) === 0)) { - $_SESSION['joyride'] = 'first_show'; + $_SESSION['joyride'] = 'first_show'; } $twig = CoreUtils::getTemplateObject()->setTemplate('Scheduler/myShows.twig') - ->addVariable('title', 'My Shows') - ->addVariable('tabledata', ServiceAPI::setToDataSource($shows)) - ->addVariable('tablescript', 'myury.scheduler.showlist'); + ->addVariable('title', 'Scheduler') + ->addVariable('subtitle', 'My Shows') + ->addVariable('tabledata', CoreUtils::setToDataSource($shows)) + ->addVariable('tablescript', 'myradio.scheduler.showlist'); + if (isset($_REQUEST['msg'])) { - switch($_REQUEST['msg']) { - case 'seasonCreated': - $twig->addInfo('Your season application has been submitted for processing.'); - break; - } + switch ($_REQUEST['msg']) { + case 'seasonCreated': + $twig->addInfo('Your season application has been submitted for processing.'); + break; + } } -$twig->render(); \ No newline at end of file +$twig->render(); diff --git a/src/Controllers/Scheduler/reject.php b/src/Controllers/Scheduler/reject.php index 8732e56d1..56cc55e5c 100644 --- a/src/Controllers/Scheduler/reject.php +++ b/src/Controllers/Scheduler/reject.php @@ -1,16 +1,24 @@ - * @version 02012013 - * @package MyRadio_Scheduler + * */ +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Season; -//Model: The Season to be rejected -$season = MyRadio_Season::getInstance((int)$_REQUEST['show_season_id']); -//Model: The Form definition -require 'Models/Scheduler/rejectfrm.php'; -$form->setFieldValue('season_id', $season->getID()); -//View: The Form -$form->render(); \ No newline at end of file +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = MyRadio_Season::getRejectForm()->readValues(); + + MyRadio_Season::getInstance($data['season_id']) + ->reject($data['reason'], $data['notify_user']); + + URLUtils::redirectWithMessage('Scheduler', 'default', 'Season Rejected!'); +} else { + //Not Submitted + + $season = MyRadio_Season::getInstance($_REQUEST['show_season_id']); + + MyRadio_Season::getRejectForm() + ->setFieldValue('season_id', $season->getID()) + ->render(); +} diff --git a/src/Controllers/Scheduler/setAutoViz.php b/src/Controllers/Scheduler/setAutoViz.php new file mode 100644 index 000000000..4be338286 --- /dev/null +++ b/src/Controllers/Scheduler/setAutoViz.php @@ -0,0 +1,29 @@ +getSeason()->isCurrentUserAnOwner()) { + AuthUtils::requirePermission(AUTH_EDITSHOWS); +} + +// And that it's not in the past +if ($timeslot->getStartTime() < time()) { + URLUtils::backWithMessage('That show is in the past!'); + die; +} + +$cfg = MyRadio_AutoVizConfiguration::getConfigForTimeslot($timeslot->getID()); +if ($cfg === null) { + $cfg = MyRadio_AutoVizConfiguration::create($timeslot->getID(), $_REQUEST['value'] === 'true', null, null); +} else { + $cfg->update($_REQUEST['value'] === 'true', null, null); +} + +URLUtils::backWithMessage('Updated successfully!'); diff --git a/src/Controllers/Scheduler/showPhoto.php b/src/Controllers/Scheduler/showPhoto.php index 8c0943bf0..43bbe9324 100644 --- a/src/Controllers/Scheduler/showPhoto.php +++ b/src/Controllers/Scheduler/showPhoto.php @@ -1,20 +1,39 @@ - * @version 20130529 - * @package MyRadio_Scheduler + * Set the show photo. */ +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\AuthUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Show; -if (!isset($_REQUEST['show_id'])) throw new MyRadioException('Show ID is required', 400); -$show = MyRadio_Show::getInstance($_REQUEST['show_id']); +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = MyRadio_Show::getPhotoForm()->readValues(); -//Require this is the user's show or the user can edit any show -if (!$show->isCurrentUserAnOwner()) { - CoreUtils::requirePermission(AUTH_EDITSHOWS); -} + $show = MyRadio_Show::getInstance($data['show_id']); + //Require this is the user's show or the user can edit any show + if (!$show->isCurrentUserAnOwner()) { + AuthUtils::requirePermission(AUTH_EDITSHOWS); + } + + $show->setShowPhoto($data['image_file']['tmp_name']); + + URLUtils::backWithMessage('Show Photo Updated!'); +} else { + //Not Submitted -//The Form definition -require 'Models/Scheduler/showphotofrm.php'; -$form->setFieldValue('show_id', $show->getID())->render(); \ No newline at end of file + if (!isset($_REQUEST['show_id'])) { + throw new MyRadioException('Show ID is required', 400); + } + $show = MyRadio_Show::getInstance($_REQUEST['show_id']); + + //Require this is the user's show or the user can edit any show + if (!$show->isCurrentUserAnOwner()) { + AuthUtils::requirePermission(AUTH_EDITSHOWS); + } + + MyRadio_Show::getPhotoForm() + ->setFieldValue('show_id', $show->getID()) + ->render(); +} diff --git a/src/Controllers/Scheduler/shows.php b/src/Controllers/Scheduler/shows.php new file mode 100644 index 000000000..712f66f45 --- /dev/null +++ b/src/Controllers/Scheduler/shows.php @@ -0,0 +1,16 @@ +setTemplate('table.twig') + ->addVariable('title', "Scheduler") + ->addVariable('subtitle', $all ? 'All Shows' : "This Term's Shows") + ->addVariable('tabledata', CoreUtils::setToDataSource($shows)) + ->addVariable('tablescript', 'myradio.scheduler.showlist') + ->render(); diff --git a/src/Controllers/Scheduler/stop.php b/src/Controllers/Scheduler/stop.php index e8eb80a43..9292ad5e2 100644 --- a/src/Controllers/Scheduler/stop.php +++ b/src/Controllers/Scheduler/stop.php @@ -2,11 +2,10 @@ /** * This stops everything. It's part of a several-stage process to trigger * the station's emergency broadcast system. - * - * @author Lloyd Wallis - * @version 20131215 - * @package MyRadio_Scheduler */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_Selector; +use \MyRadio\ServiceAPI\MyRadio_User; $result = true; $stage = isset($_POST['stage']) ? $_POST['stage'] : 1; @@ -16,11 +15,11 @@ $shows = MyRadio_User::getInstance()->getShows(); if (empty($shows)) { $result = false; - $stage--; + --$stage; } else { if (strtolower($shows[0]->getMeta('title')) !== strtolower($title)) { $result = false; - $stage--; + --$stage; } else { $result = MyRadio_User::getInstance()->getEduroam(); } @@ -28,12 +27,11 @@ } if ($stage == 0) { - $sel = new MyRadio_Selector(); - $sel->startObit(); + MyRadio_Selector::setObit(); } CoreUtils::getTemplateObject()->setTemplate('Scheduler/stop.twig') - ->addVariable('title', 'Stop Broadcast') - ->addVariable('stage', $stage) - ->addVariable('result', $result) - ->render(); + ->addVariable('title', 'Stop Broadcast') + ->addVariable('stage', $stage) + ->addVariable('result', $result) + ->render(); diff --git a/src/Controllers/Scheduler/tracking.php b/src/Controllers/Scheduler/tracking.php new file mode 100644 index 000000000..048810618 --- /dev/null +++ b/src/Controllers/Scheduler/tracking.php @@ -0,0 +1,50 @@ +getAllTimeslots() as $timeslot) { + if ($timeslot->getStartTime() < time()) { + foreach ($timeslot->getSigninInfo() as $info) { + if (isset($info["location"]) && $info["location"] != $no_track) { + if (isset($info["user"])) { + $eduroam = $info["user"]->getEduroam(); + $data[] = [ + "type" => "URY Member", + "info" => $info["user"]->getName() . ($eduroam ? " ($eduroam)" : ""), + "location" => $info["location"], + "time" => CoreUtils::happyTime($info["time"]), + "unix" => $info["time"] + ]; + } elseif ($info["guest_info"]) { + $data[] = [ + "type" => "Guest", + "info" => [ + "display" => "html", + "html" => nl2br($info["guest_info"]) + ], + "location" => $info["location"], + "time" => CoreUtils::happyTime($info["time"]), + "unix" => $info["time"] + ]; + } + } + } + } + } +} + +CoreUtils::getTemplateObject()->setTemplate("table.twig") + ->addVariable("title", "Tracking Information") + ->addVariable("tabledata", $data) + ->addVariable("tablescript", "myradio.scheduler.tracking") + ->render(); diff --git a/src/Controllers/Setup/checks.php b/src/Controllers/Setup/checks.php new file mode 100644 index 000000000..df322693c --- /dev/null +++ b/src/Controllers/Setup/checks.php @@ -0,0 +1,249 @@ + 'curl', + 'success' => 'cURL can be used to embed the IRN news service into SIS.', + 'fail' => 'If you had the cURL extension ' + .'MyRadio could use it provide IRN news information in SIS.', + 'required' => false, + ], + [ + 'module' => 'gd', + 'success' => 'The Image (GD) extension can be used to provide upload functionality ' + .'for the Podcast, Profile and Website modules.', + 'fail' => 'If you had the Image (GD) extension ' + .'MyRadio could be used to manage image content on Podcasts, Profiles and a frontend website.', + 'required' => false, + ], + [ + 'module' => 'ldap', + 'success' => 'The LDAP extension can be used to provide external authenticators that use the LDAP protocol.', + 'fail' => 'If you had the LDAP extension ' + .'MyRadio could integrate with external authentication providers.', + 'required' => false, + ], + [ + 'module' => 'pgsql', + 'success' => 'You have an appropriate database driver installed.', + 'fail' => 'The PostgreSQL extension ' + .'is required for MyRadio to talk to a database. Without this, it can\'t do much.', + 'required' => true, + ], + [ + 'module' => 'session', + 'success' => 'You have the session extension installed.', + 'fail' => 'The Session extension ' + .'is required for MyRadio to talk to keep track of who is logged in.', + 'required' => true, + ], +]; +$required_classes = [ + [ + 'class' => '\Twig_Environment', + 'success' => 'You have Twig installed! This is required for MyRadio to generate web pages.', + 'fail' => 'Your server needs to have Twig installed in order to continue. See ' + .'the Twig documentation for more information.', + 'required' => true, + ], +]; +$function_checks = [ + [ + //Check that max post size is at least 40MB + //this still won't be enough for most podcasts, but it should be for MP3s + 'function' => function () { + return min( + convertPHPSizeToBytes(ini_get('post_max_size')), + convertPHPSizeToBytes(ini_get('upload_max_filesize')) + ) > 40960; + }, + 'success' => 'Your server is configured to support large file uploads.', + 'fail' => 'Your server is set to have a small (<40MB) upload limit. Consider tweaking your php.ini to prevent ' + .'issues using Show Planner, Podcasts and other file upload utilities.', + 'required' => false, + ], +]; + +$ready = true; +$problems = []; +$warnings = []; +$successes = []; + +if (version_compare(phpversion(), '7.1', '<')) { + $ready = false; + $problems[] = 'You must be running at least PHP 7.1.'; +} else { + $successes[] = 'You are running PHP '.phpversion().'.'; +} + +foreach ($required_modules as $module) { + if (!extension_loaded($module['module'])) { + if ($module['required']) { + $ready = false; + $problems[] = $module['fail']; + } else { + $warnings[] = $module['fail']; + } + if (isset($module['set_fail'])) { + $config_overrides[$module['set_fail'][0]] = $module['set_fail'][1]; + } + } else { + $successes[] = $module['success']; + } +} +foreach ($required_classes as $class) { + if (!class_exists($class['class'])) { + if ($class['required']) { + $ready = false; + $problems[] = $class['fail']; + } else { + $warnings[] = $class['fail']; + } + } else { + $successes[] = $class['success']; + } +} +foreach ($function_checks as $check) { + if (!$check['function']()) { + if ($check['required']) { + $ready = false; + $problems[] = $check['fail']; + } else { + $warnings[] = $check['fail']; + } + } else { + $successes[] = $check['success']; + } +} + +?> + + + + Welcome to MyRadio + + + + + + + + + + +
          +
          +

          Hello there!

          +

          + It looks like you're trying to install MyRadio! Would you like some help with that? + No? Well too bad, I'm not a paperclip you can hide. +

          +

          I'm just running some background checks to see if you're ready to go.

          + '; + echo 'Good news! It looks like you\'re ready to go.'; + echo 'Click here to continue.'; + echo '

          '; + } else { + echo '

          '; + echo 'Uh oh! It looks like there\'s some things you\'ll have to get sorted out before you can continue.'; + echo 'Follow the advice below, then refresh this page to try again.'; + echo '

          '; + + echo '

          The following tests failed and must be fixed before you can proceed:

            '; + foreach ($problems as $problem) { + echo '
          • '.$problem.'
          • '; + } + echo '
          '; + } + + if (empty($warnings)) { + if ($ready) { + echo '

          Amazing! ' + .'Your server is absolutely perfect for running MyRadio.

          '; + } + } else { + echo '

          The following tests failed, but they aren\'t required for MyRadio to run:

            '; + foreach ($warnings as $warning) { + echo '
          • '.$warning.'
          • '; + } + echo '
          '; + } + + if (!empty($successes)) { + echo '

          The following tests passed without any issues:

            '; + foreach ($successes as $success) { + echo '
          • '.$success.'
          • '; + } + echo '
          '; + } + + if ($ready === false or !empty($warnings)) { + ?> +

          Cheating

          +

          If you're using Ubuntu (>=16.04), the following commands (as root) will get you most of the way:

          + + apt install php-curl php-gd php-ldap php-pgsql php-mbstring php-dev composer graphviz
          + composer update
          + service apache2 restart +
          + +
          +
          +
          +
          +
          + MyRadio by University Radio York +
          +
          +
          +
          + + diff --git a/src/Controllers/Setup/dbdata.php b/src/Controllers/Setup/dbdata.php new file mode 100644 index 000000000..02c7b00ab --- /dev/null +++ b/src/Controllers/Setup/dbdata.php @@ -0,0 +1,138 @@ +query( + 'INSERT INTO myradio.schema (attr, value) VALUES (\'datamode\', $1) ON CONFLICT (attr) DO UPDATE SET value = $1 WHERE schema.attr = \'datamode\'', + [$mode] + ); + + $warnings = []; + + /** + * For some reason, after a break; a further match of the same + * clause, so to avoid code duplication full actionauth is a function. + */ + function setUpFullActionsAuth() + { + global $db; + global $warnings; + foreach (json_decode(file_get_contents(SCHEMA_DIR.'data-actions.json')) as $action) { + //The getXxxId method creates these if they don't exist + $module = CoreUtils::getModuleId($action[0]); + CoreUtils::getActionId($module, $action[1]); + } + foreach (json_decode(file_get_contents(SCHEMA_DIR.'data-auth.json')) as $auth) { + AuthUtils::addPermission($auth[0], $auth[1]); + } + foreach (json_decode(file_get_contents(SCHEMA_DIR.'data-actionsauth.json')) as $actionauth) { + $module = CoreUtils::getModuleId($actionauth[0]); + $action = $actionauth[1] == null ? null : CoreUtils::getActionId($module, $actionauth[1]); + $auth = $actionauth[2] == null ? null : constant($actionauth[2]); + AuthUtils::addActionPermission($module, $action, $auth); + } + foreach (json_decode(file_get_contents(SCHEMA_DIR.'data-apiauth.json')) as $apiauth) { + $db->query( + 'INSERT INTO myury.api_method_auth (class_name, method_name, typeid) + VALUES ($1, $2, $3) + ON CONFLICT (class_name, method_name, typeid) DO NOTHING', + [$apiauth[0], $apiauth[1], $apiauth[2] === null ? null : constant($apiauth[2])] + ); + } + } + + switch ($mode) { + case DBDATA_PERMISSIONS: + setUpFullActionsAuth(); + break; + case DBDATA_COMPLETE: + setUpFullActionsAuth(); + $data = json_decode(file_get_contents(SCHEMA_DIR.'data-officers.json'), true); + foreach ($data['teams'] as $team) { + $oTeam = MyRadio_Team::createTeam($team[0], $team[1], $team[2], $team[3]); + foreach ($team[4] as $officer) { + MyRadio_Officer::createOfficer( + $officer[0], + $officer[1], + $officer[2], + $officer[3], + $oTeam, + $officer[4] + ); + } + } + break; + case DBDATA_SUDO: + foreach (json_decode(file_get_contents(SCHEMA_DIR.'data-actions.json')) as $action) { + if (in_array($action[1], ['actionPermissions', 'addActionPermission', 'listPermissions']) !== false) { + //Skip permissions controls + continue; + } + //The getXxxId method creates an ID if they don't exist + $module = CoreUtils::getModuleId($action[0]); + $action = CoreUtils::getActionId($module, $action[1]); + AuthUtils::addActionPermission($module, $action, null); + } + break; + case DBDATA_BLANK: + foreach (json_decode(file_get_contents(SCHEMA_DIR.'data-actions-min.json')) as $action) { + //The getXxxId method create these if they don't exist + $module = CoreUtils::getModuleId($action[0]); + CoreUtils::getActionId($module, $action[1]); + } + foreach (json_decode(file_get_contents(SCHEMA_DIR.'data-auth-min.json')) as $auth) { + try { + AuthUtils::addPermission($auth[0], $auth[1]); + } catch (MyRadioException $e) { + $warnings[] = 'Failed to create Permission "'.$auth[0].'". It may already exist.'; + } + } + foreach (json_decode(file_get_contents(SCHEMA_DIR.'data-actionsauth-min.json')) as $actionauth) { + $module = CoreUtils::getModuleId($actionauth[0]); + $action = $actionauth[1] == null ? null : CoreUtils::getActionId($module, $actionauth[1]); + $auth = $actionauth[2] == null ? null : constant($actionauth[2]); + AuthUtils::addActionPermission($module, $action, $auth); + } + break; + default: + die('Invalid mode control sequence.'); + } + + if (!empty($warnings)) { + CoreUtils::getTemplateObject() + ->setTemplate('Setup/dbdata_warning.twig') + ->addVariable('title', 'Database Data') + ->addVariable('warnings', $warnings) + ->addVariable('next', 'strings') + ->render(); + } else { + header('Location: ?c=strings'); + } +} else { + CoreUtils::getTemplateObject() + ->setTemplate('Setup/dbdata.twig') + ->addVariable('title', 'Database Data') + ->render(); +} diff --git a/src/Controllers/Setup/dbschema.php b/src/Controllers/Setup/dbschema.php new file mode 100644 index 000000000..425648f10 --- /dev/null +++ b/src/Controllers/Setup/dbschema.php @@ -0,0 +1,99 @@ +getMessage()); +} + +// What does the database currently look like? +$action = 'ERROR'; +try { + $result = Database::getInstance()->fetchColumn('SELECT value FROM myradio.schema WHERE attr=\'version\''); +} catch (Exception $e) { + $result = null; +} + +if (!isset($result[0])) { + //Well, it looks like MyRadio isn't installed here. + $version = 0; + $operation = 'NEW'; +} else { + $version = (int) $result[0]; + if ($version < MYRADIO_CURRENT_SCHEMA_VERSION) { + //MyRadio schema has been created, but is out of date. + $operation = 'UPGRADE'; + } elseif ($version > MYRADIO_CURRENT_SCHEMA_VERSION) { + //The MyRadio schema seems to be newer than the one we're expecting. + $operation = 'NEWER_WARN'; + } elseif ($version == MYRADIO_CURRENT_SCHEMA_VERSION) { + //Yay, nothing to do! + $operation = 'CURRENT'; + } +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + switch ($operation) { + case 'NEW': + $db = Database::getInstance(); + try { + $db->query(file_get_contents(SCHEMA_DIR.'base.sql')); + } catch (MyRadioException $e) { + $error = pg_last_error(); + CoreUtils::getTemplateObject() + ->setTemplate('Setup/dbschema_error.twig') + ->addVariable('title', 'Database Schema') + ->addVariable('error', $error) + ->render(); + exit; + } + + //Force a repeat until it recognises that base.sql has been imported + $next = '?c=dbschema'; + //Break deliberately ommitted - fallthrough to UPGRADE + case 'UPGRADE': + if (!isset($db)) { + $db = Database::getInstance(); + } + + $db->query('BEGIN'); + while ($version < MYRADIO_CURRENT_SCHEMA_VERSION) { + ++$version; + try { + $db->query(file_get_contents(SCHEMA_DIR.'patches/'.$version.'.sql')); + $db->query('UPDATE myradio.schema SET value='.$version.' WHERE attr=\'version\''); + } catch (MyRadioException $e) { + $error = pg_last_error(); + CoreUtils::getTemplateObject() + ->setTemplate('Setup/dbschema_error.twig') + ->addVariable('title', 'Database Schema') + ->addVariable('error', $error) + ->render(); + } + } + $db->query('COMMIT'); + if (!isset($next)) { + $next = '?c=dbdata'; + } + break; + case 'NEWER_WARN': + case 'CURRENT': + $next = '?c=dbdata'; + break; + default: + die('Unexpected database operation.'); + } + header('Location: '.$next); +} else { + CoreUtils::getTemplateObject() + ->setTemplate('Setup/dbschema.twig') + ->addVariable('title', 'Database Schema') + ->addVariable('operation', $operation) + ->render(); +} diff --git a/src/Controllers/Setup/dbserver.php b/src/Controllers/Setup/dbserver.php new file mode 100644 index 000000000..580561350 --- /dev/null +++ b/src/Controllers/Setup/dbserver.php @@ -0,0 +1,36 @@ + $v) { + Config::$$k = $v; + } + + //Test a DB connection + try { + $handle = Database::getInstance(); + } catch (MyRadioException $e) { + header('Location: ?c=dbserver&db_error=true'); + exit; //prevent further execution + } + //else + header('Location: ?c=dbschema'); +} else { + CoreUtils::getTemplateObject() + ->setTemplate('Setup/dbserver.twig') + ->addVariable('title', 'Database Server') + ->addVariable('db_error', isset($_GET['db_error'])) + ->render(); +} diff --git a/src/Controllers/Setup/root.php b/src/Controllers/Setup/root.php new file mode 100644 index 000000000..d0a705894 --- /dev/null +++ b/src/Controllers/Setup/root.php @@ -0,0 +1,47 @@ + $v) { + Config::$$k = $v; + } +} + +session_write_close(); +register_shutdown_function(function () { + if (isset($_SESSION)) { + //Something restarts this sometimes, it seems + session_write_close(); + } + if (isset($GLOBALS['config_overrides'])) { + session_start(); + $_SESSION['myradio_setup_config'] = $GLOBALS['config_overrides']; + } + + ob_end_flush(); +}); + +CoreUtils::actionSafe($controller); + +ob_start(); +require_once 'Controllers/Setup/'.$controller.'.php'; diff --git a/src/Controllers/Setup/save.php b/src/Controllers/Setup/save.php new file mode 100644 index 000000000..03730ba73 --- /dev/null +++ b/src/Controllers/Setup/save.php @@ -0,0 +1,78 @@ + $v) { + if (is_numeric($v) != true && is_bool($v) != true) { + $v = "'".str_replace("'", "\\'", $v)."'"; + } elseif ($v === true) { + $v = 'true'; + } elseif ($v === false) { + $v = 'false'; + } + $file_str .= 'Config::$'.$k.' = '.strval($v).";\n"; +} + +//Actually write the file +$file = @fopen($path, 'w'); +if (!$file) { + //...or not + CoreUtils::getTemplateObject() + ->setTemplate('minimal.twig') + ->addVariable( + 'content', + "An error occurred saving your settings.\n" + ."This is OK! It's probably best that the web server doesn't have write access to the webroot.\n" + ."Either write out the following into '$path' or give me write access to it, then reload this page.\n\n" + ) + ->addVariable( + 'rawcontent', + "

          " + ."' + ) + ->render(); +} else { + fwrite($file, $file_str); + fclose($file); + header('Location: ./'); +} diff --git a/src/Controllers/Setup/strings.php b/src/Controllers/Setup/strings.php new file mode 100644 index 000000000..eff557281 --- /dev/null +++ b/src/Controllers/Setup/strings.php @@ -0,0 +1,82 @@ +getProperty($key); + $name = ucwords(str_replace('_', ' ', $key)); + $desc = implode('
          ', MyRadio_Swagger::parseDoc($rProperty)['lines']); + $short_params[] = [$key, $name, $desc, Config::$$key]; +} + +foreach ($longtext as $key) { + $rProperty = $rConfig->getProperty($key); + $name = ucwords(str_replace('_', ' ', $key)); + $desc = implode('
          ', MyRadio_Swagger::parseDoc($rProperty)['lines']); + $long_params[] = [$key, $name, $desc, Config::$$key]; +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + foreach ($_POST as $k => $v) { + if (Config::$$k !== $v) { + $config_overrides[$k] = $v; + } + if (array_key_exists($k, $path_cfgs) && (!is_dir($v) && !mkdir($v, 0755, true))) { + die("Could not create '$k' directory at '$v'"); + } + } + /* Make sure log directory exists as well */ + $log_dir = dirname(Config::$log_file); + if (!is_dir($log_dir) && !mkdir($log_dir, 0755)) { + die("Could not create log directory at '$log_dir'"); + } + header('Location: ?c=user'); +} else { + CoreUtils::getTemplateObject() + ->setTemplate('Setup/strings.twig') + ->addVariable('title', 'Configurables') + ->addVariable('short', $short_params) + ->addVariable('long', $long_params) + ->render(); +} diff --git a/src/Controllers/Setup/user.php b/src/Controllers/Setup/user.php new file mode 100644 index 000000000..ffb1fdb21 --- /dev/null +++ b/src/Controllers/Setup/user.php @@ -0,0 +1,41 @@ + $_REQUEST['first-name'], + 'sname' => $_REQUEST['last-name'], + 'email' => $_REQUEST['email'], + 'phone' => $_REQUEST['phone'], + 'paid' => Config::$membership_fee, + 'provided_password' => $_REQUEST['password'] + ]; + $user = MyRadio_User::create($params); + + // Give this user most possible permissions + AuthUtils::setUpAuth(); + foreach (json_decode(file_get_contents(SCHEMA_DIR.'data-auth.json')) as $auth) { + if (!$auth[2] or !defined($auth[1])) { + continue; + } + $user->grantPermission(constant($auth[1])); + } + + header('Location: ?c=save'); +} else { + $pass_error = 'Password and Password Confirmation must be the same'; + CoreUtils::getTemplateObject() + ->setTemplate('Setup/user.twig') + ->addVariable('title', 'User') + ->addVariable('db_error', isset($_GET['err'])) + ->addVariable('pass_error', $pass_error) + ->render(); +} diff --git a/src/Controllers/Stats/bapsPlayCounter.php b/src/Controllers/Stats/bapsPlayCounter.php index d3dec5aa4..76547d825 100644 --- a/src/Controllers/Stats/bapsPlayCounter.php +++ b/src/Controllers/Stats/bapsPlayCounter.php @@ -1,19 +1,17 @@ - * @version 20130708 - * @package MyRadio_Stats + * The most played BAPS tracks for the given timeframe. */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_TracklistItem; -$start = isset($_GET['rangesel-starttime']) ? strtotime($_GET['rangesel-starttime']) : time()-(86400*28); +$start = isset($_GET['rangesel-starttime']) ? strtotime($_GET['rangesel-starttime']) : time() - (86400 * 28); $end = isset($_GET['rangesel-endtime']) ? strtotime($_GET['rangesel-endtime']) : time(); CoreUtils::getTemplateObject()->setTemplate('table_timeinput.twig') - ->addVariable('title', 'BAPS Track Statistics') - ->addVariable('tabledata', MyRadio_TracklistItem::getTracklistStatsForBAPS($start, $end)) - ->addVariable('tablescript', 'myury.stats.jukeboxplaycounter') - ->addVariable('starttime', CoreUtils::happyTime($start)) - ->addVariable('endtime', CoreUtils::happyTime($end)) - ->render(); \ No newline at end of file + ->addVariable('title', 'BAPS Track Statistics') + ->addVariable('tabledata', MyRadio_TracklistItem::getTracklistStatsForBAPS($start, $end)) + ->addVariable('tablescript', 'myradio.stats.jukeboxplaycounter') + ->addVariable('starttime', CoreUtils::happyTime($start)) + ->addVariable('endtime', CoreUtils::happyTime($end)) + ->render(); diff --git a/src/Controllers/Stats/default.php b/src/Controllers/Stats/default.php index 309a2bde5..f615fc441 100644 --- a/src/Controllers/Stats/default.php +++ b/src/Controllers/Stats/default.php @@ -1,12 +1,13 @@ - * @version 20130624 - * @package MyRadio_Stats + * Stats Overview. */ +use \MyRadio\MyRadio\CoreUtils; + CoreUtils::getTemplateObject()->setTemplate('MyRadio/text.twig') - ->addVariable('title', 'Statistics') - ->addVariable('text', 'This part of MyRadio shows you some interesting statistics about the station, from training maps to college breakdowns.') - ->render(); \ No newline at end of file + ->addVariable('title', 'Statistics') + ->addVariable( + 'text', + 'This part of MyRadio shows you some interesting statistics about the station, ' + .'from training maps to college breakdowns.' + )->render(); diff --git a/src/Controllers/Stats/digitisation.php b/src/Controllers/Stats/digitisation.php index 3db59e5c2..5f275ae47 100644 --- a/src/Controllers/Stats/digitisation.php +++ b/src/Controllers/Stats/digitisation.php @@ -1,15 +1,16 @@ - * @version 20130803 - * @package MyRadio_Stats + * CML Digitisation Status. */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_Track; + $options = null; CoreUtils::getTemplateObject()->setTemplate('bargraph.twig') - ->addVariable('title', 'Central Music Library Content Stats') - ->addVariable('data', json_encode(MyRadio_Track::getLibraryStats())) - ->addVariable('options', json_encode($options)) - ->addVariable('caption', 'This graph show aggregate statistics about the contents of The Central Music Library.') - ->render(); \ No newline at end of file + ->addVariable('title', 'Central Music Library Content Stats') + ->addVariable('data', json_encode(MyRadio_Track::getLibraryStats())) + ->addVariable('options', json_encode($options)) + ->addVariable( + 'caption', + 'This graph show aggregate statistics about the contents of The Central Music Library.' + )->render(); diff --git a/src/Controllers/Stats/errorRates.php b/src/Controllers/Stats/errorRates.php deleted file mode 100644 index 8da799fe7..000000000 --- a/src/Controllers/Stats/errorRates.php +++ /dev/null @@ -1,23 +0,0 @@ - - * @version 20130624 - * @package MyRadio_Stats - */ -$options = array( - 'title' => 'MyRadio Service Stats', - 'series' => array( - array('targetAxisIndex' => 0), - array('targetAxisIndex' => 0), - array('targetAxisIndex' => 1) - ) -); -CoreUtils::getTemplateObject()->setTemplate('linegraph.twig') - ->addVariable('title', 'MyRadio Error Rates') - ->addVariable('data', json_encode(CoreUtils::getErrorStats())) - ->addVariable('options', json_encode($options)) - ->addVariable('caption', 'This graph shows information error rate statistics for the last 24 hours. - Peaks in this graph can suggest a problem.') - ->render(); \ No newline at end of file diff --git a/src/Controllers/Stats/fullTracklist.php b/src/Controllers/Stats/fullTracklist.php index 6272393a2..d983eb0df 100644 --- a/src/Controllers/Stats/fullTracklist.php +++ b/src/Controllers/Stats/fullTracklist.php @@ -2,11 +2,11 @@ /** * Gets the full station tracklist - useful for PPL returns. - * - * @author Lloyd Wallis - * @version 20130830 - * @package MyRadio_Stats */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_TracklistItem; + $start = !empty($_GET['rangesel-starttime']) ? strtotime($_GET['rangesel-starttime']) : time() - (86400 * 28); $end = !empty($_GET['rangesel-endtime']) ? strtotime($_GET['rangesel-endtime']) : time(); @@ -15,36 +15,45 @@ $format = isset($_REQUEST['format']) ? $_REQUEST['format'] : 'html'; switch ($format) { - case 'csv': - $file = 'tracklist_'.$start.'-'.$end.'.csv'; - header('Content-Disposition: inline; filename="'.$file.'"'); - header("Content-Transfer-Encoding: Binary"); - header('Content-Type: text/csv'); - header('Content-Disposition: attachment; filename="' . $file . '"'); - $twig = CoreUtils::getTemplateObject()->setTemplate('csv.twig') - ->addVariable('data', $data) - ->render(); - break; - case 'html': - default: - $twig = CoreUtils::getTemplateObject()->setTemplate('table_timeinput.twig') - ->addVariable('title', 'Station Tracklist History') - ->addVariable('tablescript', 'myury.stats.fulltracklist') - ->addVariable('starttime', CoreUtils::happyTime($start)) - ->addVariable('endtime', CoreUtils::happyTime($end)); - - if (sizeof($data) >= 100000) { - $twig->addError('You have exceeded the maximum number of results for a ' - . 'single query. Please select a smaller timeframe and try again.'); - } + case 'csv': + $file = 'tracklist_'.$start.'-'.$end.'.csv'; + header('Content-Disposition: inline; filename="'.$file.'"'); + header('Content-Transfer-Encoding: Binary'); + header('Content-Type: text/csv'); + header('Content-Disposition: attachment; filename="'.$file.'"'); + CoreUtils::getTemplateObject()->setTemplate('csv.twig') + ->addVariable('data', $data) + ->render(); + break; + case 'html': + default: + $twig = CoreUtils::getTemplateObject()->setTemplate('table_timeinput.twig') + ->addVariable('title', 'Station Tracklist History') + ->addVariable('tablescript', 'myradio.stats.fulltracklist') + ->addVariable('starttime', CoreUtils::happyTime($start)) + ->addVariable('endtime', CoreUtils::happyTime($end)); - $twig->addInfo('It\'s probably easier to download this as a CSV File.'); + if (sizeof($data) >= 100000) { + $twig->addError( + 'You have exceeded the maximum number of results for a ' + .'single query. Please select a smaller timeframe and try again.' + ); + } - $twig->addVariable('tabledata', $data)->render(); - break; -} \ No newline at end of file + $twig->addInfo( + 'It\'s probably easier to download this as a CSV File.' + )->addVariable('tabledata', $data) + ->render(); + break; +} diff --git a/src/Controllers/Stats/jukeboxPlayCounter.php b/src/Controllers/Stats/jukeboxPlayCounter.php index 2544fee8e..2094f102d 100644 --- a/src/Controllers/Stats/jukeboxPlayCounter.php +++ b/src/Controllers/Stats/jukeboxPlayCounter.php @@ -1,19 +1,17 @@ - * @version 20130708 - * @package MyRadio_Stats + * The most played jukebox tracks in the given timeframe. */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_TracklistItem; -$start = isset($_GET['rangesel-starttime']) ? strtotime($_GET['rangesel-starttime']) : time()-(86400*28); +$start = isset($_GET['rangesel-starttime']) ? strtotime($_GET['rangesel-starttime']) : time() - (86400 * 28); $end = isset($_GET['rangesel-endtime']) ? strtotime($_GET['rangesel-endtime']) : time(); CoreUtils::getTemplateObject()->setTemplate('table_timeinput.twig') - ->addVariable('title', 'Jukebox Track Play Counter') - ->addVariable('tabledata', MyRadio_TracklistItem::getTracklistStatsForJukebox($start, $end)) - ->addVariable('tablescript', 'myury.stats.jukeboxplaycounter') - ->addVariable('starttime', CoreUtils::happyTime($start)) - ->addVariable('endtime', CoreUtils::happyTime($end)) - ->render(); \ No newline at end of file + ->addVariable('title', 'Jukebox Track Play Counter') + ->addVariable('tabledata', MyRadio_TracklistItem::getTracklistStatsForJukebox($start, $end)) + ->addVariable('tablescript', 'myradio.stats.jukeboxplaycounter') + ->addVariable('starttime', CoreUtils::happyTime($start)) + ->addVariable('endtime', CoreUtils::happyTime($end)) + ->render(); diff --git a/src/Controllers/Stats/mostListenedShowYear.php b/src/Controllers/Stats/mostListenedShowYear.php index 2f1233d1d..ad52a648e 100644 --- a/src/Controllers/Stats/mostListenedShowYear.php +++ b/src/Controllers/Stats/mostListenedShowYear.php @@ -1,13 +1,12 @@ - * @version 20130626 - * @package MyRadio_Stats + * The most listened to timeslots this academic year. */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_Show; + CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('title', 'Most listened to shows this academic year') - ->addVariable('tabledata', MyRadio_Show::getMostListened(strtotime(CoreUtils::getAcademicYear().'-09-01'))) - ->addVariable('tablescript', 'myury.datatable.default') - ->render(); \ No newline at end of file + ->addVariable('title', 'Most listened to shows this academic year') + ->addVariable('tabledata', MyRadio_Show::getMostListened(strtotime(CoreUtils::getAcademicYear().'-09-01'))) + ->addVariable('tablescript', 'myradio.stats.mostlistenedshow') + ->render(); diff --git a/src/Controllers/Stats/mostListenedTimeslotYear.php b/src/Controllers/Stats/mostListenedTimeslotYear.php index db3181537..566198a83 100644 --- a/src/Controllers/Stats/mostListenedTimeslotYear.php +++ b/src/Controllers/Stats/mostListenedTimeslotYear.php @@ -1,13 +1,12 @@ - * @version 20130626 - * @package MyRadio_Stats + * The most listened to timeslots this academic year. */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_Timeslot; + CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('title', 'Most listened to timeslots this academic year') - ->addVariable('tabledata', MyRadio_Timeslot::getMostListened(strtotime(CoreUtils::getAcademicYear().'-09-01'))) - ->addVariable('tablescript', 'myury.stats.mostlistenedtimeslot') - ->render(); \ No newline at end of file + ->addVariable('title', 'Most listened to timeslots this academic year') + ->addVariable('tabledata', MyRadio_Timeslot::getMostListened(strtotime(CoreUtils::getAcademicYear().'-09-01'))) + ->addVariable('tablescript', 'myradio.stats.mostlistenedtimeslot') + ->render(); diff --git a/src/Controllers/Stats/mostMessagedShowYear.php b/src/Controllers/Stats/mostMessagedShowYear.php index 6c97cc09d..eceed2f25 100644 --- a/src/Controllers/Stats/mostMessagedShowYear.php +++ b/src/Controllers/Stats/mostMessagedShowYear.php @@ -1,13 +1,12 @@ - * @version 20130626 - * @package MyRadio_Stats + * The most messaged shows this academic year. */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_Show; + CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('title', 'Most messaged shows this academic year') - ->addVariable('tabledata', MyRadio_Show::getMostMessaged(strtotime(CoreUtils::getAcademicYear().'-09-01'))) - ->addVariable('tablescript', 'myury.datatable.default') - ->render(); \ No newline at end of file + ->addVariable('title', 'Most messaged shows this academic year') + ->addVariable('tabledata', MyRadio_Show::getMostMessaged(strtotime(CoreUtils::getAcademicYear().'-09-01'))) + ->addVariable('tablescript', 'myradio.stats.mostmessagedshow') + ->render(); diff --git a/src/Controllers/Stats/mostMessagedTimeslotYear.php b/src/Controllers/Stats/mostMessagedTimeslotYear.php index c0d7a3be7..33a6913b0 100644 --- a/src/Controllers/Stats/mostMessagedTimeslotYear.php +++ b/src/Controllers/Stats/mostMessagedTimeslotYear.php @@ -1,13 +1,12 @@ - * @version 20130626 - * @package MyRadio_Stats + * The most messaged timeslots this academic year. */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_Timeslot; + CoreUtils::getTemplateObject()->setTemplate('table.twig') - ->addVariable('title', 'Most messaged timeslots this academic year') - ->addVariable('tabledata', MyRadio_Timeslot::getMostMessaged(strtotime(CoreUtils::getAcademicYear().'-09-01'))) - ->addVariable('tablescript', 'myury.stats.mostmessagedtimeslot') - ->render(); \ No newline at end of file + ->addVariable('title', 'Most messaged timeslots this academic year') + ->addVariable('tabledata', MyRadio_Timeslot::getMostMessaged(strtotime(CoreUtils::getAcademicYear().'-09-01'))) + ->addVariable('tablescript', 'myradio.stats.mostmessagedtimeslot') + ->render(); diff --git a/src/Controllers/Stats/trainingMap.php b/src/Controllers/Stats/trainingMap.php index 8e86667b2..4160adcdf 100644 --- a/src/Controllers/Stats/trainingMap.php +++ b/src/Controllers/Stats/trainingMap.php @@ -1,14 +1,25 @@ - * @version 20130624 - * @package MyRadio_Stats + * Training Map. */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_TrainingStatus; -CoreUtils::getTemplateObject()->setTemplate('MyRadio/fullimage.twig') - ->addVariable('title', 'Member Training Graph') - ->addVariable('caption', 'This screen, updated hourly, provides a complete map of who has trained who. Ever.') - ->addVariable('image', '/img/stats_training.svg') - ->render(); \ No newline at end of file +$status_map = function ($status) { + return ['value' => $status->getID(), 'text' => $status->getTitle()]; +}; + +$caption = 'Please select a Training Status above.'; +$img = ''; +if (isset($_GET['id'])) { + $title = MyRadio_TrainingStatus::getInstance($_GET['id'])->getTitle(); + $caption = 'This is a map of who trained who for the ' . $title . ' Training Status.'; + $img = 'img/stats_training_' . $_GET['id'] . '.svg'; +} + +CoreUtils::getTemplateObject()->setTemplate('MyRadio/trainingMap.twig') + ->addVariable('title', 'Member Training Graph') + ->addVariable('maps', array_map($status_map, MyRadio_TrainingStatus::getAll())) + ->addVariable('caption', $caption) + ->addVariable('image', $img) + ->render(); diff --git a/src/Controllers/Timelord/a-update.php b/src/Controllers/Timelord/a-update.php deleted file mode 100644 index 27ff21b3e..000000000 --- a/src/Controllers/Timelord/a-update.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @version 20130905 - * @package MyRadio_Timelord - */ -$sel = new MyRadio_Selector(); -$data = [ - 'selector' => $sel->query(), - 'shows' => MyRadio_Timeslot::getCurrentAndNext(null, 2), - 'breaking' => MyRadioNews::getLatestNewsItem(3), - 'ob' => MyRadio_Selector::remoteStreams(), - 'silence' => $sel->isSilence(), - 'obit' => $sel->isObitHappening() -]; - -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file diff --git a/src/Controllers/Timelord/default.php b/src/Controllers/Timelord/default.php deleted file mode 100644 index c9a439403..000000000 --- a/src/Controllers/Timelord/default.php +++ /dev/null @@ -1,12 +0,0 @@ - - * @version 20130904 - * @package MyRadio_Timelord - */ - -CoreUtils::getTemplateObject()->setTemplate('Timelord/main.twig') - ->addVariable('title', 'Studio Clock') - ->render(); \ No newline at end of file diff --git a/src/Controllers/Training/attendDemo.php b/src/Controllers/Training/attendDemo.php new file mode 100644 index 000000000..bf82654f0 --- /dev/null +++ b/src/Controllers/Training/attendDemo.php @@ -0,0 +1,9 @@ +attend(); +URLUtils::redirect($module, 'listDemos', ['msg' => $result]); diff --git a/src/Controllers/Training/cancelDemo.php b/src/Controllers/Training/cancelDemo.php new file mode 100644 index 000000000..6256d03aa --- /dev/null +++ b/src/Controllers/Training/cancelDemo.php @@ -0,0 +1,47 @@ +getDemoer()->getID() !== MyRadio_User::getCurrentUser()->getID()) { + AuthUtils::requirePermission(AUTH_CANCELANYDEMO); +} + +$form = (new MyRadioForm('cancelDemo', 'Training', 'cancelDemo')) + ->addField(new MyRadioFormField('demo_id', MyRadioFormField::TYPE_HIDDEN)) + ->setFieldValue('demo_id', $_REQUEST['demo_id']); + +$attendees = $demo->attendingDemoCount(); +if ($attendees > 0) { + $form = $form->addField(new MyRadioFormField('cancel_attendees', MyRadioFormField::TYPE_CHECK, [ + 'label' => 'Confirm cancellation of training with attendees', + 'explanation' => "This training session has $attendees attendees. If you cancel, they will receive an email notifying them." + ])); +} + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + $data = $form->readValues(); + try { + $demo->delete($data['cancel_attendees'] ?? false); + } catch (MyRadioException $e) { + if ($e->getCode() === 409) { + URLUtils::backWithMessage('This demo has attendees, please confirm you wish to cancel it.'); + } else { + throw $e; + } + } + URLUtils::redirectWithMessage('Training', 'listDemos', 'Demo cancelled!'); +} else { + $form->render(['title' => 'Cancel Training Session']); +} diff --git a/src/Controllers/Training/createDemo.php b/src/Controllers/Training/createDemo.php new file mode 100644 index 000000000..0767b3383 --- /dev/null +++ b/src/Controllers/Training/createDemo.php @@ -0,0 +1,41 @@ +readValues(); + if ($demoinfo['id']) { + // Update a Demo + MyRadio_Demo::getInstance($demoinfo['id'])->editDemo( + $demoinfo['demo_datetime'], + $demoinfo['demo_training_type'], + $demoinfo['demo_max_participants'], + $demoinfo['demo_link'], + $demoinfo['signup_cutoff_hours'] + ); + } else { + // Create a New Demo + MyRadio_Demo::registerDemo($demoinfo['demo_datetime'], $demoinfo['demo_training_type'], $demoinfo['demo_max_participants'], $demoinfo['demo_link'], $demoinfo['signup_cutoff_hours']); + } + URLUtils::backWithMessage('Session Updated!'); +} else { + //Not Submitted + if (isset($_REQUEST["demo_id"])) { + // Update Demo + MyRadio_Demo::getInstance($_REQUEST["demo_id"]) + ->getEditForm() + ->setTemplate("Training/createDemo.twig") + ->render(); + } else { + // Create New Demo + MyRadio_Demo::getForm() + ->setTemplate('Training/createDemo.twig') + ->render(); + } +} diff --git a/src/Controllers/Training/joinList.php b/src/Controllers/Training/joinList.php new file mode 100644 index 000000000..2436fb07e --- /dev/null +++ b/src/Controllers/Training/joinList.php @@ -0,0 +1,9 @@ + 0]); diff --git a/src/Controllers/Training/leaveDemo.php b/src/Controllers/Training/leaveDemo.php new file mode 100644 index 000000000..92cb2c36d --- /dev/null +++ b/src/Controllers/Training/leaveDemo.php @@ -0,0 +1,7 @@ +leave(); +URLUtils::redirect($module, 'listDemos', ['msg' => $result === 0 ? -1 : $result * -1]); // -1 means left... see listDemos.php diff --git a/src/Controllers/Training/leaveList.php b/src/Controllers/Training/leaveList.php new file mode 100644 index 000000000..a9053328b --- /dev/null +++ b/src/Controllers/Training/leaveList.php @@ -0,0 +1,9 @@ + 1]); diff --git a/src/Controllers/Training/listDemos.php b/src/Controllers/Training/listDemos.php new file mode 100644 index 000000000..9d5a183b6 --- /dev/null +++ b/src/Controllers/Training/listDemos.php @@ -0,0 +1,120 @@ +usersAttendingDemo(); + $demo['join'] = [ + 'display' => 'text', + 'value' => 'Edit Session', + 'url' => URLUtils::makeURL('Training', 'createDemo', ['demo_id' => $demo['demo_id']]), + ]; + $demo['cancel'] = [ + 'display' => 'icon', + 'value' => 'trash', + 'title' => 'Cancel Session', + 'url' => URLUtils::makeURL('Training', 'cancelDemo', ['demo_id' => $demo['demo_id']]), + ]; + } else { + if ($demo_object->isUserAttendingDemo($currentUser->getID())) { + if ($demo_object->tooCloseToStart()) { + $demo['attending'] = 'You are attending this demo. Please contact your trainer if you can no longer make it.'; + $demo['join'] = [ + 'display' => 'none', + ]; + } else { + $demo['attending'] = 'You are attending this demo'; + $demo['join'] = [ + 'display' => 'text', + 'value' => 'Leave', + 'url' => URLUtils::makeURL('Training', 'leaveDemo', ['demoid' => $demo['demo_id']]), + ]; + } + } elseif ($demo_object->isSpaceOnDemo()) { + if ($demo_object->tooCloseToStart()) { + $demo['attending'] = 'Too late to sign up'; + $demo['join'] = [ + 'display' => 'none', + ]; + } else { + $demo['attending'] = 'Space available!'; + $demo['join'] = [ + 'display' => 'text', + 'value' => 'Join', + 'url' => URLUtils::makeURL('Training', 'attendDemo', ['demoid' => $demo['demo_id']]), + ]; + } + } else { + $demo['attending'] = 'Demo full'; + $demo['join'] = ['display' => 'none']; + } + $demo["cancel"] = ''; + } + + if ($demo['demo_link']) { + $demo['demo_link'] = [ + "display" => "icon", + "value" => "headphones", + "title" => "Online Training" + ]; + } else { + $demo['demo_link'] = [ + "display" => "icon", + "value" => "user", + "title" => "In-Person Training" + ]; + } + $tabledata[] = $demo; +} + +if (empty($tabledata)) { + $tabledata = [['', '', '', '', '', 'Error' => 'There are currently no training slots available.', '']]; +} + +//print_r($tabledata); +$twig->setTemplate('table.twig') + ->addVariable('title', 'Upcoming Training Slots') + ->addVariable('tabledata', $tabledata) + ->addVariable('tablescript', 'myradio.training.demolist'); + +if (isset($_REQUEST['msg'])) { + switch ($_REQUEST['msg']) { + case 0: //joined + $twig->addInfo('You have successfully been added to this session.'); + break; + case 1: //full + $twig->addError('Sorry, but too many people are already attending this session.'); + break; + case 2: //attending already + $twig->addError('You can only attend one session for that training at a time.'); + break; + case 3: //too late + $twig->addError('It is now too late to join this session. If you still wish to attend, please contact the trainer directly to see if there is space available.'); + break; + case -1: // Left session + $twig->addInfo('You have left the training session.'); + break; + case -3: //too late + $twig->addError('It is now too late to leave this session. If you cannot make it, please contact the trainer directly.'); + break; + } +} + +$twig->render(); diff --git a/src/Controllers/Training/listWaitingLists.php b/src/Controllers/Training/listWaitingLists.php new file mode 100644 index 000000000..8d270fb6b --- /dev/null +++ b/src/Controllers/Training/listWaitingLists.php @@ -0,0 +1,70 @@ +getTitle(); + $list['link'] = [ + "display" => "text", + "value" => "Leave Waiting List", + "url" => URLUtils::makeURL("Training", "leaveList", ["presenterstatusid" => $presenterstatusid]) + ]; + $list['date_added'] = date('d M Y', strtotime($list['date_added'])); + $tabledata[] = $list; + $waiting_statuses[] = $presenterstatusid; +} + +$can_be_awarded = MyRadio_TrainingStatus::getAllToBeEarned(MyRadio_User::getCurrentUser()); +foreach ($can_be_awarded as $status) { + if (!in_array($status->getID(), $waiting_statuses)) { + $list = [ + "presenterstatusid" => $status->getTitle(), + "date_added" => "", + "link" => [ + "display" => "text", + "value" => "Join Waiting List", + "url" => URLUtils::makeURL("Training", "joinList", ["presenterstatusid" => $status->getID()]) + ] + ]; + + $tabledata[] = $list; + } +} + +$twig->setTemplate('table.twig') + ->addVariable('title', 'My Training Waiting Lists') + ->addVariable('tabledata', $tabledata) + ->addVariable('tablescript', 'myradio.training.waitinglist'); + +if (isset($_REQUEST['msg'])) { + switch ($_REQUEST['msg']) { + case 0: //joined + $twig->addInfo("You have successfully been added to this waiting list." + . "We'll let you know if a training session becomes available."); + break; + case 1: //left + $twig->addInfo('You have left the waiting list.'); + break; + } +} + +$twig->render(); diff --git a/src/Controllers/Webcam/a-trackViewer.php b/src/Controllers/Webcam/a-trackViewer.php deleted file mode 100644 index ba6f1110a..000000000 --- a/src/Controllers/Webcam/a-trackViewer.php +++ /dev/null @@ -1,13 +0,0 @@ - time()-10) { - require 'Controllers/Errors/400.php'; -} -$_SESSION['webcam_lastcounterincrement'] = time(); - -$data = MyRadio_Webcam::incrementViewCounter(MyRadio_User::getInstance()); - -require 'Views/MyRadio/datatojson.php'; \ No newline at end of file diff --git a/src/Controllers/Webcam/archive.php b/src/Controllers/Webcam/archive.php index 28a8686bc..2396dd796 100644 --- a/src/Controllers/Webcam/archive.php +++ b/src/Controllers/Webcam/archive.php @@ -1,16 +1,10 @@ - * @version 21112012 - * @package MyRadio_Webcam + * Controller for viewing webcam archives. */ -$streams = MyRadio_Webcam::getStreams(); -//Skip "Live" -/** - * @todo This is quite a nasty way of doing it. Is there a better one? - */ -array_shift($streams); +use \MyRadio\MyRadio\CoreUtils; -$times = MyRadio_Webcam::getArchiveTimeRange(); \ No newline at end of file +CoreUtils::getTemplateObject()->setTemplate('MyRadio/text.twig') + ->addVariable('title', 'Webcams Archive') + ->addInfo('Still coming soon to a MyRadio near you...') + ->render(); diff --git a/src/Controllers/Webcam/default.php b/src/Controllers/Webcam/default.php index c88e19385..a7c05f03c 100644 --- a/src/Controllers/Webcam/default.php +++ b/src/Controllers/Webcam/default.php @@ -1,10 +1,12 @@ - * @version 28072012 - * @package MyRadio_Webcam */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_Webcam; + $streams = MyRadio_Webcam::getStreams(); -require 'Views/Webcam/streams.php'; \ No newline at end of file + +CoreUtils::getTemplateObject()->setTemplate('Webcam/grid.twig') + ->addVariable('streams', $streams) + ->render(); diff --git a/src/Controllers/Webcam/focus.php b/src/Controllers/Webcam/focus.php index c8da323c6..e15f42be8 100644 --- a/src/Controllers/Webcam/focus.php +++ b/src/Controllers/Webcam/focus.php @@ -1,11 +1,14 @@ - * @version 02082012 - * @package MyRadio_Webcam */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\ServiceAPI\MyRadio_Webcam; + $streams = MyRadio_Webcam::getStreams(); -$live = array_shift($streams); -require 'Views/Webcam/focus.php'; \ No newline at end of file +$live = $streams[0]; + +CoreUtils::getTemplateObject()->setTemplate('Webcam/focus.twig') + ->addVariable('streams', $streams) + ->addVariable('live', $live) + ->render(); diff --git a/src/Controllers/Website/banners.php b/src/Controllers/Website/banners.php index e8100507c..5bcad4c65 100644 --- a/src/Controllers/Website/banners.php +++ b/src/Controllers/Website/banners.php @@ -1,14 +1,13 @@ - * @version 20130807 - * @package MyRadio_Website */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Banner; CoreUtils::getTemplateObject()->setTemplate('Website/banners.twig')->addVariable('title', 'Website Banners') - ->addVariable('newbannerurl', CoreUtils::makeURL('Website', 'createBanner')) - ->addVariable('tabledata', CoreUtils::dataSourceParser(MyRadio_Banner::getAllBanners())) - ->addVariable('tablescript', 'myury.website.bannerlist') - ->render(); \ No newline at end of file + ->addVariable('newbannerurl', URLUtils::makeURL('Website', 'editBanner')) + ->addVariable('tabledata', CoreUtils::dataSourceParser(MyRadio_Banner::getAllBanners())) + ->addVariable('tablescript', 'myradio.website.bannerlist') + ->render(); diff --git a/src/Controllers/Website/campaigns.php b/src/Controllers/Website/campaigns.php index 5be91ff14..40af22548 100644 --- a/src/Controllers/Website/campaigns.php +++ b/src/Controllers/Website/campaigns.php @@ -1,22 +1,24 @@ - * @version 20130807 - * @package MyRadio_Website + * List Campaigns. */ +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Banner; if (!isset($_REQUEST['bannerid'])) { - throw new MyRadioException('You must provide a bannerid', 400); + throw new MyRadioException('You must provide a bannerid', 400); } $banner = MyRadio_Banner::getInstance($_REQUEST['bannerid']); CoreUtils::getTemplateObject()->setTemplate('Website/campaigns.twig')->addVariable('title', 'Banner Campaigns') - ->addVariable('newcampaignurl', CoreUtils::makeURL('Website', 'createCampaign', ['bannerid' => $_REQUEST['bannerid']])) - ->addVariable('bannersurl', CoreUtils::makeURL('Website', 'banners')) - ->addVariable('bannerName', $banner->getAlt()) - ->addVariable('tabledata', CoreUtils::dataSourceParser($banner->getCampaigns())) - ->addVariable('tablescript', 'myury.website.campaignlist') - ->render(); \ No newline at end of file + ->addVariable( + 'newcampaignurl', + URLUtils::makeURL('Website', 'editCampaign', ['bannerid' => $_REQUEST['bannerid']]) + )->addVariable('bannersurl', URLUtils::makeURL('Website', 'banners')) + ->addVariable('bannerName', $banner->getAlt()) + ->addVariable('tabledata', CoreUtils::dataSourceParser($banner->getCampaigns())) + ->addVariable('tablescript', 'myradio.website.campaignlist') + ->render(); diff --git a/src/Controllers/Website/createBanner.php b/src/Controllers/Website/createBanner.php deleted file mode 100644 index 1d498a8bf..000000000 --- a/src/Controllers/Website/createBanner.php +++ /dev/null @@ -1,10 +0,0 @@ - - * @version 20130809 - * @package MyRadio_Website - */ - -MyRadio_Banner::getBannerForm()->render(); \ No newline at end of file diff --git a/src/Controllers/Website/createCampaign.php b/src/Controllers/Website/createCampaign.php deleted file mode 100644 index c476569b1..000000000 --- a/src/Controllers/Website/createCampaign.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @version 20130808 - * @package MyRadio_Website - */ -if (!isset($_REQUEST['bannerid'])) { - throw new MyRadioException('You must provide a bannerid', 400); -} - -$banner = MyRadio_Banner::getInstance($_REQUEST['bannerid']); - -MyRadio_BannerCampaign::getBannerCampaignForm($banner->getBannerID())->render([ - 'bannerName' => $banner->getAlt() -]); \ No newline at end of file diff --git a/src/Controllers/Website/default.php b/src/Controllers/Website/default.php index adf0f0e89..132782973 100644 --- a/src/Controllers/Website/default.php +++ b/src/Controllers/Website/default.php @@ -1,12 +1,11 @@ - * @version 20130806 - * @package MyRadio_Website + * Landing page for Website Tools. */ +use \MyRadio\MyRadio\CoreUtils; CoreUtils::getTemplateObject()->setTemplate('MyRadio/text.twig')->addVariable('title', 'Website Tools') - ->addVariable('text', 'This section of MyRadio lets you control some aspects of the Website, such as banners and themes.') - ->render(); \ No newline at end of file + ->addVariable( + 'text', + 'This section of MyRadio lets you control some aspects of the Website, such as banners and themes.' + )->render(); diff --git a/src/Controllers/Website/doCreateBanner.php b/src/Controllers/Website/doCreateBanner.php deleted file mode 100644 index cd833d0fe..000000000 --- a/src/Controllers/Website/doCreateBanner.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @version 20130806 - * @package MyRadio_Website - */ - -$data = MyRadio_Banner::getBannerForm()->readValues(); - -$photo = MyRadio_Photo::create($data['photo']['tmp_name']); - -$banner = MyRadio_Banner::create($photo, $data['alt'], $data['target'], $data['type']); - -header('Location: '.CoreUtils::makeURL('Website', 'campaigns', [ - 'bannerid' => $banner->getBannerID(), - 'message' => base64_encode('Your new Banner has been created!') - ])); \ No newline at end of file diff --git a/src/Controllers/Website/doCreateCampaign.php b/src/Controllers/Website/doCreateCampaign.php deleted file mode 100644 index 3c7e95dcc..000000000 --- a/src/Controllers/Website/doCreateCampaign.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @version 20130809 - * @package MyRadio_Website - */ - -$data = MyRadio_BannerCampaign::getBannerCampaignForm()->readValues(); - -$campaign = MyRadio_BannerCampaign::create(MyRadio_Banner::getInstance($data['bannerid']), - $data['location'], $data['effective_from'], $data['effective_to'], $data['timeslots']); - -header('Location: '.CoreUtils::makeURL('Website', 'editCampaign', [ - 'campaignid' => $campaign->getID(), - 'message' => base64_encode('The new Campaign was created successfully!') -])); \ No newline at end of file diff --git a/src/Controllers/Website/doEditBanner.php b/src/Controllers/Website/doEditBanner.php deleted file mode 100644 index 03bdd254d..000000000 --- a/src/Controllers/Website/doEditBanner.php +++ /dev/null @@ -1,24 +0,0 @@ - - * @version 20130806 - * @package MyRadio_Website - */ - -$data = MyRadio_Banner::getBannerForm()->readValues(); - -$banner = MyRadio_Banner::getInstance($data['id']) - ->setAlt($data['alt']) - ->setTarget($data['target']) - ->setType($data['type']); - -if ($data['photo']['error'] == 0) { - //Upload replacement Photo - $banner->setPhoto(MyRadioPhoto::create($data['photo']['tmp_name'])); -} - -header('Location: '.CoreUtils::makeURL('Website', 'banners', [ - 'message' => base64_encode('The Banner was updated successfully!') -])); \ No newline at end of file diff --git a/src/Controllers/Website/doEditCampaign.php b/src/Controllers/Website/doEditCampaign.php deleted file mode 100644 index 95a3c0d15..000000000 --- a/src/Controllers/Website/doEditCampaign.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @version 20130809 - * @package MyRadio_Website - */ -$data = MyRadio_BannerCampaign::getBannerCampaignForm()->readValues(); - -$campaign = MyRadio_BannerCampaign::getInstance($data['id']); - -$campaign->clearTimeslots(); - -foreach ($data['timeslots'] as $timeslot) { - $campaign->addTimeslot($timeslot['day'], $timeslot['start_time'], $timeslot['end_time']); -} - -$campaign->setEffectiveFrom($data['effective_from']) - ->setEffectiveTo($data['effective_to']) - ->setLocation($data['location']); - -header('Location: '.CoreUtils::makeURL('Website', 'campaigns', [ - 'bannerid' => $campaign->getBanner()->getBannerID(), - 'message' => base64_encode('The Campaign was updated succesfully!') - ])); \ No newline at end of file diff --git a/src/Controllers/Website/editBanner.php b/src/Controllers/Website/editBanner.php index 59098926f..e8c1abaaa 100644 --- a/src/Controllers/Website/editBanner.php +++ b/src/Controllers/Website/editBanner.php @@ -1,15 +1,44 @@ - * @version 20130806 - * @package MyRadio_Website + * Edit a Banner. */ +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Banner; +use \MyRadio\ServiceAPI\MyRadio_Photo; -if (!isset($_REQUEST['bannerid'])) { - throw new MyRadioException('You must provide a bannerid', 400); -} +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = MyRadio_Banner::getForm()->readValues(); + + if (empty($data['id'])) { + //create new + $photo = MyRadio_Photo::create($data['photo']['tmp_name']); + $banner = MyRadio_Banner::create($photo, $data['alt'], $data['target'], $data['type']); + } else { + //submit edit + $banner = MyRadio_Banner::getInstance($data['id']) + ->setAlt($data['alt']) + ->setTarget($data['target']) + ->setType($data['type']); + + if ($data['photo']['error'] == 0) { + //Upload replacement Photo + $banner->setPhoto(MyRadio_Photo::create($data['photo']['tmp_name'])); + } + } -$banner = MyRadio_Banner::getInstance($_REQUEST['bannerid']); -$banner->getEditForm()->render(['bannerName' => $banner->getAlt()]); \ No newline at end of file + URLUtils::backWithMessage('Banner Updated!'); +} else { + //Not Submitted + + if (isset($_REQUEST['bannerid'])) { + //edit form + $banner = MyRadio_Banner::getInstance($_REQUEST['bannerid']); + $banner + ->getEditForm() + ->render(['bannerName' => $banner->getAlt(), 'bannerURL' => $banner->getURL()]); + } else { + //create form + MyRadio_Banner::getForm()->render(); + } +} diff --git a/src/Controllers/Website/editCampaign.php b/src/Controllers/Website/editCampaign.php index 403b31dfd..9343fd9d3 100644 --- a/src/Controllers/Website/editCampaign.php +++ b/src/Controllers/Website/editCampaign.php @@ -1,18 +1,69 @@ - * @version 20130808 - * @package MyRadio_Website + * Edit a Campaign. */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\ServiceAPI\MyRadio_Banner; +use \MyRadio\ServiceAPI\MyRadio_BannerCampaign; -if (!isset($_REQUEST['campaignid'])) { - throw new MyRadioException('You must provide a campaignid', 400); -} +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = MyRadio_BannerCampaign::getForm()->readValues(); + + if (empty($data['id'])) { + //create new + $campaign = MyRadio_BannerCampaign::create( + MyRadio_Banner::getInstance($data['bannerid']), + $data['location'], + $data['effective_from'], + $data['effective_to'], + $data['timeslots'] + ); + } else { + //submit edit + $campaign = MyRadio_BannerCampaign::getInstance($data['id']); + + $campaign->clearTimeslots(); + + foreach ($data['timeslots'] as $timeslot) { + $campaign->addTimeslot($timeslot['day'], $timeslot['start_time'], $timeslot['end_time']); + } + + $campaign->setEffectiveFrom($data['effective_from']) + ->setEffectiveTo($data['effective_to']) + ->setLocation($data['location']); + } + + URLUtils::backWithMessage('Campaign Updated!'); +} else { + //Not Submitted -$campaign = MyRadio_BannerCampaign::getInstance($_REQUEST['campaignid']); -$campaign->getEditForm()->render([ - 'campaignStart'=> CoreUtils::happyTime($campaign->getEffectiveFrom()), - 'bannerName'=> $campaign->getBanner()->getAlt() -]); \ No newline at end of file + if (isset($_REQUEST['campaignid'])) { + //edit form + + $campaign = MyRadio_BannerCampaign::getInstance($_REQUEST['campaignid']); + $campaign->getEditForm() + ->render( + [ + 'campaignStart' => CoreUtils::happyTime($campaign->getEffectiveFrom()), + 'bannerName' => $campaign->getBanner()->getAlt(), + ] + ); + } else { + //create form + + if (!isset($_REQUEST['bannerid'])) { + throw new MyRadioException('You must provide a bannerid', 400); + } + + $banner = MyRadio_Banner::getInstance($_REQUEST['bannerid']); + + MyRadio_BannerCampaign::getForm($banner->getBannerID()) + ->render( + [ + 'bannerName' => $banner->getAlt(), + ] + ); + } +} diff --git a/src/Controllers/Website/editShortUrl.php b/src/Controllers/Website/editShortUrl.php new file mode 100644 index 000000000..9fdbb253d --- /dev/null +++ b/src/Controllers/Website/editShortUrl.php @@ -0,0 +1,55 @@ +readValues(); + + $slug = trim($data['slug']); + + if ($slug[0] === '/') { + URLUtils::backWithMessage("Slugs can't start with a slash."); + exit; + } + + foreach (Config::$short_url_forbidden_slugs as $test) { + if (strpos($slug, $test) === 0) { + URLUtils::backWithMessage("You can't use '$test' as a slug. Sorry. Please choose another one."); + exit; + } + } + + if (empty($data['id'])) { + //create new + $shortUrl = MyRadio_ShortURL::create($slug, $data['redirect_to']); + } else { + //submit edit + $shortUrl = MyRadio_ShortURL::getInstance($data['id']) + ->setSlug($slug) + ->setRedirectTo($data['redirect_to']); + } + + URLUtils::backWithMessage('Short URL updated! ' . + 'Please note that it can take up to 10 minutes for it to become active.'); +} else { + //Not Submitted + + if (isset($_REQUEST['shorturlid'])) { + //edit form + /** @var MyRadio_ShortURL $shortUrl */ + $shortUrl = MyRadio_ShortURL::getInstance($_REQUEST['shorturlid']); + $shortUrl + ->getEditForm() + ->render(); + } else { + //create form + MyRadio_ShortURL::getForm()->render(); + } +} diff --git a/src/Controllers/Website/shortUrls.php b/src/Controllers/Website/shortUrls.php new file mode 100644 index 000000000..2170bc8e2 --- /dev/null +++ b/src/Controllers/Website/shortUrls.php @@ -0,0 +1,13 @@ +setTemplate('Website/shortUrls.twig') + ->addVariable('title', 'Short URLs') + ->addVariable('newshorturlurl', URLUtils::makeURL('Website', 'editShortUrl')) + ->addVariable('tabledata', CoreUtils::dataSourceParser(MyRadio_ShortURL::getAll())) + ->addVariable('tablescript', 'myradio.website.shorturllist') + ->render(); + diff --git a/src/Controllers/api/graphql.php b/src/Controllers/api/graphql.php new file mode 100644 index 000000000..8f7c8e24d --- /dev/null +++ b/src/Controllers/api/graphql.php @@ -0,0 +1,394 @@ +schema->getType($typeName); + } + // If not, we need to get a bit crafty - in this case it might be an array. + // Go through all the possible types of the union, exclude all the ones that are MyRadioObjects + // (as they would be ServiceAPIs, and thus caught above) + // If only one is left, use that, otherwise it's ambiguous + /** @var UnionType $union */ + $union = $info->returnType; + if ($union instanceof WrappingType) { + $union = $union->getWrappedType(true); + } + /** @var InterfaceType $myRadioObjectType */ + $myRadioObjectType = $info->schema->getType('MyRadioObject'); + $candidates = []; + foreach ($union->getTypes() as $test) { + if (!($test->implementsInterface($myRadioObjectType))) { + $candidates[] = $test->name; + } + } + if (count($candidates) === 1) { + return $candidates[0]; + } else { + $typeName = $info->returnType->name; + $parent = $info->parentType->name; + $field = $info->fieldName; + throw new MyRadioException( + "Ambiguous union type $typeName for $parent.$field - candidates " . implode(', ', $candidates) + ); + } + }; + } + $name = $typeConfig['name']; + switch ($name) { + case "Node": + $typeConfig['resolveType'] = function ($value, $context, ResolveInfo $info) { + // If it's a Node, it'll implement ServiceAPI, and thus we can use getGraphQLTypeName + $className = get_class($value); + if ($className === false) { + throw new MyRadioException('Tried to resolve a node that isn\'t a class!'); + } + $rc = new ReflectionClass($className); + if (!($rc->isSubclassOf(ServiceAPI::class))) { + throw new MyRadioException("Tried to resolve $className through Node, but it's not a ServiceAPI"); + } + $typeName = $className::getGraphQLTypeName(); + return $info->schema->getType($typeName); + }; + break; + case "Query": + // Gets special handling + $typeConfig['fields'] = function () use ($typeConfig) { + $orig = is_callable($typeConfig['fields']) ? $typeConfig['fields']() : $typeConfig['fields']; + return array_merge_recursive( + $orig, + [ + 'node' => [ + 'resolve' => function ($value, $args, GraphQLContext $context, ResolveInfo $info) { + $id_val = base64_decode($args['id']); + list($type, $id) = explode('#', $id_val); + if ($type[0] !== '\\') { + $type = '\\' . $type; + } + $rc = new ReflectionClass($type); + if (!($rc->isSubclassOf(ServiceAPI::class))) { + throw new MyRadioException( + "Tried to resolve node $type#$id but it's not a ServiceAPI" + ); + } + // Node resolution checks authorisation for $type::toDataSource + if (GraphQLUtils::isAuthorisedToAccess($info, $type, 'toDataSource')) { + return $type::getInstance($id); + } else { + return GraphQLUtils::returnNullOrThrowForbiddenException($info); + } + } + ] + ] + ); + }; + + $typeConfig['resolveField'] = function ($source, $args, GraphQLContext $context, ResolveInfo $info) { + $fieldName = $info->fieldName; + // If we're on the Query type, we're entering the graph, so we'll want a static method. + // Unlike elsewhere in the graph, we can assume everything on Query will have an @bind. + $bindDirective = GraphQLUtils::getDirectiveByName($info, 'bind'); + if (!$bindDirective) { + throw new MyRadioException("Tried to resolve $fieldName on Query but it didn't have an @bind"); + } + $bindArgs = GraphQLUtils::getDirectiveArguments($bindDirective); + if (isset($bindArgs['class'])) { + // we know class is a string + /** @noinspection PhpPossiblePolymorphicInvocationInspection */ + $className = $bindArgs['class']->value; + } else { + throw new MyRadioException( + "Tried to resolve $fieldName on Query but its @bind didn't have a class" + ); + } + if (isset($bindArgs['method'])) { + $methodName = $bindArgs['method']->value; + } else { + throw new MyRadioException( + "Tried to resolve $fieldName on Query but its @bind didn't have a method" + ); + } + // Wonderful! + $clazz = new ReflectionClass($className); + $meth = $clazz->getMethod($methodName); + if (GraphQLUtils::isAuthorisedToAccess($info, $className, $methodName)) { + return GraphQLUtils::processScalarIfNecessary( + $info, + GraphQLUtils::invokeNamed($meth, null, $args) + ); + } else { + return GraphQLUtils::returnNullOrThrowForbiddenException($info); + } + }; + break; + case "Mutation": + throw new MyRadioException('Mutations not supported'); + } + return $typeConfig; +}; + +$schema = BuildSchema::build($schemaText, $typeConfigDecorator); + +if (isset($_SERVER['CONTENT_TYPE']) && strpos($_SERVER['CONTENT_TYPE'], 'application/json') !== false) { + $raw = file_get_contents('php://input') ?: ''; + $data = json_decode($raw, true) ?: []; +} else { + $data = $_REQUEST; +} + +function graphQlResolver($source, $args, GraphQLContext $context, ResolveInfo $info) +{ + $typeName = $info->parentType->name; + $fieldName = $info->fieldName; + // First up, check if we have a bind directive + $bindDirective = GraphQLUtils::getDirectiveByName($info, 'bind'); + if ($bindDirective) { + $bindArgs = GraphQLUtils::getDirectiveArguments($bindDirective); + // If it has a method set, use that. It'll override the rest of method resolution, even if it doesn't exist. + if (isset($bindArgs['method'])) { + // we know method is a string + /** @noinspection PhpPossiblePolymorphicInvocationInspection */ + $methodName = $bindArgs['method']->value; + } + } + // Okay, we're in the Wild West. We're on an object and we need to get a field. + // Before we start, check if the bind directive has a class - in that case, it's a static method + if (isset($bindArgs['class']) && isset($bindArgs['method'])) { + // It's a static method, short-circuit the rest of the resolver + $className = $bindArgs['class']->value; + $methodName = $bindArgs['method']->value; + + if (GraphQLUtils::isAuthorisedToAccess($info, get_class($source), $methodName, $source)) { + $meth = new ReflectionMethod($className, $methodName); + + if (isset($bindArgs['callingConvention'])) { + $callingConvention = $bindArgs['callingConvention']->value; + switch ($callingConvention) { + case 'FirstArgCurrentUser': + $val = $className::{$methodName}(MyRadio_User::getInstance()->getID()); + break; + case 'FirstArgCurrentObject': + // Find name of the first argument, set as the source, and pass in the rest to invokeNamed + $firstArg = $meth->getParameters()[0]; + $args[$firstArg->getName()] = $source; + $val = GraphQLUtils::invokeNamed($meth, null, $args); + break; + default: + throw new MyRadioException( + "Unsupported calling convention $callingConvention for static method" + ); + } + } else { + $val = GraphQLUtils::invokeNamed($meth, null, $args); + } + return GraphQLUtils::processScalarIfNecessary($info, $val); + } else { + $context->addWarning("Unauthorised to access $typeName::$fieldName"); + return GraphQLUtils::returnNullOrThrowForbiddenException($info); + } + } + // Next, check if we're on an array + if (is_array($source)) { + if (array_key_exists($fieldName, $source)) { + if (GraphQLUtils::isAuthorisedToAccess($info, null, null)) { + return GraphQLUtils::processScalarIfNecessary($info, $source[$fieldName]); + } else { + $context->addWarning("Unauthorised to access $typeName::$fieldName"); + return GraphQLUtils::returnNullOrThrowForbiddenException($info); + } + } else { + // We're on an array, but the key we're looking for doesn't exist. No hope of doing the rest + // of the checks, for fear of returning the array itself. + // Do an authz check just for the warning, but return null. + if (GraphQLUtils::isAuthorisedToAccess($info, null, null)) { + return null; + } else { + $context->addWarning("Unauthorised to access $typeName::$fieldName"); + return GraphQLUtils::returnNullOrThrowForbiddenException($info); + } + } + } + // Next, check if it's an object + if (is_object($source)) { + // Before we move on, check if we're getting `id` on a `Node`. This merits special handling. + if ($fieldName === "id") { + /** @noinspection PhpParamsInspection - we know `Node` is an interface */ + if ($info->parentType->implementsInterface( + $info->schema->getType("Node") + )) { + $clazz = get_class($source); + // If we've used @bind on the ID, use that method. Otherwise, assume $source->getID() exists. + // Also, ID methods can't take any arguments, by dint of the Node spec + $id = null; + if (isset($methodName) && method_exists($source, $methodName)) { + $id = $source->{$methodName}(); + } elseif (method_exists($source, "getID")) { + $id = $source->getID(); + } else { + throw new MyRadioException("Couldn't resolve ID for type $clazz"); + } + // Not done yet. Remember, GraphQL IDs have to be unique + // We combine it with the class name and base64encode it + // Also note that `id` is bypassed from authorization, as it's controlled by access to the parent object + return base64_encode($clazz . '#' . strval($id)); + } + } + // Now, check if it's a meta field, as given by the @meta directive + $metaDirective = GraphQLUtils::getDirectiveByName($info, "meta"); + if ($metaDirective !== null) { + $metaArgs = GraphQLUtils::getDirectiveArguments($metaDirective); + // Authorization for metadata is the same as toDataSource + // TODO is this really the best way + if (GraphQLUtils::isAuthorisedToAccess($info, get_class($source), "toDataSource", $source)) { + // This Is Fine. + /** @noinspection PhpPossiblePolymorphicInvocationInspection */ + /** @noinspection PhpUndefinedMethodInspection */ + return $source->getMeta($metaArgs['key']->value); + } else { + $context->addWarning("Unauthorised to access $typeName::$fieldName"); + return GraphQLUtils::returnNullOrThrowForbiddenException($info); + } + } + // At this point, we check the method given by @bind again. + if (isset($methodName) && method_exists($source, $methodName)) { + // Yipee! + } else { + // Right, nothing there. + // Check on the method directly + if (method_exists($source, $fieldName)) { + $methodName = $fieldName; + } else { + // Try making it into a getter + $getterName = 'get' . strtoupper($fieldName[0]) . substr($fieldName, 1); + if (method_exists($source, $getterName)) { + $methodName = $getterName; + } + } + } + // Okay. Have we tracked down a method? + if (isset($methodName)) { + // Great. Can we access it? + if (GraphQLUtils::isAuthorisedToAccess($info, get_class($source), $methodName, $source)) { + // Yay. Call it! + // (We'll get a ReflectionException here if it's inaccessible. But That's Okay. + $meth = new ReflectionMethod( + // Assume we know what we're doing. + get_class($source), + $methodName + ); + // First, though, check if we should be using a calling convention + if (isset($bindArgs['callingConvention'])) { + $callingConvention = $bindArgs['callingConvention']->value; + switch ($callingConvention) { + case 'FirstArgCurrentUser': + $val = $source->{$methodName}(MyRadio_User::getInstance()->getID()); + break; + case 'FirstArgCurrentObject': + // Find name of the first argument, set as the source, and pass in the rest to invokeNamed + $firstArg = $meth->getParameters()[0]; + $args[$firstArg->getName()] = $source; + $val = GraphQLUtils::invokeNamed($meth, $source, $args); + break; + default: + throw new MyRadioException( + "Unsupported calling convention $callingConvention for dynamic field" + ); + } + } else { + $val = GraphQLUtils::invokeNamed($meth, $source, $args); + } + return GraphQLUtils::processScalarIfNecessary($info, $val); + } else { + // So the method exists, but we can't access it. + $context->addWarning("Unauthorised to access $typeName::$fieldName"); + return GraphQLUtils::returnNullOrThrowForbiddenException($info); + } + } + // Giving up on methods. Last shot: is it a property? + if (isset($source->{$fieldName})) { + // Right. Can we access it? + if (GraphQLUtils::isAuthorisedToAccess($info, get_class($source), $fieldName, $source)) { + return GraphQLUtils::processScalarIfNecessary($info, $source->{$fieldName}); + } else { + $context->addWarning("Unauthorised to access $typeName::$fieldName"); + return GraphQLUtils::returnNullOrThrowForbiddenException($info); + } + } + // Darn. + throw new MyRadioException("Couldn't track down a resolution for $fieldName"); + } + + // It's probably a scalar. Return it directly. + // Check authz just in case it's overridden + if (GraphQLUtils::isAuthorisedToAccess($info, null, null, $source)) { + return GraphQLUtils::processScalarIfNecessary($info, $source); + } else { + $context->addWarning("Unauthorised to access $typeName::$fieldName"); + return GraphQLUtils::returnNullOrThrowForbiddenException($info); + } +} + +$ctx = new GraphQLContext(); + +try { + $queryResult = GraphQL::executeQuery( + $schema, + $data['query'], + null, + $ctx, + (array) $data['variables'], + $data['operationName'], + 'graphQlResolver' + ); + $result = $queryResult->toArray($debug); +} catch (Exception $e) { + $status = $e instanceof MyRadioException ? $e->getCode() : 500; + $result = [ + 'errors' => FormattedError::createFromException($e, $debug) + ]; +} + +$warnings = $ctx->getWarnings(); +if (count($warnings) > 0) { + $result['warnings'] = $warnings; +} + +$corsWhitelistOrigins = [ + 'https://ury.org.uk', + 'http://localhost:3000' +]; + +$origin = $_SERVER['HTTP_ORIGIN']; +if (in_array($origin, $corsWhitelistOrigins)) { + header("Access-Control-Allow-Origin: $origin"); + header("Access-Control-Allow-Credentials: true"); +} + +header('Content-Type: application/json', true, $status); +echo json_encode($result); diff --git a/src/Controllers/api/v1.php b/src/Controllers/api/v1.php new file mode 100644 index 000000000..c70ae76ec --- /dev/null +++ b/src/Controllers/api/v1.php @@ -0,0 +1,151 @@ +getMethod($method); +} catch (ReflectionException $e) { + api_error(404); +} + +/* + * If it's an OPTIONS request report what methods are allowed + * Otherwise, check they're using one of those methods + */ +$allowed_methods = MyRadio_Swagger::getOptionsAllow($methodReflection); +if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { + header('Allow: '.implode(', ', $allowed_methods)); + exit; +} elseif (!in_array($_SERVER['REQUEST_METHOD'], $allowed_methods)) { + header('Allow: '.implode(', ', $allowed_methods)); + api_error(405); +} + +/* + * Okay, the method exists. Does the given API key have access to it? + */ +if (!$api_key->canCall($classes[$class], $method)) { + api_error(403, 'Your API Key ('.$api_key->getID().') does not have access to this method.'); +} else { + /* + * Map the paramaters + */ + $args = []; + foreach ($methodReflection->getParameters() as $param) { + if (isset($_REQUEST[$param->getName()])) { + //If the param has a class hint, initialise the class, assuming the argument is an ID. + if ($param->getClass() !== null) { + try { + $hint = $param->getClass()->getName(); + $args[$param->getName()] = $hint::getInstance($_REQUEST[$param->getName()]); + } catch (MyRadioException $ex) { + api_error( + 400, + 'Parameter '.$param->getName().' got an invalid ID. Must be an ID for '.$param->getClass().'.' + ); + } + } else { + $args[$param->getName()] = $_REQUEST[$param->getName()]; + } + } elseif (!$param->isOptional()) { + //Uh-oh, required option missing + api_error(400, 'Parameter '.$param->getName().' is required but not provided.'); + } + } + + /* + * From here on out, return a happy error message. If something goes awry. + */ + try { + /* + * Okay, now if the method isn't static, then we need to initialise an object. + */ + if (!$methodReflection->isStatic()) { + if (method_exists($classes[$class], 'getInstance')) { + $object = $classes[$class]::getInstance($id); + } else { + $object = new $classes[$class]($id); + } + } else { + $object = null; + } + + /* + * Let's process the request! + */ + $result = invokeArgsNamed($methodReflection, $object, $args); + } catch (MyRadioException $e) { + api_error($e->getCode(), $e->getMessage()); + } + + header('Content-Type: application/json'); + + $data = $class === 'resources' ? $result : [ + 'status' => 'OK', + 'payload' => CoreUtils::dataSourceParser($result, $_REQUEST['mixins'] ?? []), + 'time' => sprintf('%f', $__start + microtime(true)), + ]; + + echo json_encode($data); +} diff --git a/src/Controllers/api/v2.php b/src/Controllers/api/v2.php new file mode 100644 index 000000000..5ec96caaf --- /dev/null +++ b/src/Controllers/api/v2.php @@ -0,0 +1,74 @@ + 'OK', + 'payload' => CoreUtils::dataSourceParser($response['content'], $response['mixins']), + 'time' => sprintf('%f', $__start + microtime(true)), + ]; +} catch (MyRadioException $e) { + header('HTTP/1.1 '.$e->getCode().' '.$e->getCodeName()); + header('Content-Type: application/json'); + $data = [ + 'status' => 'FAIL', + 'payload' => $e->getMessage(), + 'time' => sprintf('%f', $__start + microtime(true)), + ]; +} + +echo json_encode($data); diff --git a/src/Controllers/brokerVersion.php b/src/Controllers/brokerVersion.php deleted file mode 100644 index 2f3c6d7f7..000000000 --- a/src/Controllers/brokerVersion.php +++ /dev/null @@ -1,36 +0,0 @@ - - * @version 20130525 - * @package MyRadio_Core - * - * @uses $member - The current user - * - * Sets the $service_version Global Variable - */ - -// Get a list of Service Versions for this Service -$versions = CoreUtils::getServiceVersions(); - -// If the version selector has just been submitted, update the session -if (isset($_REQUEST['svc_version'])) { - $serviceid = Config::$service_id; - foreach ($versions as $version) { - if ($version['version'] === $_REQUEST['svc_version']) { - $_SESSION['myury_svc_version_'.$serviceid] = $version['version']; - $_SESSION['myury_svc_version_'.$serviceid.'_path'] = $version['path']; - $_SESSION['myury_svc_version_'.$serviceid.'_proxy_static'] = ($version['proxy_static'] === 't'); - break; - } - } - header('Location: ?service='.$_REQUEST['svc_name']); - exit; -} - -if (isset($_REQUEST['select_version'])) { - $service = $_REQUEST['select_version']; - require 'Views/MyRadio/brokerVersion.php'; - exit; -} \ No newline at end of file diff --git a/src/Controllers/cli_common.php b/src/Controllers/cli_common.php deleted file mode 100644 index d73b8d3db..000000000 --- a/src/Controllers/cli_common.php +++ /dev/null @@ -1,12 +0,0 @@ - - * @version 20130720 - * @package MyRadio_Deamon - * @uses \Database - * @uses \CoreUtils - * + * + * @uses \Database + * @uses \CoreUtils + * * @todo Make this not use echo in various Daemons * @todo Install the pcntl extension on thunderhorn */ +use \MyRadio\Database; +use \MyRadio\MyRadioException; +use \MyRadio\MyRadioError; +use \MyRadio\MyRadioEmail; +use \MyRadio\MyRadio\CoreUtils; + $log_level = 4; //0: Critical, 1: Important, 2: Run Process, 3: Info, 4: Debug -/** +/* * @todo Make paths nicer. This variable is used in MyRadio_Track directly. */ -$syspath = '/usr/local/bin/'; - -function dlog($x, $level = 3) { - if ($level == 0) { - //Write to stderr - $f = fopen('php://stderr', 'w'); - fwrite($f, $x); - fclose($f); - } - if ($GLOBALS['log_level'] >= $level) { - echo $x . "\n"; - } +$syspath = ''; + +$pidfile = '/var/run/myradio_daemon.pid'; + +function dlog($x, $level = 3) +{ + if ($level == 0) { + //Write to stderr + $f = fopen('php://stderr', 'w'); + fwrite($f, $x); + fclose($f); + } + if ($GLOBALS['log_level'] >= $level) { + echo $x."\n"; + } } //Gracefully handle stop requests -function signal_handler($signo) { - switch ($signo) { - case SIGTERM: - //Shutdown - dlog('Caught SIGTERM. Shutting down after this loop.', 1); - $GLOBALS['once'] = true; //This will kill after next iteration - } +function signal_handler($signo) +{ + switch ($signo) { + case SIGTERM: + //Shutdown + dlog('Caught SIGTERM. Shutting down after this loop.', 1); + $GLOBALS['once'] = true; //This will kill after next iteration + } +} + +$pid = getmypid(); +if (!file_put_contents($pidfile, "$pid\n")) { + die("Can't write pid file $pidfile\n"); } //Is the extension installed? if (function_exists('pcntl_signal')) { - pcntl_signal(SIGTERM, "signal_handler"); + pcntl_signal(SIGTERM, 'signal_handler'); } chdir(__DIR__); @@ -61,86 +73,91 @@ function signal_handler($signo) { $path = '../Classes/Daemons/'; $handle = opendir($path); if (!$handle) { - die('PATH DOES NOT EXIST ' . $path . "\n"); + die('PATH DOES NOT EXIST '.$path."\n"); } -$classes = array(); +$classes = []; -require_once 'cli_common.php'; +require_once 'root_cli.php'; //Should this run once or loop forever? $once = in_array('--once', $argv); //Load all classes that should be run while (false !== ($file = readdir($handle))) { - if ($file === '.' or $file === '..') { - continue; - } - //Is the file valid PHP? - system($syspath . 'php -l ' . $path . $file, $result); - if ($result !== 0) { - dlog('Not checking ' . $file . ' - Parse Error', 1); - } else { - require $path . $file; - $class = str_replace('.php', '', $file); - if (!class_exists($class)) { - echo dlog('Daemon does not exist -' . $class, 1); + if ($file === '.' or $file === '..') { + continue; + } + //Is the file valid PHP? + system(PHP_BINDIR."/php -l $path$file", $result); + if ($result !== 0) { + dlog('Not checking '.$file.' - Parse Error', 1); } else { - $classes[] = $class; + require $path.$file; + // TODO: php5.5 allows ClassName:class to remove this hack + $class = '\MyRadio\Daemons\\'.str_replace('.php', '', $file); + if (!class_exists($class)) { + echo dlog('Daemon does not exist - '.$class, 1); + } else { + $classes[] = $class; + } } - } } if (empty($classes)) { - dlog('No daemons to execute', 0); - exit; + dlog('No daemons to execute', 0); + exit; } //Run each while (true) { - foreach ($classes as $class) { - try { - if ($class::isEnabled()) { - dlog('Running ' . $class, 2); - $class::run(); - if (!$once) { - sleep(1); + foreach ($classes as $class) { + try { + if ($class::isEnabled()) { + dlog('Running '.$class, 2); + $class::run(); + if (!$once) { + sleep(1); + } + } + } catch (MyRadioException $e) { } - } - } catch (MyRadioException $e) { - } - } - - //Every once in a while, check database connection. If it's lost, routinely try to reconnect. - if (!Database::getInstance()->status()) { - dlog('CRITICAL: Database server connection lost. Attempting to reconnect...', 0); - $db_fail_start = time(); - while (!Database::getInstance()->reconnect()) { - if (time() - $db_fail_start > 900) { - //Connection has been lost for more than 15 minutes. Give up. - MyRadioEmail::sendEmailToComputing('[MyRadio] Background Service Failure', "MyRadio's connection to the Database Server has been lost. Attempts to reconnect for the last 15 minutes have proved futile, so the service has stopped.\r\n\Please investigate Database connectivity and restart the service one access is restored."); - } - dlog('FAILED! Will retry in 30 seconds.', 0); - sleep(30); + + //Every once in a while, check database connection. If it's lost, routinely try to reconnect. + if (!Database::getInstance()->status()) { + dlog('CRITICAL: Database server connection lost. Attempting to reconnect...', 0); + $db_fail_start = time(); + while (!Database::getInstance()->reconnect()) { + if (time() - $db_fail_start > 900) { + //Connection has been lost for more than 15 minutes. Give up. + MyRadioEmail::sendEmailToComputing( + '[MyRadio] Background Service Failure', + "MyRadio's connection to the Database Server has been lost. " + ."Attempts to reconnect for the last 15 minutes have proved futile, so the service has stopped.\r\n" + .'Please investigate Database connectivity and restart the service once access is restored.' + ); + } + dlog('FAILED! Will retry in 30 seconds.', 0); + sleep(30); + } + dlog('RECONNECTED', 0); + } + + if ($once) { + break; + } + + //At the end of an interation, commit a query and error count. + //This is both nice for statistics, and prevents an entry of several tens of thousands when the server restarts :) + try { + CoreUtils::shutdown(); + Database::getInstance()->resetCounter(); + MyRadioException::resetExceptionCount(); + } catch (MyRadioException $e) { } - dlog('RECONNECTED', 0); - } - - if ($once) { - break; - } - - //At the end of an interation, commit a query and error count. - //This is both nice for statistics, and prevents an entry of several tens of thousands when the server restarts :) - try { - CoreUtils::shutdown(); - Database::getInstance()->resetCounter(); - MyRadioException::resetExceptionCount(); - MyRadioError::resetErrorCount(); - } catch (MyRadioException $e) { - - } - - //Reload the configuration to see if it has changed - include 'MyRadio_Config.local.php'; -} \ No newline at end of file + + //Reload the configuration to see if it has changed + include 'MyRadio_Config.local.php'; +} + +unlink($pidfile); diff --git a/src/Controllers/email_pipe.php b/src/Controllers/email_pipe.php index b3d61dd66..94899016f 100755 --- a/src/Controllers/email_pipe.php +++ b/src/Controllers/email_pipe.php @@ -1,25 +1,29 @@ #!/usr/local/bin/php -q - * @version 20130712 - * @package MyRadio_Mail - * @uses \Database - * @uses \CoreUtils + * + * @uses \Database + * @uses \CoreUtils */ +use \MyRadio\MyRadioException; +use \MyRadio\ServiceAPI\MyRadio_User; +use \MyRadio\ServiceAPI\MyRadio_List; + define('SILENT_EXCEPTIONS', true); -ini_set("log_errors", 1); -ini_set("error_log", "/tmp/php-mailparser-error.log"); -ini_set('display_errors','Off'); -require_once __DIR__.'/cli_common.php'; -ini_set('display_errors','Off'); +ini_set('log_errors', 1); +ini_set('error_log', '/tmp/php-mailparser-error.log'); +ini_set('display_errors', 'Off'); + +require_once __DIR__.'/root_cli.php'; +ini_set('display_errors', 'Off'); -set_exception_handler(function() {exit(0);}); //We do not want bounce messages from this! +set_exception_handler(function () { + exit(0); +}); //We do not want bounce messages from this! //Read in email $fd = fopen('php://stdin', 'r'); @@ -30,45 +34,45 @@ preg_match_all('/(^|\s)From:(.*)/i', $email, $sender); preg_match_all('/(^|\s)(To|CC):(.*)/i', $email, $recipients); -preg_match('/(^|\s)X\-Spam\-Status:(.*)/i', $email, $spam); -if (!empty($spam) && strtolower(trim($spam[2])) == 'yes') { - //Don't archive spam. - exit(0); +preg_match('/(^|\s)X\-Spam_action:(.*)/i', $email, $spam); +if (!empty($spam) && strtolower(trim($spam[2])) == 'reject') { + //Don't archive spam. + exit(0); } fclose($fd); if (!isset($sender[2][0])) { - $sender = null; + $sender = null; } else { - if (strstr($sender[2][0],'<') !== false) { - $addr = preg_replace('/.*<(.*)>.*/', '$1', $sender[2][0]); - } else { - $addr = trim($sender[2][0]); - } - $sender = MyRadio_User::findByEmail($addr); + if (strstr($sender[2][0], '<') !== false) { + $addr = preg_replace('/.*<(.*)>.*/', '$1', $sender[2][0]); + } else { + $addr = trim($sender[2][0]); + } + $sender = MyRadio_User::findByEmail($addr); } foreach ($recipients[3] as $recipient) { - if (strstr($recipient,'<') !== false) { - $addr = preg_replace('/.*<(.*)>.*/', '$1', $recipient); - } else { - $addr = trim($recipient); - } - - $list = MyRadio_List::getByName(explode('@',$addr)[0]); - if (empty($list)) { - exit(0); - } - if ($list->getID() == 52 && $sender == null) { - continue; //Prevent loops - } - if ($list !== null) { - try { - $list->archiveMessage($sender, $email); - } catch (MyRadioException $e) { - //Yes, it failed, but we don't want bounce messages - exit(0); + if (strstr($recipient, '<') !== false) { + $addr = preg_replace('/.*<(.*)>.*/', '$1', $recipient); + } else { + $addr = trim($recipient); + } + + $list = MyRadio_List::getByName(explode('@', $addr)[0]); + if (empty($list)) { + exit(0); + } + if ($list->getID() == 52 && $sender == null) { + continue; //Prevent loops + } + if ($list !== null) { + try { + $list->archiveMessage($sender, $email); + } catch (MyRadioException $e) { + //Yes, it failed, but we don't want bounce messages + exit(0); + } } - } } diff --git a/src/Controllers/iTones/allPlaylists.php b/src/Controllers/iTones/allPlaylists.php new file mode 100644 index 000000000..d1e97d04a --- /dev/null +++ b/src/Controllers/iTones/allPlaylists.php @@ -0,0 +1,17 @@ +setTemplate('table.twig') + ->addVariable('title', 'All Playlists') + ->addVariable( + 'tabledata', + CoreUtils::dataSourceParser(iTones_Playlist::getAlliTonesPlaylists($includeArchived = true)) + ) + ->addVariable('tablescript', 'myradio.iTones.allPlaylists') + ->render(); diff --git a/src/Controllers/iTones/configurePlaylist.php b/src/Controllers/iTones/configurePlaylist.php new file mode 100644 index 000000000..05cb48577 --- /dev/null +++ b/src/Controllers/iTones/configurePlaylist.php @@ -0,0 +1,59 @@ +readValues(); + + if (empty($data['id'])) { + //Create + $playlist = iTones_Playlist::create($data['title'], $data['description'], $data['category'], $data['archived']); + URLUtils::redirect( + 'iTones', + 'configurePlaylist', + [ + 'playlistid' => $playlist->getID(), + 'message' => base64_encode('The playlist has been created.'), + ] + ); + } else { + //Edit + $playlist = iTones_Playlist::getInstance($data['id']); + + $playlist->setTitle($data['title']); + $playlist->setDescription($data['description']); + $playlist->setCategoryById($data['category']); + $playlist->setArchived($data['archived']); + + URLUtils::backWithMessage('The playlist has been updated.'); + } +} else { + //Not Submitted + if (empty($_REQUEST['playlistid'])) { + //Create + $playlist = iTones_Playlist::getForm() + ->setTemplate('iTones/configurePlaylist.twig') + ->render(); + } else { + //Update + $playlist = iTones_Playlist::getInstance($_REQUEST['playlistid']); + $playlist->getEditForm() + ->setTemplate('iTones/configurePlaylist.twig') + ->render( + [ + 'tabledata' => CoreUtils::dataSourceParser( + iTones_PlaylistAvailability::getAvailabilitiesForPlaylist($playlist->getID()) + ), + 'playlistid' => $_REQUEST['playlistid'], + ] + ); + } +} diff --git a/src/Controllers/iTones/default.php b/src/Controllers/iTones/default.php index e5791eadc..ac40e70b6 100644 --- a/src/Controllers/iTones/default.php +++ b/src/Controllers/iTones/default.php @@ -1,10 +1,10 @@ - * @version 20130712 - * @package MyRadio_iTones + * Landing page for iTones. */ +use \MyRadio\MyRadio\CoreUtils; -CoreUtils::getTemplateObject()->setTemplate('iTones/default.twig')->addVariable('title', 'Campus Jukebox Manager')->render(); \ No newline at end of file +CoreUtils::getTemplateObject() + ->setTemplate('iTones/default.twig') + ->addVariable('title', 'Campus Jukebox Manager') + ->render(); diff --git a/src/Controllers/iTones/doEditPlaylist.php b/src/Controllers/iTones/doEditPlaylist.php deleted file mode 100644 index c29881da0..000000000 --- a/src/Controllers/iTones/doEditPlaylist.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @version 20130712 - * @package MyRadio_iTones - */ - -require 'Models/iTones/editplaylistfrm.php'; - -$data = $form->readValues(); - -if (empty($data['playlistid'])) { - throw new MyRadioException('No Playlist ID provided.', 400); -} - -$playlist = iTones_Playlist::getInstance($data['playlistid']); - -if ($playlist->validateLock($_SESSION['itones_lock_'.$playlist->getID()]) === false) { - CoreUtils::getTemplateObject() - ->setTemplate('error.twig') - ->addVariable('body', 'You do not have a valid lock for this playlist or the lock has expired.') - ->render(); -} else { - $playlist->setTracks($data['tracks']['track'], $_SESSION['itones_lock_'.$playlist->getID()], $data['notes']); - - $playlist->releaseLock($_SESSION['itones_lock_'.$playlist->getID()]); - - CoreUtils::backWithMessage('The playlist has been updated.'); -} \ No newline at end of file diff --git a/src/Controllers/iTones/doRequestTrack.php b/src/Controllers/iTones/doRequestTrack.php deleted file mode 100644 index f381fbf36..000000000 --- a/src/Controllers/iTones/doRequestTrack.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @author Matt Windsor - * @version 20140112 - * @package MyRadio_iTones - */ - -$data = MyRadio_JsonFormLoader::loadFromModule( - $module, - 'requesttrackfrm', - 'doRequestTrack', - [ 'remaining_requests' => iTones_Utils::getRemainingRequests() - ] -)->readValues(); - -$success = iTones_Utils::requestTrack($data['track']); -if ($success === true) { - $message = 'Track request submitted.'; -} else { - $message = 'Sorry, but this track cannot be requested right now.' - . ' Please try again later.'; -} - -CoreUtils::backWithMessage($message); diff --git a/src/Controllers/iTones/editAvailability.php b/src/Controllers/iTones/editAvailability.php new file mode 100644 index 000000000..f02af9a7f --- /dev/null +++ b/src/Controllers/iTones/editAvailability.php @@ -0,0 +1,60 @@ +readValues(); + + if (empty($data['id'])) { + //Create + $availability = iTones_PlaylistAvailability::create( + iTones_Playlist::getInstance($data['playlistid']), + $data['weight'], + $data['effective_from'], + $data['effective_to'], + $data['timeslots'] + ); + + URLUtils::redirect( + 'iTones', + 'editAvailability', + [ + 'availabilityid' => $availability->getID(), + 'message' => base64_encode('The availability has been created.'), + ] + ); + } else { + //Update + $availability = iTones_PlaylistAvailability::getInstance($data['id']); + + $availability->setEffectiveFrom($data['effective_from']); + $availability->setEffectiveTo($data['effective_to']); + $availability->setWeight($data['weight']); + $availability->clearTimeslots(); + foreach ($data['timeslots'] as $timeslot) { + $availability->addTimeslot($timeslot['day'], $timeslot['start_time'], $timeslot['end_time']); + } + + URLUtils::backWithMessage('The availability has been updated.'); + } +} elseif (!empty($_REQUEST['availabilityid'])) { + //Not Submitted, update + $availability = iTones_PlaylistAvailability::getInstance($_REQUEST['availabilityid']); + + $availability->getEditForm()//->setTemplate('iTones/editAvailability.twig') + ->render(); +} else { + //Not Submitted, create + if (empty($_REQUEST['playlistid'])) { + throw new MyRadioException('No Playlist ID provided.', 400); + } + + iTones_PlaylistAvailability::getForm($_REQUEST['playlistid']) + ->render(); +} diff --git a/src/Controllers/iTones/editPlaylist.php b/src/Controllers/iTones/editPlaylist.php index 5e9dd8193..6d3fed3df 100644 --- a/src/Controllers/iTones/editPlaylist.php +++ b/src/Controllers/iTones/editPlaylist.php @@ -1,39 +1,62 @@ - * @version 20130712 - * @package MyRadio_iTones + * Allows a User to edit the tracks in an iTones Playlist. */ +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\iTones\iTones_Playlist; -if (empty($_REQUEST['playlistid'])) throw new MyRadioException('No Playlist ID provided.', 400); +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = iTones_Playlist::getTracksForm()->readValues(); -$playlist = iTones_Playlist::getInstance($_REQUEST['playlistid']); + if (empty($data['id'])) { + throw new MyRadioException('No Playlist ID provided.', 400); + } + + $playlist = iTones_Playlist::getInstance($data['id']); + + if ($playlist->validateLock($_SESSION['itones_lock_'.$playlist->getID()]) === false) { + CoreUtils::getTemplateObject() + ->setTemplate('error.twig') + ->addVariable('body', 'You do not have a valid lock for this playlist or the lock has expired.') + ->render(); + } else { + $playlist->setTracks( + $data['tracks']['track'], + $_SESSION['itones_lock_'.$playlist->getID()], + $data['notes'] + ); -$lock = $playlist->acquireOrRenewLock(empty($_SESSION['itones_lock_'.$playlist->getID()]) - ? null : $_SESSION['itones_lock_'.$playlist->getID()]); + $playlist->releaseLock( + $_SESSION['itones_lock_'.$playlist->getID()] + ); -if ($lock === false) { - CoreUtils::getTemplateObject() - ->setTemplate('error.twig') - ->addVariable('body', 'Sorry, this playlist is currently being edited by someone else.') - ->render(); + URLUtils::backWithMessage('The playlist has been updated.'); + } } else { - $_SESSION['itones_lock_'.$playlist->getID()] = $lock; - //The Form definition - require 'Models/iTones/editplaylistfrm.php'; - - $tracks = $playlist->getTracks(); - $artists = array(); - foreach ($tracks as $track) { - if ($track instanceof MyRadio_Track) { - $artists[] = $track->getArtist(); + //Not Submitted + if (empty($_REQUEST['playlistid'])) { + throw new MyRadioException('No Playlist ID provided.', 400); + } + + $playlist = iTones_Playlist::getInstance($_REQUEST['playlistid']); + + $lock = $playlist->acquireOrRenewLock( + empty($_SESSION['itones_lock_'.$playlist->getID()]) + ? null : $_SESSION['itones_lock_'.$playlist->getID()] + ); + + if ($lock === false) { + CoreUtils::getTemplateObject() + ->setTemplate('error.twig') + ->addVariable('body', 'Sorry, this playlist is currently being edited by someone else.') + ->render(); + } else { + $_SESSION['itones_lock_'.$playlist->getID()] = $lock; + + $playlist->getTracksEditForm()->setTemplate('iTones/editPlaylist.twig') + ->render(); } - } - $form->setTemplate('iTones/editPlaylist.twig') - ->setFieldValue('tracks.track', $tracks) - ->setFieldValue('tracks.artist', $artists) - ->setFieldValue('playlistid', $playlist->getID()) - ->render(); -} \ No newline at end of file +} diff --git a/src/Controllers/iTones/listPlaylists.php b/src/Controllers/iTones/listPlaylists.php index ef38edd03..4fbcb1cfc 100644 --- a/src/Controllers/iTones/listPlaylists.php +++ b/src/Controllers/iTones/listPlaylists.php @@ -1,14 +1,22 @@ - * @version 20130712 - * @package MyRadio_iTones + * List of iTones_Playlists. */ -CoreUtils::getTemplateObject()->setTemplate('table.twig') +use MyRadio\iTones\iTones_PlaylistCategory; +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\iTones\iTones_Playlist; + +if (isset($_GET['category'])) { + $category = (int) $_GET['category']; +} else { + $category = 1; +} + +CoreUtils::getTemplateObject()->setTemplate('iTones/listPlaylists.twig') ->addVariable('title', 'Campus Jukebox Playlists') - ->addVariable('tabledata', CoreUtils::dataSourceParser(iTones_Playlist::getAlliTonesPlaylists())) - ->addVariable('tablescript', 'myury.datatable.default') - ->render(); \ No newline at end of file + ->addVariable('tabledata', CoreUtils::dataSourceParser(iTones_Playlist::getAllPlaylistsOfCategory($category))) + ->addVariable('tablescript', 'myradio.iTones.listPlaylists') + ->addVariable('category', $category) + ->addVariable('allCategories', iTones_PlaylistCategory::getAll()) + ->render(); diff --git a/src/Controllers/iTones/refreshLock.php b/src/Controllers/iTones/refreshLock.php index 27040189b..a0712017a 100644 --- a/src/Controllers/iTones/refreshLock.php +++ b/src/Controllers/iTones/refreshLock.php @@ -1,24 +1,27 @@ - * @version 20130712 - * @package MyRadio_iTones + * Refreshes a lock on a playlist to prevent it expiring. */ +use \MyRadio\MyRadioException; +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\iTones\iTones_Playlist; -if (empty($_REQUEST['playlistid'])) throw new MyRadioException('No Playlist ID provided.', 400); +if (empty($_REQUEST['playlistid'])) { + throw new MyRadioException('No Playlist ID provided.', 400); +} $playlist = iTones_Playlist::getInstance($_REQUEST['playlistid']); -$lock = $playlist->acquireOrRenewLock(empty($_SESSION['itones_lock_'.$playlist->getID()]) - ? null : $_SESSION['itones_lock_'.$playlist->getID()]); +$lock = $playlist->acquireOrRenewLock( + empty($_SESSION['itones_lock_'.$playlist->getID()]) + ? null : $_SESSION['itones_lock_'.$playlist->getID()] +); if ($lock === false) { - $data = array('FAIL','Locked for editing by another user'); + $data = ['FAIL', 'Locked for editing by another user']; } else { - $_SESSION['itones_lock_'.$playlist->getID()] = $lock; - $data = array('SUCCESS', $lock); + $_SESSION['itones_lock_'.$playlist->getID()] = $lock; + $data = ['SUCCESS', $lock]; } -require_once 'Views/MyRadio/datatojson.php'; \ No newline at end of file +URLUtils::dataToJSON($data); diff --git a/src/Controllers/iTones/requestTrack.php b/src/Controllers/iTones/requestTrack.php index c76c6b2fe..66386c66d 100644 --- a/src/Controllers/iTones/requestTrack.php +++ b/src/Controllers/iTones/requestTrack.php @@ -1,18 +1,60 @@ - * @author Matt Windsor - * @version 20140112 - * @package MyRadio_iTones + * Allows a User to request a track on the jukebox. */ +use \MyRadio\MyRadio\URLUtils; +use \MyRadio\MyRadio\MyRadioForm; +use \MyRadio\MyRadio\MyRadioFormField; +use \MyRadio\iTones\iTones_Utils; -MyRadio_JsonFormLoader::loadFromModule( - $module, - 'requesttrackfrm', - 'doRequestTrack', - [ 'remaining_requests' => iTones_Utils::getRemainingRequests() - ] -)->render(); +$form = ( + new MyRadioForm( + 'itones_trackrequest', + $module, + $action, + [ + 'debug' => true, + 'title' => 'Request Campus Jukebox Track', + ] + ) +)->addField( + new MyRadioFormField( + 'track', + MyRadioFormField::TYPE_TRACK, + [ + 'explanation' => 'Enter a track here to request it on the Jukebox.', + 'label' => 'Track', + ] + ) +)->addField( + new MyRadioFormField( + 'requests', + MyRadioFormField::TYPE_NUMBER, + [ + 'explanation' => 'This is the number of requests you can make at the moment. ' + .'If you run out of requests, please wait a while and try again.', + 'label' => 'Remaining Requests', + 'value' => iTones_Utils::getRemainingRequests(), + 'enabled' => false, + 'required' => false, + ] + ) +); + +if ($_SERVER['REQUEST_METHOD'] === 'POST') { + //Submitted + $data = $form->readValues(); + + $success = iTones_Utils::requestTrack($data['track']); + if ($success === true) { + $message = 'Track request submitted.'; + } else { + $message = 'Sorry, but this track cannot be requested right now. Please try again later.'; + } + + URLUtils::backWithMessage($message); +} else { + //Not Submitted + $form->render(); +} diff --git a/src/Controllers/iTones/viewPlaylistHistory.php b/src/Controllers/iTones/viewPlaylistHistory.php index 522cdd992..4543ff621 100644 --- a/src/Controllers/iTones/viewPlaylistHistory.php +++ b/src/Controllers/iTones/viewPlaylistHistory.php @@ -1,14 +1,15 @@ - * @version 20130714 - * @package MyRadio_iTones + * List of iTones_Playlists. */ +use \MyRadio\MyRadio\CoreUtils; +use \MyRadio\iTones\iTones_PlaylistRevision; CoreUtils::getTemplateObject()->setTemplate('table.twig') ->addVariable('title', 'Playlist History') - ->addVariable('tabledata', CoreUtils::dataSourceParser(iTones_PlaylistRevision::getAllRevisions($_REQUEST['playlistid']))) - ->addVariable('tablescript', 'myury.datatable.default') - ->render(); \ No newline at end of file + ->addVariable( + 'tabledata', + CoreUtils::dataSourceParser(iTones_PlaylistRevision::getAllRevisions($_REQUEST['playlistid'])) + ) + ->addVariable('tablescript', 'myradio.datatable.default') + ->render(); diff --git a/src/Controllers/jukebox_sched.php b/src/Controllers/jukebox_sched.php index 3650a30a0..0e3854fae 100755 --- a/src/Controllers/jukebox_sched.php +++ b/src/Controllers/jukebox_sched.php @@ -1,30 +1,17 @@ #!/usr/bin/php - * @version 20130712 - * @package MyRadio_iTones - * @uses \Database - * @uses \CoreUtils + * This is the Jukebox Scheduler Controller - when triggered, it will inject a track into the iTones playout queue. + * + * @uses \Database + * @uses \CoreUtils */ -require_once __DIR__.'/cli_common.php'; +use \MyRadio\iTones\iTones_Utils; +use \MyRadio\iTones\iTones_Playlist; +use \MyRadio\ServiceAPI\MyRadio_TracklistItem; -do { - $tracks = null; - //Pick a playlist at random, until we find one that actually has tracks - while (empty($tracks)) { - $playlist = iTones_Playlist::getPlaylistFromWeights(); - $tracks = $playlist->getTracks(); - } - //Pick a track at random from the playlist - $track = $tracks[array_rand($tracks)]; - - //If this track has been played recently or is currently queued, we can't play it. Try again. - } while ($track->getClean() === 'n' or - (MyRadio_TracklistItem::getIfPlayedRecently($track) or iTones_Utils::getIfQueued($track) - or !MyRadio_TracklistItem::getIfAlbumArtistCompliant($track)) - or $track->isBlacklisted()); - -echo $track->getPath()."\n"; +require_once __DIR__.'/root_cli.php'; + +$track = iTones_Utils::getTrackForJukebox(); + +echo $track->getPath() . "\n"; diff --git a/src/Controllers/root.php b/src/Controllers/root.php index febc7b1a7..afd0adef0 100644 --- a/src/Controllers/root.php +++ b/src/Controllers/root.php @@ -1,131 +1,125 @@ - * @version 20131230 - * @package MyRadio_Core - * @uses \CacheProvider - * @uses \Database - * @uses \CoreUtils + * This is the Root Controller - it is the backbone of everything MyRadio. */ -/** +use \MyRadio\Config; +use \MyRadio\ServiceAPI\ServiceAPI; +use \MyRadio\MyRadio\AuthUtils; +use \MyRadio\MyRadio\MyRadioSession; +use \MyRadio\MyRadio\MyRadioNullSession; + +/* + * This number is incremented every time a database patch is released. + * Patches are scripts in schema/patches. + */ +define('MYRADIO_CURRENT_SCHEMA_VERSION', 18); + +/* * Turn on Error Reporting for the start. Once the Config object is loaded * this is updated to reflect Config. */ -error_reporting(E_ALL ^ E_STRICT); +error_reporting(E_ALL); ini_set('display_errors', 'On'); -/** +/* * Set the Default Timezone. * Once Config is available, this value should be used instead. */ -date_default_timezone_get('Europe/London'); -/** +date_default_timezone_set('Europe/London'); +/* * Sets the include path to include MyRadio at the end - makes for nicer includes */ -ini_set('include_path', str_replace('Controllers', '', __DIR__) . ':' . ini_get('include_path')); +set_include_path(str_replace('Controllers', '', __DIR__).PATH_SEPARATOR.get_include_path()); /** - * The CoreUtils static class provides some useful standard functions for MyRadio. Take a look at it before you start - * developing - it may just save you some head scratching. - */ -require_once 'Classes/MyRadio/CoreUtils.php'; -/** - * Load up the general Configurables - this includes things like the Database connection settings, the CacheProvider - * to use and whether debug mode is enabled. + * Sets up the autoloader for all MyRadio classes. */ -require_once 'Classes/Config.php'; -require_once 'MyRadio_Config.local.php'; +require_once 'Classes/Autoloader.php'; +// instantiate the loader +$loader = new \MyRadio\Autoloader(); +// register the autoloader +$loader->register(); +// register the base directories for the namespace prefix +$_basepath = str_replace('Controllers', '', __DIR__).DIRECTORY_SEPARATOR; -/** - * Call the model that prepares the Database and the Global Abstraction API - */ -require 'Models/Core/api.php'; +$loader->addNamespace('MyRadio', $_basepath.'Classes'); +$loader->addNamespace('MyRadio\Iface', $_basepath.'Interfaces'); -/** - * Load in email functions - */ -require_once 'Classes/MyRadioEmail.php'; +unset($_basepath); /** - * Load the phpError handler class - this has functions to put errors nicely on - * the page, or to log them elsewhere. - * And set the error handler to use it + * Sets up the autoloader for composer. */ -require_once 'Classes/MyRadioError.php'; -set_error_handler('MyRadioError::errorsToArray'); +require 'vendor/autoload.php'; -/** - * Turn off visible error reporting, if needed - * 269 is AUTH_SHOWERRORS - the constants aren't initialised yet +/* + * Load configuration specific to this system. + * Or, if it doesn't exist, kick into setup. */ -if (!Config::$display_errors && !CoreUtils::hasPermission(269)) { - ini_set('display_errors', 'Off'); +if (stream_resolve_include_path('MyRadio_Config.local.php') && file_exists(stream_resolve_include_path('MyRadio_Config.local.php'))) { + require_once 'MyRadio_Config.local.php'; + if (Config::$setup === true) { + require 'Controllers/Setup/root.php'; + exit; + } +} else { + /** + * This install hasn't been configured yet. We should do that. + */ + require 'Controllers/Setup/root.php'; + exit; } -ini_set('error_log', Config::$log_file); // Set error log file -date_default_timezone_set(Config::$timezone); //Set timezone -/** - * The Service Broker decides what version of a Service the user has access to. This includes MyRadio, so gets added - * here. - * @todo Discuss or document the parts of MyRadio core that cannot be brokered, see if this can be moved earlier - */ -require_once 'Controllers/service_broker.php'; -/** - * Load configuration specific to this Version. - */ -require_once 'MyRadio_Config.local.php'; +set_error_handler('\MyRadio\MyRadioError::errorsToArray'); +set_exception_handler( + function ($e) { + if (method_exists($e, 'uncaught')) { + $e->uncaught(); + } else { + echo 'This information is not available at the moment. Please try again later.'; + print_r($e); + } + } +); -/** - * Set up the Module and Action global variables. These are used by Module/Action controllers as well as this file. - * Notice how the default Module is MyRadio. This is basically the MyRadio Menu, and maybe a couple of admin pages. - * Notice how the default Action is 'default'. This means that the "default" Controller should exist for all Modules. - * The top half deals with Rewritten URLs, which get mapped to ?request= +// Set error log file +ini_set('error_log', Config::$log_file); + +//Wake up ServiceAPI if it isn't already +//Otherwise ServiceAPI::$db/$cache may not be available and upset controllers +ServiceAPI::wakeup(); + +//Initialise the permission constants +AuthUtils::setUpAuth(); + +/* + * Turn off visible error reporting, if needed + * must come after AuthUtils::setUpAuth() */ -if (isset($_REQUEST['request'])) { - $info = explode('/', $_REQUEST['request']); - //If both are defined, it's Module/Action - if (!empty($info[1])) { - $module = $info[0]; - $action = $info[1]; - //If there's only one, determine if it's the module or action - } elseif (CoreUtils::isValidController(Config::$default_module, $info[0])) { - $module = Config::$default_module; - $action = $info[0]; - } elseif (CoreUtils::isValidController($info[0], Config::$default_action)) { - $module = $info[0]; - $action = Config::$default_action; - } else { - require 'Controllers/Errors/404.php'; - exit; - } -} else { - $module = (isset($_REQUEST['module']) ? $_REQUEST['module'] : Config::$default_module); - $action = (isset($_REQUEST['action']) ? $_REQUEST['action'] : Config::$default_action); - if (!CoreUtils::isValidController($module, $action)) { - //Yep, that doesn't exist. - require 'Controllers/Errors/404.php'; - exit; - } +if (!Config::$display_errors && defined('AUTH_SHOWERRORS') && !AuthUtils::hasPermission(AUTH_SHOWERRORS)) { + ini_set('display_errors', 'Off'); } -/** - * Use the Database authentication data to check whether the use has permission to access that. - * This method will automatically cause a premature exit if necessary. - * - * IMPORTANT: This will cause a fatal error if an action does not have any permissions associated with it. - * This is to prevent developers from forgetting to assign permissions to an action. - */ -CoreUtils::requirePermissionAuto($module, $action); +//Set up a shutdown function +//AFTER other things to ensure DB is registered +register_shutdown_function('\MyRadio\MyRadio\CoreUtils::shutdown'); -/** - * If a Joyride is defined, start it +/* + * Sets up a session stored in the database - uesful for sharing between more + * than one server. */ -if (isset($_REQUEST['joyride'])) { - $_SESSION['joyride'] = $_REQUEST['joyride']; +//Override any existing session +if (isset($_SESSION)) { + session_write_close(); + session_id($_COOKIE['PHPSESSID']); } -//Include the requested action -require 'Controllers/' . $module . '/' . $action . '.php'; \ No newline at end of file +if ((!defined('DISABLE_SESSION')) or DISABLE_SESSION === false) { + $session_handler = MyRadioSession::factory(); + // Changing the serialize handler to the general serialize/unserialize methods lets us + // read sessions without actually having to activate them and read them into $_SESSION + ini_set('session.serialize_handler', 'php_serialize'); + + session_set_save_handler($session_handler, true); + session_start(); +} diff --git a/src/Controllers/root_cli.php b/src/Controllers/root_cli.php new file mode 100644 index 000000000..32fcde3c8 --- /dev/null +++ b/src/Controllers/root_cli.php @@ -0,0 +1,5 @@ +query( + 'SELECT myradio.create_analytics_record($1, $2, $3, $4)', + [ + $module . '/' . $action, + $_GET['ref'] ?? '', + $_SESSION['memberid'] ?? -1, + session_id() + ] + ); + } +} + +//Include the requested action +require 'Controllers/'.$module.'/'.$action.'.php'; diff --git a/src/Controllers/service_broker.php b/src/Controllers/service_broker.php deleted file mode 100644 index a2d88e4e8..000000000 --- a/src/Controllers/service_broker.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @version 20130525 - * @package MyRadio_Core - * - * @uses $service - The current service being requested - * @uses $member - The current user - * - * @deprecated 20130525 There is now only one Service - MyRadio. Modules are not seperate. - * @todo Remove unneccessary queries for services - * - * Sets the $service_version Global Variable - */ - - -//Check if the user is allowed to select a version of the service -if (CoreUtils::hasPermission(AUTH_SELECTSERVICEVERSION)) { - require 'Controllers/brokerVersion.php'; -} - -$path = CoreUtils::getServiceVersionForUser(); - -$service_version = $path['version']; -set_include_path($path['path'] . ':' . get_include_path()); -$service_path = $path['path']; -unset($path); diff --git a/src/Controllers/traditional_auth.php b/src/Controllers/traditional_auth.php index b0750af3a..107e03dcc 100644 --- a/src/Controllers/traditional_auth.php +++ b/src/Controllers/traditional_auth.php @@ -2,25 +2,30 @@ /** * Provides basic login requirement functionality to other PHP web systems, - * backwards compatible with the old Shibbobleh system URY use to use + * backwards compatible with the old Shibbobleh system URY use to use. */ //Load the basic MyRadio framework -require_once __DIR__ . '/cli_common.php'; + +use \MyRadio\MyRadio\URLUtils; + +require_once __DIR__.'/root_cli.php'; //Check the current authentication status of the user -if ((!isset($_SESSION['memberid']) or $_SESSION['auth_use_locked']) && - (!defined('SHIBBOBLEH_ALLOW_READONLY') or SHIBBOBLEH_ALLOW_READONLY === false)) { +if ((!isset($_SESSION['memberid']) or $_SESSION['auth_use_locked']) + && (!defined('SHIBBOBLEH_ALLOW_READONLY') or SHIBBOBLEH_ALLOW_READONLY === false) +) { //Authentication is required. header('HTTP/1.1 403 Forbidden'); - header('Location: ' . CoreUtils::makeURL('MyRadio', 'login', ['next' => $_SERVER['REQUEST_URI']])); + URLUtils::redirect('MyRadio', 'login', ['next' => $_SERVER['REQUEST_URI']]); exit; } //Check if the current app needs a timeslot selected -if ((!isset($_SESSION['timeslotid']) or $_SESSION['timeslotid'] === null) && - (defined('SHIBBOBLEH_REQUIRE_TIMESLOT') and SHIBBOBLEH_REQUIRE_TIMESLOT)) { +if ((!isset($_SESSION['timeslotid']) or $_SESSION['timeslotid'] === null) + && (defined('SHIBBOBLEH_REQUIRE_TIMESLOT') and SHIBBOBLEH_REQUIRE_TIMESLOT) +) { //Timeslot needs configuring header('HTTP/1.1 403 Forbidden'); - header('Location: ' . CoreUtils::makeURL('MyRadio', 'timeslot', ['next' => $_SERVER['REQUEST_URI']])); + URLUtils::redirect('MyRadio', 'timeslot', ['next' => $_SERVER['REQUEST_URI']]); exit; } diff --git a/src/Interfaces/APICaller.php b/src/Interfaces/APICaller.php new file mode 100644 index 000000000..9984b3654 --- /dev/null +++ b/src/Interfaces/APICaller.php @@ -0,0 +1,29 @@ + - * @version 21072012 - * @package MyRadio_Core + * swapped out later (MemcachedProvider, APCProvider, PsqlProvider, FileProvider...). */ -interface CacheProvider extends Singleton { - /** - * Inserts or Updates a cache entry - * @param int $expires The number of seconds the cache is valid for - * 0 is forever. - * Performs no action if cache disabled - */ - public function set($key, $value, $expires = 0); - /** - * Gets a cache entry - * @return false if not exists, or cache disabled - */ - public function get($key); - /** - * Deletes a cache entry - */ - public function delete($key); - /** - * Empties the cache - */ - public function purge(); -} \ No newline at end of file +interface CacheProvider extends Singleton +{ + /** + * Inserts or Updates a cache entry. + * + * @param int $expires The number of seconds the cache is valid for + * 0 is forever. + * Performs no action if cache disabled + */ + public function set($key, $value, $expires = 0); + /** + * Gets a cache entry. + * + * @return false if not exists, or cache disabled + */ + public function get($key); + /** + * Gets all cache entries using the given keys. + * + * @return array + */ + public function getAll($keys); + /** + * Deletes a cache entry. + */ + public function delete($key); + /** + * Empties the cache. + */ + public function purge(); +} diff --git a/src/Interfaces/IServiceAPI.php b/src/Interfaces/IServiceAPI.php deleted file mode 100644 index cd4abf053..000000000 --- a/src/Interfaces/IServiceAPI.php +++ /dev/null @@ -1,26 +0,0 @@ - - * @version 20130808 - * @package MyRadio_Core - */ -interface IServiceAPI { - /** - * Reestablishes the database connection after being Cached - */ - function __wakeup(); - - function toDataSource($full = false); - - /** - * Static Factory method to setup an instance of a ServiceAPI Object - */ - static function getInstance($serviceObjectId); -} \ No newline at end of file diff --git a/src/Interfaces/MyRadioAuthenticator.php b/src/Interfaces/MyRadioAuthenticator.php new file mode 100644 index 000000000..ec4f1d140 --- /dev/null +++ b/src/Interfaces/MyRadioAuthenticator.php @@ -0,0 +1,53 @@ + - */ -interface MyRadio_DataSource { - /** - * Returns an Array representing this object that can be used by a MyRadio DataTable implementation. - * It should also include any links/buttons the object should have associated with it - the JS renderer can prettify - * as needed. - */ - public function toDataSource(); - public static function setToDataSource($array); -} diff --git a/src/Interfaces/Singleton.php b/src/Interfaces/Singleton.php index b9730926e..038417065 100644 --- a/src/Interfaces/Singleton.php +++ b/src/Interfaces/Singleton.php @@ -1,11 +1,11 @@ - * @version 21072012 - * @package MyRadio_Core + * Provides a standard layout for all URY Singletons. */ -interface Singleton { - public static function getInstance(); +interface Singleton +{ + public static function getInstance(); } diff --git a/src/Interfaces/TemplateEngine.php b/src/Interfaces/TemplateEngine.php index 71e7c6227..a90d845e1 100644 --- a/src/Interfaces/TemplateEngine.php +++ b/src/Interfaces/TemplateEngine.php @@ -1,14 +1,14 @@ - * @version 21072012 - * @package MyRadio_Core + * Allows drop-in replacement of template systems. */ -interface TemplateEngine extends Singleton { - public function addVariable($name, $value); - public function setTemplate($template); - public function render(); -} \ No newline at end of file +interface TemplateEngine extends Singleton +{ + public function addVariable($name, $value); + public function setTemplate($template); + public function render(); +} diff --git a/src/Menus/Charts.json b/src/Menus/Charts.json new file mode 100644 index 000000000..2df5a7c6f --- /dev/null +++ b/src/Menus/Charts.json @@ -0,0 +1,15 @@ +{ + "menu": + [ + { + "title": "View Charts", + "url": "module=Charts", + "description": "List the Charts" + }, + { + "title": "Publish a Chart", + "url": "module=Charts,action=editChartRelease", + "description": "Publishes a new chart release." + } + ] +} diff --git a/src/Menus/Events.json b/src/Menus/Events.json new file mode 100644 index 000000000..cafa8b1ad --- /dev/null +++ b/src/Menus/Events.json @@ -0,0 +1,20 @@ +{ + "menu": + [ + { + "title": "Calendar", + "url": "module=Events", + "description": "View upcoming events." + }, + { + "title": "Create Event", + "url": "module=Events,action=editEvent", + "description": "Add an event to the calendar." + }, + { + "title": "Add to Calendar", + "url": "module=Events,action=addToCalendar", + "description": "Add the station calendar to your personal calendar" + } + ] +} diff --git a/src/Menus/Library.json b/src/Menus/Library.json new file mode 100644 index 000000000..e1f2f82ee --- /dev/null +++ b/src/Menus/Library.json @@ -0,0 +1,45 @@ +{ + "menu": + [ + { + "title": "Library", + "url": "module=Library", + "description": "This part of MyRadio allows you to do some library management." + }, + { + "title": "Gap Filler", + "url": "module=Library,action=gapFiller", + "description": "Fill gaps in the library, such as unknown lyric clean/dirty" + }, + { + "title": "Missing Track Files", + "url": "module=Library,action=findMissing", + "description": "Check the Central Database for tracks that claim to be digitised but don't exist." + }, + { + "title": "Misplaced Tracks", + "url": "module=Library,action=findWrong", + "description": "List tracks that exist in the Central Database filestore, but not in the Record Database." + }, + { + "title": "Duplicate Tracks", + "url": "module=Library,action=findDuplicate", + "description": "Search the music library for duplicated tracks" + }, + { + "title": "Autocorrect Review", + "url": "module=Library,action=viewTrackCorrection", + "description": "Review suggestions our system has made to ensure the music library is accurate." + }, + { + "title": "Search", + "url": "module=Library,action=search", + "description": "Search the music library" + }, + { + "title": "Upload Track", + "url": "module=Library,action=addTrack", + "description": "Add music to the music library" + } + ] +} diff --git a/src/Menus/Mail.json b/src/Menus/Mail.json new file mode 100644 index 000000000..f71f26bff --- /dev/null +++ b/src/Menus/Mail.json @@ -0,0 +1,10 @@ +{ + "menu": + [ + { + "title": "Mail", + "url": "module=Mail", + "description": "Mailing List Manager" + } + ] +} diff --git a/src/Menus/Podcast.json b/src/Menus/Podcast.json new file mode 100644 index 000000000..038bf1c34 --- /dev/null +++ b/src/Menus/Podcast.json @@ -0,0 +1,20 @@ +{ + "menu": + [ + { + "title": "My Podcasts", + "url": "module=Podcast", + "description": "View your Podcasts" + }, + { + "title": "All Podcasts", + "url": "module=Podcast,action=allPodcast", + "description": "View all Podcasts" + }, + { + "title": "Create Podcast", + "url": "module=Podcast,action=editPodcast", + "description": "Upload a new Podcast" + } + ] +} diff --git a/src/Menus/Profile.json b/src/Menus/Profile.json new file mode 100644 index 000000000..f0fabb5b9 --- /dev/null +++ b/src/Menus/Profile.json @@ -0,0 +1,40 @@ +{ + "menu": + [ + { + "title": "View Profile", + "url": "module=Profile", + "description": "Go to your profile" + }, + { + "title": "List Members", + "url": "module=Profile,action=list", + "description": "List this year's members" + }, + { + "title": "List Previous Members", + "url": "module=Profile,action=listPrevious", + "description": "List last year's members" + }, + { + "title": "List Officers", + "url": "module=Profile,action=listOfficers", + "description": "List the current officers" + }, + { + "title": "List Trainers", + "url": "module=Profile,action=listTrainers", + "description": "List the current trainers" + }, + { + "title": "Add Member", + "url": "module=Profile,action=quickAdd", + "description": "Add a new Member" + }, + { + "title": "Bulk Add Members", + "url": "module=Profile,action=bulkAdd", + "description": "For when you need to create a lot of user accounts, fast." + } + ] +} diff --git a/src/Menus/Scheduler.json b/src/Menus/Scheduler.json new file mode 100644 index 000000000..8f6c40d15 --- /dev/null +++ b/src/Menus/Scheduler.json @@ -0,0 +1,40 @@ +{ + "menu": + [ + { + "title": "BaradDur", + "url": "https://baraddur.ury.org.uk/", + "description": "Scheduler 2.0 - A simpler way to do every day tasks." + }, + { + "title": "Pending Allocations", + "url": "module=Scheduler", + "description": "View Seasons that haven't been allocated a timeslot yet." + }, + { + "title": "My Shows", + "url": "module=Scheduler,action=myShows", + "description": "View a list of all your shows, where you can edit them and apply for seasons." + }, + { + "title": "This Term's Shows", + "url": "module=Scheduler,action=shows", + "description": "View a list of all shows with a season this term." + }, + { + "title": "All Shows", + "url": "module=Scheduler,action=shows,all=true", + "description": "View a list of all shows since time began." + }, + { + "title": "Create a Show", + "url": "module=Scheduler,action=editShow", + "description": "Create a new show on the URY Scheduler." + }, + { + "title": "Manage Terms", + "url": "module=Scheduler,action=listTerms", + "description": "Seasons are applied to in relation to Terms. Manage Terms here." + } + ] +} diff --git a/src/Menus/Stats.json b/src/Menus/Stats.json new file mode 100644 index 000000000..20da311d0 --- /dev/null +++ b/src/Menus/Stats.json @@ -0,0 +1,55 @@ +{ + "menu": + [ + { + "title": "Stats", + "url": "module=Stats", + "desctiption": "This part of MyRadio shows you some interesting statistics about the station, from training maps to college breakdowns." + }, + { + "title": "Training Map", + "url": "module=Stats,action=trainingMap", + "desctiption": "See a digraph of all training, demoes and trainer trainings!" + }, + { + "title": "Most Messaged Shows", + "url": "module=Stats,action=mostMessagedShowYear", + "desctiption": "View the shows that have been most messaged this academic year!" + }, + { + "title": "Most Messaged Timeslots", + "url": "module=Stats,action=mostMessagedTimeslotYear", + "desctiption": "See the Timeslots that have had the most messages this year!" + }, + { + "title": "Most Listened Shows", + "url": "module=Stats,action=mostListenedShowYear", + "desctiption": "See the shows that have had the most listeners this academic year!" + }, + { + "title": "Most Listened Timeslots", + "url": "module=Stats,action=mostListenedTimeslotYear", + "desctiption": "See the shows that have had the most listeners this academic year!" + }, + { + "title": "Jukebox Play Stats", + "url": "module=Stats,action=jukeboxPlayCounter", + "desctiption": "View Jukebox Playout Statistics" + }, + { + "title": "BAPS Play Stats", + "url": "module=Stats,action=bapsPlayCounter", + "desctiption": "View BAPS Playout Statistics" + }, + { + "title": "Central Music Library", + "url": "module=Stats,action=digitisation", + "desctiption": "View stats about the CML." + }, + { + "title": "Full Tracklist", + "url": "module=Stats,action=fullTracklist", + "desctiption": "See every track played within a timeframe." + } + ] +} diff --git a/src/Menus/Training.json b/src/Menus/Training.json new file mode 100644 index 000000000..fecb6afa2 --- /dev/null +++ b/src/Menus/Training.json @@ -0,0 +1,20 @@ +{ + "menu": + [ + { + "title": "Create Training Session", + "url": "module=Training,action=createDemo", + "description": "Create a training slot that newbies can sign up to." + }, + { + "title": "View Training Sessions", + "url": "module=Training,action=listDemos", + "description": "View available training sessions" + }, + { + "title": "My Waiting Lists", + "url": "module=Training,action=listWaitingLists", + "description": "Join a waiting list for training" + } + ] +} diff --git a/src/Menus/Webcam.json b/src/Menus/Webcam.json new file mode 100644 index 000000000..53fe0f18b --- /dev/null +++ b/src/Menus/Webcam.json @@ -0,0 +1,20 @@ +{ + "menu": + [ + { + "title": "Grid View", + "url": "module=Webcam", + "description": "View the URY Webcams in a Grid" + }, + { + "title": "Focus View", + "url": "module=Webcam,action=focus", + "description": "An alternative webcam view with thumbnails and a central display" + }, + { + "title": "Archives", + "url": "module=Webcam,action=archive", + "description": "View our video archives to check for coffee spillages and general tomfoolery.
          Yes, we're looking at you, Mr Edwards." + } + ] +} diff --git a/src/Menus/Website.json b/src/Menus/Website.json new file mode 100644 index 000000000..eb5908a63 --- /dev/null +++ b/src/Menus/Website.json @@ -0,0 +1,20 @@ +{ + "menu": + [ + { + "title": "Website Tools", + "url": "module=Website", + "description": "This section of MyRadio lets you control some aspects of the Website, such as banners and themes." + }, + { + "title": "Manage Banners", + "url": "module=Website,action=banners", + "description": "Create and Modify that Banners that appear on URY's Front website." + }, + { + "title": "Manage Short URLs", + "url": "module=Website,action=shortUrls", + "description": "Create shortened versions of long, unwieldy URLs." + } + ] +} diff --git a/src/Menus/iTones.json b/src/Menus/iTones.json new file mode 100644 index 000000000..24e26435e --- /dev/null +++ b/src/Menus/iTones.json @@ -0,0 +1,25 @@ +{ + "menu": + [ + { + "title": "iTones", + "url": "module=iTones", + "description": "Welcome the the Campus Jukebox Manager. These pages allow you to control what the Campus Jukebox plays out on air. This includes editing the playlists, requesting tracks, or scheduling the playout of pre-recorded material." + }, + { + "title": "Manage Playlists", + "url": "module=iTones,action=listPlaylists", + "description": "The Campus Jukebox maintains a list of lots of different playlists. Use this page to make them do cool things." + }, + { + "title": "All Playlists", + "url": "module=iTones,action=allPlaylists", + "description": "Access all playlists, whether archived or not!" + }, + { + "title": "Request Track", + "url": "module=iTones,action=requestTrack", + "description": "Want to hear a song played right now? Why not request it?" + } + ] +} diff --git a/src/Menus/menu.json b/src/Menus/menu.json new file mode 100644 index 000000000..73a4ab3e3 --- /dev/null +++ b/src/Menus/menu.json @@ -0,0 +1,322 @@ +{ + "columns": [ + { + "title": "My Stuff", + "sections": [ + { + "title": "About Me", + "items": [ + { + "title": "Change My Password", + "url": "module=MyRadio,action=pwChange", + "description": "Go here to update your MyRadio password." + }, + { + "title": "My Profile", + "url": "module=Profile", + "description": "Update your contact details, or just find out a little about yourself!" + }, + { + "title": "Webmail", + "url": "/rainloop", + "description": "If you have a mailbox, click here to access your emails. If you would like one, please contact the Computing Team." + } + ] + }, + { + "title": "Get On Air", + "items": [ + { + "title": "Join our Slack!", + "url": "https://ury.slack.com/", + "description": "Join our Slack server and chat with our team and members. We highly recommend joining if you're wanting to present a show." + }, + { + "title": "Add Studio Training Session", + "url": "module=Training,action=createDemo", + "description": "Add a training slot for presenters-to-be to sign up to." + }, + { + "title": "Studio Training Video", + "url": "http://youtu.be/S5SuC-_Ut-w", + "description": "Forgotten how to use the studios? Want to get ahead of studio training? Here's the place to start." + }, + { + "title": "WebStudio Training", + "url": "https://ury.org.uk/wiki/Working_From_Home_Resources/WebStudio", + "description": "Want to view our videos on how to use our online broadcasting tool WebStudio? Well here they are!" + }, + { + "template": "MyRadio/getOnAir.twig" + } + ] + }, + { + "title": "Tools", + "items": [ + { + "title": "Change/Cancel Show", + "url": "module=Scheduler,action=myShows", + "description": "You signed a contract saying you'd give 48 hours of cancelling a show. Click here to do just that. Or if you want to fix a spelling mistake in the title of your show." + }, + { + "title": "Book an off-air studio", + "url": "https://booking.ury.org.uk", + "description": "Need an off-air studio? Book here now!" + }, + { + "title": "Get Beds/Jingles", + "url": "https://docs.google.com/forms/d/e/1FAIpQLSdLauYhir0wRBHAJBXIw1OcpyRtdJulIueoVUz0CyM12qObSQ/viewform", + "description": "The Audio Resources team have a huge library and a little experience - give them a shot if you want that little something that makes your show stand out." + } + ] + }, + { + "title": "Merch", + "items": [ + { + "title": "Buy URY Merchandise", + "url": "https://ury-2.creator-spring.com/", + "description": "Buy URY merch from our online store." + }, + { + "title": "Request personalised merch", + "url": "https://docs.google.com/forms/d/1a9hxZopwqTsLzwYewd8JK1lvB12NixztHtqN_sWyBF8/edit?usp=drivesdk", + "description": "Buy URY merch from our online store." + } + ] + } + ] + }, + { + "title": "My Services", + "sections": [ + { + "title": "Show Tools", + "items": [ + { + "title": "Audio Logger", + "url": "/loggerng/", + "description": "Download a high-quality log." + }, + { + "title": "Clipper", + "url": "https://clipper.ury.org.uk/", + "description": "Download select clips of your show in real-time." + }, + { + "title": "List My Shows", + "url": "module=Scheduler,action=myShows", + "description": "List your applied-for and scheduled shows." + }, + { + "title": "Automatic Visualisation", + "url": "module=Scheduler,action=autoViz", + "description": "Request automatic visualisation of your shows, and download clips of past shows" + }, + { + "title": "Podcast Manager", + "url": "module=Podcast", + "description": "Upload, edit and manage podcasts." + }, + { + "title": "Studio Information Service", + "url": "module=SIS", + "description": "Access the Studio Information Service." + }, + { + "title": "WebStudio", + "url": "module=MyRadio,action=webstudio", + "description": "Access WebStudio. Plan, create, practise and broadcast your shows!" + } + ] + }, + { + "title": "Management Tools", + "items": [ + { + "title": "Chart Manager", + "url": "module=Charts", + "description": "Edit the chart and recommended music lists." + }, + { + "title": "Library Manager", + "url": "module=Library", + "description": "Edit, upload and generally maintain the URY library from here." + }, + { + "title": "Playlist Manager", + "url": "module=iTones,action=listPlaylists", + "description": "Manage system playlists, these are accessible to everyone, and are played out on the campus jukebox." + }, + { + "title": "Show Scheduler", + "url": "module=Scheduler", + "description": "Schedule outstanding show applications." + }, + { + "title": "Website Tools", + "url": "module=Website", + "description": "Edit website banners, navigation and probably other stuff too." + } + ] + }, + { + "title": "Admin Tools", + "items": [ + { + "title": "Assign Action Permissions", + "url": "module=MyRadio,action=actionPermissions", + "description": "Manage the requirements to use MyRadio Actions" + }, + { + "title": "Campus Jukebox Manager", + "url": "module=iTones", + "description": "Manage the system that keeps URY on air, even when you aren't." + }, + { + "title": "Mailing List Manager", + "url": "module=Mail", + "description": "Manage Mailing lists and email aliases." + } + ] + }, + { + "title": "Request", + "items": [ + { + "title": "Request Songs", + "url": "https://song-requests.ury.org.uk/", + "description": "Want to play a song that we don't have? Request it here and we can buy it!" + }, + { + "title": "Request Show Art for My Show", + "url": "https://forms.gle/XEdMFRy21hDjsEpFA", + "description": "Want some art people can associate with your show? Fill out this form!" + }, + { + "title": "Request Feedback on My Presenting", + "url": "https://docs.google.com/forms/d/e/1FAIpQLSed1ddilaPWdfMVWwACOO2hUMfisVcGvRj5nHuj0ESRWwgm_g/viewform", + "description": "Fill out this form to get your demo reviewed by URY members past and present." + }, + { + "title": "Request Press Access to an Event", + "url": "https://docs.google.com/forms/d/1CmTXxFJIUgPhUngLLeQYsYEpU4Ivg46Gl53pSO92C5U/viewform", + "description": "Want to go to a gig, festival, or any other event and cover it for URY? Fill out this form and we can try and get you press access." + }, + { + "title": "Request key card access to the studio", + "url": "https://forms.gle/T8V2xt5UvzPxN1mB7", + "description": "Fill out this form to request key card access to the studio" + } + ] + } + ] + }, + { + "title": "My Station", + "sections": [ + { + "title": "General", + "items": [ + { + "title": "Constitution", + "url": "https://ury.org.uk/wiki/Constitution", + "description": "The Constitution is the overarching contract by which we are all bound for life. Worth a skim read." + }, + { + "title": "Presenter's Contract", + "url": "https://drive.google.com/drive/folders/1njt5quPCok7Jkab9lyKTSZnwJ4tXnq99?usp=sharing", + "description": "The Presenter's Contract is URY's station rules and regulations." + }, + { + "title": "Members", + "url": "module=Profile,action=list", + "description": "List and manage members." + }, + { + "title": "Minutes", + "url": "https://drive.google.com/drive/folders/1txLjiR0LUvaZo9DvuDjN7uGHrPkGGmkX", + "description": "Want to know what happened in previous station meetings? Check the documents out here." + }, + { + "title": "Officers", + "url": "module=Profile,action=officers", + "description": "List and manage officerships and their permissions." + }, + { + "title": "Webcam", + "url": "module=Webcam", + "description": "Spy on people in the office." + }, + { + "title": "Wiki", + "url": "/wiki/", + "description": "A huge repository of information about URY." + }, + { + "title": "Events Calendar", + "url": "module=Events", + "description": "View upcoming station events." + } + ] + }, + { + "title": "Info", + "items": [ + { + "title": "How to Get Involved", + "url": "/wiki/HowTo_Index", + "description": "Find out how to get involved in URY." + }, + { + "title": "SIS Login Information", + "url": "module=Scheduler,action=attendance", + "description": "Check who's been logging into their shows and if people have actually turned up to do radio." + }, + { + "title": "Track Statistics", + "url": "module=Stats,action=bapsPlayCounter", + "description": "View play count statistics for library tracks." + } + ] + }, + { + "title": "Members", + "items": [ + { + "title": "Edit Members News", + "url": "module=MyRadio,action=news,feed=1", + "description": "Edit the members news feed." + }, + { + "title": "Edit PIS", + "url": "module=MyRadio,action=news,feed=4", + "description": "Edit the Presenter Information Sheet, give some information to presenters." + }, + { + "title": "Edit Tech News", + "url": "module=MyRadio,action=news,feed=2", + "description": "Edit the Tech News feed." + } + ] + }, + { + "title": "Contacts", + "items": [ + { + "title": "Contact Management", + "url": "module=Mail,action=send,list=44", + "description": "Management are the people that get the really boring paperwork emails. If that's what you need, then click here. They'd also appreciate a fun random email every once in a while for fun." + }, + { + "title": "Report a Technical Fault", + "url": "module=Mail,action=send,list=30", + "description": "Having a problem? Report it here and Computing and/or Engineering team will fix it for you. Or at least try." + } + ] + } + ] + } + ] +} diff --git a/src/Models/Charts/editChartRelease.json b/src/Models/Charts/editChartRelease.json deleted file mode 100644 index 2dbc38e5c..000000000 --- a/src/Models/Charts/editChartRelease.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "name": "charts_editchartrelease", - "options": { - "title": "Edit Chart Release" - }, - - "fields": { - "chart_type_id": { - "type": "select", - "label": "Chart Type", - "explanation": "The type of chart.", - "options": "!bind(chart_types)" - }, - "submitted_time": { - "type": "date", - "label": "Release Date", - "explanation": "The date on which the chart is released." - }, - "!section(Tracks)": { - "!repeat(1, 10)": { - "track": { - "type": "track", - "label": "!bind(repeater)", - "options": { - "autotrackname": true - } - } - } - } - } -} diff --git a/src/Models/Charts/editChartType.json b/src/Models/Charts/editChartType.json deleted file mode 100644 index 33935de1c..000000000 --- a/src/Models/Charts/editChartType.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "name": "charts_editcharttype", - "options": { - "title": "Edit Chart Type" - }, - - "fields": { - "name": { - "type": "text", - "label": "Identifier", - "explanation": "What the chart will be referred to in the website code." - }, - "description": { - "type": "text", - "label": "Name", - "explanation": "What the chart will be called on the website itself." - } - } -} diff --git a/src/Models/Core/api.php b/src/Models/Core/api.php deleted file mode 100644 index 8e9095a97..000000000 --- a/src/Models/Core/api.php +++ /dev/null @@ -1,86 +0,0 @@ - - * - The Singleton for efficiency, as almost every class uses it
          - * - The IServiceAPI (including the ServiceAPI abstract class) to provide a standard way of establishing Database and - * CacheProvider connections
          - * - The MyRadio Configuration Class - This file contains the settings required to make MyRadio Start
          - * - The MyRadioException Class - This is used for critical MyRadio errors, formatting and communicating them as needed. - * It also sets the global exception handler as something that does nothing - preventing unwanted output
          - * - The Database and CacheProvider Classes/Interfaces to provide access to the MyRadio Data Stores
          - * - The MyRadio ServiceAPI Autoloader dynamically loads additional classes as needed - * - * This file also does the following:
          - * - Provides the $member global variable - this contains the current User
          - * - Calls CoreUtils::setUpAuth, which configures the MyRadio authentication constants - * - * @version 20130106 - * @author Lloyd Wallis - * @package MyRadio_Core - */ -require_once 'Interfaces/Singleton.php'; -//Create a function to autoload classes when needed -spl_autoload_register(function($class) { - $class .= '.php'; - if (stream_resolve_include_path('Classes/ServiceAPI/' . $class)) { - //This path *must* be absolute - differing versions causes it to be reincluded otherwise - require_once __DIR__ . '/../../Interfaces/MyRadio_DataSource.php'; - require_once __DIR__ . '/../../Interfaces/IServiceAPI.php'; - require_once 'Classes/ServiceAPI/' . $class; - return; - } - - /** - * @todo Is there a better way of doing this? - */ - foreach (array('MyRadio', 'NIPSWeb', 'SIS', 'iTones', 'Vendor', 'BRA') as $dir) { - if (stream_resolve_include_path('Classes/' . $dir . '/' . $class)) { - require_once 'Classes/' . $dir . '/' . $class; - return; - } - } - }); - -require_once 'Classes/MyRadioException.php'; -require_once 'Classes/MyRadioError.php'; -set_error_handler('MyRadioError::errorsToEmail'); - -//Initiate Database -require_once 'Classes/Database.php'; - -//Initiate Cache -require_once 'Interfaces/CacheProvider.php'; -require_once 'Classes/' . Config::$cache_provider . '.php'; - -//Initialise the permission constants -CoreUtils::setUpAuth(); - -//Set up a shutdown function -//AFTER other things to ensure DB is registered -register_shutdown_function('CoreUtils::shutdown'); - -/** - * Sets up a session stored in the database - uesful for sharing between more - * than one server. - * We disable this for the API using the DISABLE_SESSION constant. - */ -if ((!defined('DISABLE_SESSION')) or DISABLE_SESSION === false) { - //Override any existing session - if (isset($_SESSION)) { - session_write_close(); - session_id($_COOKIE['PHPSESSID']); - } - $session_handler = MyRadioSession::factory(); - session_set_save_handler( - array($session_handler, 'open'), - array($session_handler, 'close'), - array($session_handler, 'read'), - array($session_handler, 'write'), - array($session_handler, 'destroy'), - array($session_handler, 'gc') - ); - session_start(); -} \ No newline at end of file diff --git a/src/Models/Library/trackfrm.php b/src/Models/Library/trackfrm.php deleted file mode 100644 index 228b7a349..000000000 --- a/src/Models/Library/trackfrm.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @version 25042013 - * @package MyRadio_Library - */ - -$form = (new MyRadioForm('lib_edittrack', $module, 'doEditTrack', - array( - 'title' => 'Edit Track' - ) - ))->addField(new MyRadioFormField('title', MyRadioFormField::TYPE_TEXT, array('label' => 'Title'))) - ->addField(new MyRadioFormField('artist', MyRadioFormField::TYPE_TEXT, array('label' => 'Artist'))) - ->addField(new MyRadioFormField('album', MyRadioFormField::TYPE_ALBUM, array('label' => 'Album'))); \ No newline at end of file diff --git a/src/Models/Mail/send.json b/src/Models/Mail/send.json deleted file mode 100644 index a9fbc6110..000000000 --- a/src/Models/Mail/send.json +++ /dev/null @@ -1,28 +0,0 @@ -{ - "name": "mail_send", - "options": { - "debug": true, - "title": "Send Email" - }, - - "fields": { - "subject": { - "type": "text", - "label": "", - "explanation": "", - "options": { - "placeholder": "Subject ([%!SHORTNAME%] is prefixed automatically)" - } - }, - - "body": { - "type": "blocktext", - "label": "", - "explanation": "" - }, - - "list": { - "type": "hidden" - } - } -} diff --git a/src/Models/MyRadio/actionPermissionsForm.php b/src/Models/MyRadio/actionPermissionsForm.php index 0681596fd..4012cd521 100644 --- a/src/Models/MyRadio/actionPermissionsForm.php +++ b/src/Models/MyRadio/actionPermissionsForm.php @@ -1,48 +1,64 @@ - * @version 24072012 * @package MyRadio_Core */ -$form = new MyRadioForm('assign_action_permissions', $module, 'addActionPermission', - array( - 'debug' => true, - 'title' => 'Assign Action Permissions' - )); +$form = new MyRadioForm( + 'assign_action_permissions', + $module, + 'addActionPermission', + [ + 'debug' => true, + 'title' => 'Assign Action Permissions', + ] +); $form->addField( - new MyRadioFormField('service', MyRadioFormField::TYPE_SELECT, - array( - 'options' => CoreUtils::getServices(), - 'explanation' => 'Select a Service to apply permissions to', - 'label' => 'Service' - ))) - ->addField( - new MyRadioFormField('module', MyRadioFormField::TYPE_TEXT, - array( - 'explanation' => 'Type a Module to apply permissions to', - 'label' => 'Module' - )) - ) - ->addField( - new MyRadioFormField('action', MyRadioFormField::TYPE_TEXT, - array( - 'explanation' => 'Type an Action within that Module to apply permissions to. - Leave blank to apply it to all Actions.', - 'label' => 'Action', - 'required' => false - ))) - ->addField( - new MyRadioFormField('permission', MyRadioFormField::TYPE_SELECT, - array( - 'explanation' => 'Select a permission that you want to add which when granted - allows a user to perform this Action. These use boolean OR, not AND so may not - stack as you would like depending on circumstances. Leave blank to allow global - access.', - 'label' => 'Permission', - 'required' => false, - 'options' => array_merge(array(array('value' => null, 'text' => 'GLOBAL ACCESS')),CoreUtils::getAllPermissions()) - ))); \ No newline at end of file + new MyRadioFormField( + 'module', + MyRadioFormField::TYPE_TEXT, + [ + 'explanation' => 'Type a Module to apply permissions to', + 'label' => 'Module', + ] + ) +)->addField( + new MyRadioFormField( + 'action', + MyRadioFormField::TYPE_TEXT, + [ + 'explanation' => 'Type an Action within that Module to apply permissions to. ' + .'Leave blank to apply it to all Actions.', + 'label' => 'Action', + 'required' => false, + ] + ) +)->addField( + new MyRadioFormField( + 'permission', + MyRadioFormField::TYPE_SELECT, + [ + 'explanation' => 'Select a permission that you want to add which when granted ' + .'allows a user to perform this Action. These use boolean OR, not AND so may not ' + .'stack as you would like depending on circumstances. Leave blank to allow global ' + .'access.', + 'label' => 'Permission', + 'required' => false, + 'options' => array_merge( + [ + [ + 'value' => null, + 'text' => 'GLOBAL ACCESS', + ], + ], + AuthUtils::getAllPermissions() + ), + ] + ) +); diff --git a/src/Models/MyRadio/addActionPermission.php b/src/Models/MyRadio/addActionPermission.php deleted file mode 100644 index 92c46bd87..000000000 --- a/src/Models/MyRadio/addActionPermission.php +++ /dev/null @@ -1,16 +0,0 @@ -readValues() or identical format. - * - * @author Lloyd Wallis - * @version 20130525 - * @package MyRadio_Core - */ - -$module = CoreUtils::getModuleId($data['module']); -$action = CoreUtils::getActionId($module, $data['action']); -$permission = $data['permission']; -if (empty($action)) $action = null; -if (empty($permission)) $permission = null; - -CoreUtils::addActionPermission($module, $action, $permission); \ No newline at end of file diff --git a/src/Models/Podcast/setCover.json b/src/Models/Podcast/setCover.json deleted file mode 100644 index e64196991..000000000 --- a/src/Models/Podcast/setCover.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "name": "podcast_setcover", - "options": { - "title": "Set Podcast Cover" - }, - - "fields": { - "cover_method": { - "type": "select", - "label": "Method", - "options": [ - { "value": "existing", "text": "Existing Cover File" }, - { "value": "new", "text": "Upload New Cover File" } - ] - }, - "existing_cover": { - "type": "text", - "label": "Existing Cover File", - "explanation": "To use an existing cover file, copy the Existing Cover File of a podcast with that file into here.", - "required": false - }, - "new_cover": { - "type": "file", - "label": "Upload New Cover File", - "explanation": "If you selected Upload New below, add the file here.", - "required": false - }, - "podcastid": { - "type": "hidden" - } - } -} diff --git a/src/Models/Quotes/addQuote.json b/src/Models/Quotes/addQuote.json deleted file mode 100644 index 938d430ab..000000000 --- a/src/Models/Quotes/addQuote.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "name": "quotes_addQuote", - "options": { - "title": "Add Quote" - }, - - "fields": { - "source": { - "type": "member", - "label": "Source", - "explanation": "Which member said it?" - }, - - "date": { - "type": "date", - "label": "Date", - "explanation": "When did they say it?" - }, - - "text": { - "type": "blocktext", - "label": "Text", - "explanation": "What was said?" - } - } -} - diff --git a/src/Models/Quotes/quotefrm.json b/src/Models/Quotes/quotefrm.json deleted file mode 100644 index 4b7e8f675..000000000 --- a/src/Models/Quotes/quotefrm.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "quotes_editquote", - "options": { - "title": "Edit Quote" - }, - "fields": { - "date": { - "type": "date", - "label": "Submission Date", - "explanation": "The date on which the quote is 'submitted'." - }, - "text": { - "type": "text", - "label": "Quote", - "explanation": "The actual quote!" - }, - "source": { - "type": "member", - "label": "Source", - "explanation": "Who said this?" - } - } -} diff --git a/src/Models/SIS/modules/messages.php b/src/Models/SIS/modules/messages.php new file mode 100644 index 000000000..be67ac61d --- /dev/null +++ b/src/Models/SIS/modules/messages.php @@ -0,0 +1,7 @@ + '\MyRadio\SIS\SIS_Remote::queryMessages', +]; diff --git a/src/Models/SIS/modules/obit.php b/src/Models/SIS/modules/obit.php new file mode 100644 index 000000000..9af049bea --- /dev/null +++ b/src/Models/SIS/modules/obit.php @@ -0,0 +1,7 @@ + AUTH_STOPBROADCAST, +]; diff --git a/src/Models/SIS/modules/presenterinfo.php b/src/Models/SIS/modules/presenterinfo.php new file mode 100644 index 000000000..d7f6b01f2 --- /dev/null +++ b/src/Models/SIS/modules/presenterinfo.php @@ -0,0 +1,7 @@ + 'MyRadio\SIS\SIS_Remote::queryPresenterInfo', +]; diff --git a/src/Models/SIS/modules/selector.php b/src/Models/SIS/modules/selector.php new file mode 100644 index 000000000..e3a1484f6 --- /dev/null +++ b/src/Models/SIS/modules/selector.php @@ -0,0 +1,13 @@ + '\MyRadio\SIS\SIS_Remote::querySelector', + 'required_permission' => AUTH_MODIFYSELECTOR, +]; + + /* + * @todo: check if the OB mount is available + * @todo: $selectorStatusFile - use MyRadio_Selector + */ diff --git a/src/Models/SIS/modules/tracklist.php b/src/Models/SIS/modules/tracklist.php new file mode 100644 index 000000000..87ff13edd --- /dev/null +++ b/src/Models/SIS/modules/tracklist.php @@ -0,0 +1,7 @@ + '\MyRadio\SIS\SIS_Remote::queryTracklist', +]; diff --git a/src/Models/SIS/modules/webcam.php b/src/Models/SIS/modules/webcam.php new file mode 100644 index 000000000..1db156615 --- /dev/null +++ b/src/Models/SIS/modules/webcam.php @@ -0,0 +1,8 @@ + '\MyRadio\SIS\SIS_Remote::queryWebcam', + 'required_permission' => AUTH_MODIFYWEBCAM, +]; diff --git a/src/Models/SIS/plugins/10-links.php b/src/Models/SIS/plugins/10-links.php deleted file mode 100755 index 64709b08c..000000000 --- a/src/Models/SIS/plugins/10-links.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @version 20130923 - * @package MyRadio_SIS - */ - -$moduleInfo = array( -'name' => 'links', -'title' => 'Contact Links', -'enabled' => true, -'startOpen' => true, -'help' => 'Provides some handy links for getting in contact with people and reporting problems.', -); \ No newline at end of file diff --git a/src/Models/SIS/plugins/20-selector.php b/src/Models/SIS/plugins/20-selector.php deleted file mode 100755 index 1cf77f0fc..000000000 --- a/src/Models/SIS/plugins/20-selector.php +++ /dev/null @@ -1,24 +0,0 @@ - - * @version 20130923 - * @package MyRadio_SIS - */ - -$moduleInfo = array( -'name' => 'selector', -'title' => 'Studio Selector', -'enabled' => true, -'startOpen' => false, -'pollfunc' => 'SIS_Remote::query_selector', -'help' => 'To the left, you can see a Studio Selector button. This contains a digital version of the magic buttons you can see in each studio. Don\'t worry - most members can\'t see this so they won\'t go messing with your show from the comfort of their own homes. You must have special priveleges if you can see this! Please use it carefully - it includes a fourth button, Outside Broadcast, which usually plays placeholder noises, which are a terrible substitute for a radio show.', -'required_permission' => AUTH_MODIFYSELECTOR, -'required_location' => false -); - - /** - * @todo: check if the OB mount is available - * @todo: $selectorStatusFile - use MyRadio_Selector - */ \ No newline at end of file diff --git a/src/Models/SIS/plugins/30-webcam.php b/src/Models/SIS/plugins/30-webcam.php deleted file mode 100755 index 168c84eb3..000000000 --- a/src/Models/SIS/plugins/30-webcam.php +++ /dev/null @@ -1,27 +0,0 @@ - - * @version 20130923 - * @package MyRadio_SIS - */ - -$vars = array( - 'webcam_prefix' => Config::$webcam_prefix, - 'cameras' => array('jukebox.jpg', 'studio1', 's1-fos', 'studio2'), - 'current' => MyRadio_Webcam::getCurrentWebcam()['current'], - 'streams' => MyRadio_Webcam::getStreams() - ); - -$moduleInfo = array( -'name' => 'webcam', -'title' => 'Webcam Selector', -'enabled' => true, -'startOpen' => false, -'pollfunc' => 'SIS_Remote::query_webcam', -'help' => 'You may have noticed that Studio 1 now has two webcams. The Webcam section over to the left lets you choose which of the station\'s cameras can be seen by listeners.', -'vars' => $vars, -'required_permission' => AUTH_MODIFYWEBCAM, -'required_location' => true, -); diff --git a/src/Models/SIS/plugins/40-stats.php b/src/Models/SIS/plugins/40-stats.php deleted file mode 100755 index a245ddb2c..000000000 --- a/src/Models/SIS/plugins/40-stats.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @version 20130923 - * @package MyRadio_SIS - */ - -$moduleInfo = array( -'name' => 'stats', -'title' => 'Statistics', -'enabled' => true, -'startOpen' => true, -'help' => 'Listenership figures are shown in realtime here. WARNING: Do NOT announce any listener statistics on-air, it is unprofessional!', -); \ No newline at end of file diff --git a/src/Models/SIS/plugins/50-obit.php b/src/Models/SIS/plugins/50-obit.php deleted file mode 100755 index b3687aa0f..000000000 --- a/src/Models/SIS/plugins/50-obit.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @version 20140109 - * @package MyRadio_SIS - */ -$moduleInfo = array( - 'name' => 'obit', - 'title' => 'Emergency Procedure', - 'enabled' => true, - 'startOpen' => false, - 'required_permission' => AUTH_STOPBROADCAST, - 'help' => 'Provides some handy links for getting in contact with people and reporting problems.', -); \ No newline at end of file diff --git a/src/Models/SIS/tabs/0-help.php b/src/Models/SIS/tabs/0-help.php deleted file mode 100755 index b8ea154d5..000000000 --- a/src/Models/SIS/tabs/0-help.php +++ /dev/null @@ -1,14 +0,0 @@ - - * @version 20131123 - * @package MyRadio_SIS - */ - -$moduleInfo = array( -'name' => 'help', -'title' => 'Getting Started', -'enabled' => SIS_Utils::getShowHelpTab($_SESSION['memberid']) -); \ No newline at end of file diff --git a/src/Models/SIS/tabs/10-piss.php b/src/Models/SIS/tabs/10-piss.php deleted file mode 100755 index 096223de8..000000000 --- a/src/Models/SIS/tabs/10-piss.php +++ /dev/null @@ -1,20 +0,0 @@ - - * @version 20130925 - * @package MyRadio_SIS - */ - -$vars = array( - 'piss' => MyRadioNews::getLatestNewsItem(Config::$piss_feed, MyRadio_User::getInstance()) - ); - -$moduleInfo = array( -'name' => 'piss', -'title' => 'Presenter Information', -'enabled' => true, -'help' => 'Please read this before the start of your show. It\'s among the tabs up at the top and provides lots of useful information from our great lord and master, . It\'s a great way to find out how to get more involved in '.Config::$short_name.' or see what you events you can advertise on your show.', -'vars' => $vars -); \ No newline at end of file diff --git a/src/Models/SIS/tabs/20-messages.php b/src/Models/SIS/tabs/20-messages.php deleted file mode 100755 index b51846201..000000000 --- a/src/Models/SIS/tabs/20-messages.php +++ /dev/null @@ -1,17 +0,0 @@ - - * @version 20130925 - * @package MyRadio_SIS - */ - - -$moduleInfo = array( -'name' => 'messages', -'title' => 'Messages', -'enabled' => true, -'help' => 'This is the big one, probably where you will spend most of your time in SIS. The Message tab provides you with all the comunication you can get with the listener, whether the message "Via the website" or text the studio it all comes here.', -'pollfunc' => 'SIS_Remote::query_messages' -); diff --git a/src/Models/SIS/tabs/30-news.php b/src/Models/SIS/tabs/30-news.php deleted file mode 100755 index 263cb2228..000000000 --- a/src/Models/SIS/tabs/30-news.php +++ /dev/null @@ -1,19 +0,0 @@ - - * @version 20130925 - * @package MyRadio_SIS - */ - -$moduleInfo = array( -'name' => 'news', -'title' => 'News', -'enabled' => true, -'help' => 'Do you like talking about the news on your show? The news tab links you into our subscribed news service IRN, from where ou can pull up news stories, headlines and create bullitins. These are the same stories that come in on our hourly IRN newsfeed (the news fader).', -); - - /** - * @todo: query_news ? - */ \ No newline at end of file diff --git a/src/Models/SIS/tabs/40-schedule.php b/src/Models/SIS/tabs/40-schedule.php deleted file mode 100755 index 7ad427949..000000000 --- a/src/Models/SIS/tabs/40-schedule.php +++ /dev/null @@ -1,15 +0,0 @@ - - * @version 20130925 - * @package MyRadio_SIS - */ - -$moduleInfo = array( -'name' => 'schedule', -'title' => 'Schedule', -'enabled' => true, -'help' => 'Schedule tab lets you see what\'s on for the rest of the day', -); \ No newline at end of file diff --git a/src/Models/SIS/tabs/50-tracklist.php b/src/Models/SIS/tabs/50-tracklist.php deleted file mode 100755 index eb9303eab..000000000 --- a/src/Models/SIS/tabs/50-tracklist.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @version 20130925 - * @package MyRadio_SIS - */ - -$moduleInfo = array( -'name' => 'tracklist', -'title' => 'Track Listing', -'enabled' => true, -'pollfunc' => 'SIS_Remote::query_tracklist', -'help' => 'Tracklisting is a legal requirement for '.Config::$short_name.' to broadcast, so you must fill this in. If you use BAPS this will be done automatically, but if you use other sources you must fill this in yourself', -); diff --git a/src/Models/Scheduler/allocatefrm.php b/src/Models/Scheduler/allocatefrm.php deleted file mode 100644 index 338ef7f32..000000000 --- a/src/Models/Scheduler/allocatefrm.php +++ /dev/null @@ -1,63 +0,0 @@ - - * @version 21072012 - * @package MyRadio_Scheduler - */ -$form = new MyRadioForm('sched_allocate', $module, 'doAllocate', - array( - 'title' => 'Allocate Timeslots to Season', - 'template' => 'Scheduler/allocate.twig', - )); - -//Set up the weeks checkboxes -$weeks = array(); -for ($i = 1; $i <= 10; $i++) { - $weeks[] = new MyRadioFormField('wk' . $i, MyRadioFormField::TYPE_CHECK, - array( - 'label' => 'Week ' . $i, - 'required' => false, - 'options' => array('checked' => in_array($i, $season->getRequestedWeeks())) - )); -} - -//Set up the requested times radios -$times = array(); -$i = 0; -foreach ($season->getRequestedTimesAvail() as $time) { - $times[] = array( - 'value' => $i, - 'text' => $time['time'] . ' ' . $time['info'], - 'disabled' => $time['conflict'], - 'class' => $time['conflict'] ? 'ui-state-error' : '' - ); - $i++; -} - -$times[] = array('value' => -1, 'text' => 'Other (Choose below)'); - -$form->addField( - new MyRadioFormField('weeks', MyRadioFormField::TYPE_CHECKGRP, - array('options' => $weeks, - 'label' => 'Schedule for Weeks' - ) - ) -)->addField( - new MyRadioFormField('time', MyRadioFormField::TYPE_RADIO, - array('options' => $times, 'label' => 'Timeslot', 'required' => false) - ) -)->addField( - new MyRadioFormField('timecustom_day', MyRadioFormField::TYPE_DAY, - array('label' => 'Other Day: ', 'required' => false)) -)->addField( - new MyRadioFormField('timecustom_stime', MyRadioFormField::TYPE_TIME, - array('label' => 'from', 'required' => false)) -)->addField( - new MyRadioFormField('timecustom_etime', MyRadioFormField::TYPE_TIME, - array('label' => 'duration', 'required' => false, 'value' => '01:00')) -); \ No newline at end of file diff --git a/src/Models/Scheduler/demofrm.php b/src/Models/Scheduler/demofrm.php deleted file mode 100644 index 9dbab98af..000000000 --- a/src/Models/Scheduler/demofrm.php +++ /dev/null @@ -1,16 +0,0 @@ - - * @version 21072012 - * @package MyRadio_Scheduler - */ - -$form = (new MyRadioForm('sched_demo', $module, 'doDemo', - array( - 'title' => 'Create Demo' - ) - ))->addField(new MyRadioFormField('demo-datetime', MyRadioFormField::TYPE_DATETIME, array('label' => 'Date and Time of the Demo')) -); \ No newline at end of file diff --git a/src/Models/Scheduler/reasonfrm.php b/src/Models/Scheduler/reasonfrm.php deleted file mode 100644 index 470b824ad..000000000 --- a/src/Models/Scheduler/reasonfrm.php +++ /dev/null @@ -1,21 +0,0 @@ - - * @version 05012013 - * @package MyRadio_Scheduler - */ - -$form = (new MyRadioForm('sched_cancel', $module, 'doCancelEpisode', - array( - 'debug' => false, - 'title' => 'Cancel Episode' - ) - ))->addField( - new MyRadioFormField('reason', MyRadioFormField::TYPE_BLOCKTEXT, - array('label' => 'Please explain why this Episode should be removed from the Schedule')) - )->addField( - new MyRadioFormField('show_season_timeslot_id', MyRadioFormField::TYPE_HIDDEN, - array('value' => $_REQUEST['show_season_timeslot_id']))); \ No newline at end of file diff --git a/src/Models/Scheduler/rejectfrm.php b/src/Models/Scheduler/rejectfrm.php deleted file mode 100644 index 10db9fed8..000000000 --- a/src/Models/Scheduler/rejectfrm.php +++ /dev/null @@ -1,32 +0,0 @@ - - * @version 02012013 - * @package MyRadio_Scheduler - */ -$form = (new MyRadioForm('sched_reject', $module, 'doReject', - array( - 'debug' => false, - 'title' => 'Reject Season Application' - ) - ))->addField( - new MyRadioFormField('season_id', MyRadioFormField::TYPE_HIDDEN) - )->addField( - new MyRadioFormField('reason', MyRadioFormField::TYPE_BLOCKTEXT, - array( - 'label' => 'Reason for Rejection: ', - 'explanation' => 'You can enter a reason here for the application being rejected.' - .' If you then choose to send this response to the applicant, they can then edit their' - .' application and resubmit.' - )) - )->addField( - new MyRadioFormField('notify_user', MyRadioFormField::TYPE_CHECK, - array( - 'label' => 'Notify the Applicant via Email?', - 'options' => array('checked' => true), - 'required' => false - )) -); \ No newline at end of file diff --git a/src/Models/Scheduler/seasonfrm.php b/src/Models/Scheduler/seasonfrm.php deleted file mode 100644 index 440ed9289..000000000 --- a/src/Models/Scheduler/seasonfrm.php +++ /dev/null @@ -1,69 +0,0 @@ - - * @version 21072012 - * @package MyRadio_Scheduler - */ - -//Set up the weeks checkboxes -$weeks = array(); -for ($i = 1; $i <= 10; $i++) { - $weeks[] = new MyRadioFormField('wk' . $i, MyRadioFormField::TYPE_CHECK, array('label' => 'Week ' . $i, 'required' => false)); -} - -$form = (new MyRadioForm('sched_season', $module, 'doSeason', - array( - 'debug' => true, - 'title' => 'Edit Season' - ) - ))->addField( - new MyRadioFormField('show_id', MyRadioFormField::TYPE_HIDDEN) - )->addField( - new MyRadioFormField('grp-basics', MyRadioFormField::TYPE_SECTION, - array('label' => '')) - )->addField( - new MyRadioFormField('weeks', MyRadioFormField::TYPE_CHECKGRP, - array('options' => $weeks, - 'explanation' => 'Select what weeks this term this show will be on air', - 'label' => 'Schedule for Weeks' - ) - ) - )->addField( - new MyRadioFormField('times', MyRadioFormField::TYPE_TABULARSET, - array('label' => 'Preferred Times', - 'options' => array( - new MyRadioFormField('day', MyRadioFormField::TYPE_DAY, - array('label' => 'On')), - new MyRadioFormField('stime', MyRadioFormField::TYPE_TIME, - array('label' => 'from')), - new MyRadioFormField('etime', MyRadioFormField::TYPE_TIME, - array('label' => 'until')) - ))) - )->addField(new MyRadioFormField('grp-basics_close', MyRadioFormField::TYPE_SECTION_CLOSE) - )->addField( - new MyRadioFormField('grp-adv', MyRadioFormField::TYPE_SECTION, - array('label' => 'Advanced Options')) - )->addField( - new MyRadioFormField('description', MyRadioFormField::TYPE_BLOCKTEXT, - array( - 'explanation' => 'Each season of your show can have its own description. ' - . 'If you leave this blank, the main description for your Show will be used.', - 'label' => 'Description', - 'options' => array('minlength' => 140), - 'required' => false - ) - ) - )->addField( - new MyRadioFormField('tags', MyRadioFormField::TYPE_TEXT, - array( - 'label' => 'Tags', - 'explanation' => 'A set of keywords to describe this Season. These will be added onto the' - . ' Tags you already have set for the Show.', - 'required' => false - ) - ) - )->addField(new MyRadioFormField('grp-adv_close', MyRadioFormField::TYPE_SECTION_CLOSE) - ); \ No newline at end of file diff --git a/src/Models/Scheduler/showfrm.php b/src/Models/Scheduler/showfrm.php deleted file mode 100644 index 33878fdf9..000000000 --- a/src/Models/Scheduler/showfrm.php +++ /dev/null @@ -1,66 +0,0 @@ - - * @version 20130727 - * @package MyRadio_Scheduler - */ -$form = (new MyRadioForm('sched_show', 'Scheduler', 'doShow', array( - 'debug' => true, - 'title' => 'Edit Show' - ) - ))->addField( - new MyRadioFormField('grp-basics', MyRadioFormField::TYPE_SECTION, array('label' => 'About My Show')) - )->addField( - new MyRadioFormField('title', MyRadioFormField::TYPE_TEXT, array( - 'explanation' => 'Enter a name for your new show. Try and make it unique.', - 'label' => 'Show Name' - ) - ) - )->addField( - new MyRadioFormField('description', MyRadioFormField::TYPE_BLOCKTEXT, array( - 'explanation' => 'Describe your show as best you can. This goes on the public-facing website.', - 'label' => 'Description' - ) - ) - )->addField( - new MyRadioFormField('genres', MyRadioFormField::TYPE_SELECT, array( - 'options' => array_merge(array(array('text' => 'Please select...', 'disabled' => true)), MyRadio_Scheduler::getGenres()), - 'label' => 'Genre', - 'explanation' => 'What type of music do you play, if any?' - ) - ) - )->addField( - new MyRadioFormField('tags', MyRadioFormField::TYPE_TEXT, array( - 'label' => 'Tags', - 'explanation' => 'A set of keywords to describe your show generally, seperated with spaces.' - ) - ) - )->addField(new MyRadioFormField('grp-basics_close', MyRadioFormField::TYPE_SECTION_CLOSE) - )->addField( - new MyRadioFormField('grp-credits', MyRadioFormField::TYPE_SECTION, array('label' => 'Who\'s On My Show')) - )->addField( - new MyRadioFormField('credits', MyRadioFormField::TYPE_TABULARSET, array( - 'options' => array( - new MyRadioFormField('member', MyRadioFormField::TYPE_MEMBER, array( - 'explanation' => '', - 'label' => 'Credit' - ) - ), - new MyRadioFormField('credittype', MyRadioFormField::TYPE_SELECT, array( - 'options' => array_merge(array(array('text' => 'Please select...', 'disabled' => true)), MyRadio_Scheduler::getCreditTypes()), - 'explanation' => '', - 'label' => 'Role' - ) - )))) - )->addField(new MyRadioFormField('grp-credits_close', MyRadioFormField::TYPE_SECTION_CLOSE) - )->addField( - new MyRadioFormField('mixclouder', MyRadioFormField::TYPE_CHECK, array( - 'explanation' => 'If ticked, your shows will automatically be uploaded to mixcloud', - 'label' => 'Enable Mixcloud', - 'options' => ['checked' => true], - 'required' => false - ) - ) -); \ No newline at end of file diff --git a/src/Models/Scheduler/showphotofrm.php b/src/Models/Scheduler/showphotofrm.php deleted file mode 100644 index fa578ccb5..000000000 --- a/src/Models/Scheduler/showphotofrm.php +++ /dev/null @@ -1,18 +0,0 @@ - - * @version 20130529 - * @package MyRadio_Scheduler - */ -$form = (new MyRadioForm('sched_showphoto', $module, 'doShowPhoto', array( - 'debug' => true, - 'title' => 'Update Show Photo', - )))->addField( - new MyRadioFormField('show_id', MyRadioFormField::TYPE_HIDDEN) - )->addField( - new MyRadioFormField('image_file', MyRadioFormField::TYPE_FILE, array('label' => 'Photo') - ) -); \ No newline at end of file diff --git a/src/Models/iTones/editplaylistfrm.php b/src/Models/iTones/editplaylistfrm.php deleted file mode 100644 index fb0db1bd1..000000000 --- a/src/Models/iTones/editplaylistfrm.php +++ /dev/null @@ -1,31 +0,0 @@ - - * @version 20130712 - * @package MyRadio_iTones - */ -$form = new MyRadioForm('itones_playlistedit', $module, 'doEditPlaylist', array( - 'title' => 'Edit Campus Jukebox Playlist' - )); - -$form->addField( - new MyRadioFormField('tracks', MyRadioFormField::TYPE_TABULARSET, array('options' => array( - new MyRadioFormField('track', MyRadioFormField::TYPE_TRACK, array( - 'label' => 'Tracks' - )), - new MyRadioFormField('artist', MyRadioFormField::TYPE_ARTIST, array( - 'label' => 'Artists' - )) - ) - ) - ) -)->addField(new MyRadioFormField('notes', MyRadioFormField::TYPE_TEXT, array( - 'label' => 'Notes', - 'explanation' => 'Optional. Enter notes aboout this change.', - 'required' => false - ) - ) -)->addField(new MyRadioFormField('playlistid', MyRadioFormField::TYPE_HIDDEN)); \ No newline at end of file diff --git a/src/Models/iTones/requesttrackfrm.json b/src/Models/iTones/requesttrackfrm.json deleted file mode 100644 index 5a5379b1e..000000000 --- a/src/Models/iTones/requesttrackfrm.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "itones_trackrequest", - "options": { - "debug": "true", - "title": "Request Campus Jukebox Track" - }, - - "fields": { - "track": { - "type": "track", - "label": "Track", - "explanation": "Enter a track here to request it on the Jukebox." - }, - - "requests": { - "type": "number", - "label": "Remaining Requests", - "explanation": "This is the number of requests you can make at the moment. If you run out of requests, please wait a while and try again.", - "value": "!bind(remaining_requests)", - "enabled": false, - "required": false - } - } -} diff --git a/src/MyRadio_Config.local.php.dist b/src/MyRadio_Config.local.php.dist index 0ff0c4ad4..b4dd80006 100644 --- a/src/MyRadio_Config.local.php.dist +++ b/src/MyRadio_Config.local.php.dist @@ -2,10 +2,11 @@ /** * Configuration specific to a version or too sensitive to be in the main Config * go here. - * + * * Make a copy of this file called MyRadio_Config.local.php and edit it as * necessary. It will then override settings in the Config class. */ +use \MyRadio\Config; /** * String Settings @@ -15,11 +16,26 @@ Config::$long_name = 'FULL STATION NAME HERE'; Config::$email_domain = 'EMAIL DOMAIN HERE'; Config::$founded = 'FOUNDED YEAR HERE'; Config::$facebook = 'FACEBOOK PAGE ADDRESS HERE (inc https://facebook.com)'; + +/** + * Signup Emails + */ +Config::$welcome_email_sender_memberid = null; Config::$welcome_email = << + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Public/css/fonts/glyphicons-halflings-regular.ttf b/src/Public/css/fonts/glyphicons-halflings-regular.ttf new file mode 100644 index 000000000..67fa00bf8 Binary files /dev/null and b/src/Public/css/fonts/glyphicons-halflings-regular.ttf differ diff --git a/src/Public/css/fonts/glyphicons-halflings-regular.woff b/src/Public/css/fonts/glyphicons-halflings-regular.woff new file mode 100644 index 000000000..8c54182aa Binary files /dev/null and b/src/Public/css/fonts/glyphicons-halflings-regular.woff differ diff --git a/src/Public/css/joyride.css b/src/Public/css/joyride.css new file mode 100644 index 000000000..2214a3fe8 --- /dev/null +++ b/src/Public/css/joyride.css @@ -0,0 +1,257 @@ +/* Artfully masterminded by ZURB */ +body { + position: relative; +} + +#joyRideTipContent { display: none; } + +.joyRideTipContent { display: none; } + +/* Default styles for the container */ +.joyride-tip-guide { + position: absolute; + background: #000; + background: rgba(0,0,0,0.8); + display: none; + color: #fff; + max-width: 500px; + z-index: 101; + top: 0; /* keeps the page from scrolling when calculating position */ + left: 0; + font-family: "HelveticaNeue", "Helvetica Neue", "Helvetica", Helvetica, Arial, Lucida, sans-serif; + font-weight: normal; + -moz-border-radius: 4px; + -webkit-border-radius: 4px; + border-radius: 4px; +} + +.joyride-content-wrapper { + padding: 10px 10px 15px 15px; +} + +/* Mobile */ +@media only screen and (max-width: 767px) { + .joyride-tip-guide { + width: 95% !important; + -moz-border-radius: 0; + -webkit-border-radius: 0; + border-radius: 0; + left: 2.5% !important; + } + .joyride-tip-guide-wrapper { + width: 100%; + } +} + + +/* Add a little css triangle pip, older browser just miss out on the fanciness of it */ +.joyride-tip-guide span.joyride-nub { + display: block; + position: absolute; + left: 22px; + width: 0; + height: 0; + border: solid 14px; + border: solid 14px; +} + +.joyride-tip-guide span.joyride-nub.top { + /* + IE7/IE8 Don't support rgba so we set the fallback + border color here. However, IE7/IE8 are also buggy + in that the fallback color doesn't work for + border-bottom-color so here we set the border-color + and override the top,left,right colors below. + */ + border-color: #000; + border-color: rgba(0,0,0,0.8); + border-top-color: transparent !important; + border-left-color: transparent !important; + border-right-color: transparent !important; + top: -28px; + bottom: none; +} + +.joyride-tip-guide span.joyride-nub.bottom { + /* + IE7/IE8 Don't support rgba so we set the fallback + border color here. However, IE7/IE8 are also buggy + in that the fallback color doesn't work for + border-top-color so here we set the border-color + and override the bottom,left,right colors below. + */ + border-color: #000; + border-color: rgba(0,0,0,0.8) !important; + border-bottom-color: transparent !important; + border-left-color: transparent !important; + border-right-color: transparent !important; + bottom: -28px; + bottom: none; +} + +.joyride-tip-guide span.joyride-nub.right { + border-color: #000; + border-color: rgba(0,0,0,0.8) !important; + border-top-color: transparent !important; + border-right-color: transparent !important; + border-bottom-color: transparent !important; + top: 22px; + bottom: none; + left: auto; + right: -28px; +} + +.joyride-tip-guide span.joyride-nub.left { + border-color: #000; + border-color: rgba(0,0,0,0.8) !important; + border-top-color: transparent !important; + border-left-color: transparent !important; + border-bottom-color: transparent !important; + top: 22px; + left: -28px; + right: auto; + bottom: none; +} + +.joyride-tip-guide span.joyride-nub.top-right { + border-color: #000; + border-color: rgba(0,0,0,0.8); + border-top-color: transparent !important; + border-left-color: transparent !important; + border-right-color: transparent !important; + top: -28px; + bottom: none; + left: auto; + right: 28px; +} + +/* Typography */ +.joyride-tip-guide h1,.joyride-tip-guide h2,.joyride-tip-guide h3,.joyride-tip-guide h4,.joyride-tip-guide h5,.joyride-tip-guide h6 { + line-height: 1.25; + margin: 0; + font-weight: bold; + color: #fff; +} +.joyride-tip-guide h1 { font-size: 30px; } +.joyride-tip-guide h2 { font-size: 26px; } +.joyride-tip-guide h3 { font-size: 22px; } +.joyride-tip-guide h4 { font-size: 18px; } +.joyride-tip-guide h5 { font-size: 16px; } +.joyride-tip-guide h6 { font-size: 14px; } +.joyride-tip-guide p { + margin: 0 0 18px 0; + font-size: 14px; + line-height: 18px; +} +.joyride-tip-guide a { + color: rgb(255,255,255); + text-decoration: none; + border-bottom: dotted 1px rgba(255,255,255,0.6); +} +.joyride-tip-guide a:hover { + color: rgba(255,255,255,0.8); + border-bottom: none; +} + +/* Button Style */ +.joyride-tip-guide .joyride-next-tip { + width: auto; + padding: 6px 18px 4px; + font-size: 13px; + text-decoration: none; + color: rgb(255,255,255); + border: solid 1px rgb(0,60,180); + background: rgb(0,99,255); + background: -moz-linear-gradient(top, rgb(0,99,255) 0%, rgb(0,85,214) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgb(0,99,255)), color-stop(100%,rgb(0,85,214))); + background: -webkit-linear-gradient(top, rgb(0,99,255) 0%,rgb(0,85,214) 100%); + background: -o-linear-gradient(top, rgb(0,99,255) 0%,rgb(0,85,214) 100%); + background: -ms-linear-gradient(top, rgb(0,99,255) 0%,rgb(0,85,214) 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#0063ff', endColorstr='#0055d6',GradientType=0 ); + background: linear-gradient(top, rgb(0,99,255) 0%,rgb(0,85,214) 100%); + text-shadow: 0 -1px 0 rgba(0,0,0,0.5); + -webkit-border-radius: 2px; + -moz-border-radius: 2px; + border-radius: 2px; + -webkit-box-shadow: 0px 1px 0px rgba(255,255,255,0.3) inset; + -moz-box-shadow: 0px 1px 0px rgba(255,255,255,0.3) inset; + box-shadow: 0px 1px 0px rgba(255,255,255,0.3) inset; +} + +.joyride-next-tip:hover { + color: rgb(255,255,255) !important; + border: solid 1px rgb(0,60,180) !important; + background: rgb(43,128,255); + background: -moz-linear-gradient(top, rgb(43,128,255) 0%, rgb(29,102,211) 100%); + background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,rgb(43,128,255)), color-stop(100%,rgb(29,102,211))); + background: -webkit-linear-gradient(top, rgb(43,128,255) 0%,rgb(29,102,211) 100%); + background: -o-linear-gradient(top, rgb(43,128,255) 0%,rgb(29,102,211) 100%); + background: -ms-linear-gradient(top, rgb(43,128,255) 0%,rgb(29,102,211) 100%); + filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#2b80ff', endColorstr='#1d66d3',GradientType=0 ); + background: linear-gradient(top, rgb(43,128,255) 0%,rgb(29,102,211) 100%); +} + +.joyride-timer-indicator-wrap { + width: 50px; + height: 3px; + border: solid 1px rgba(255,255,255,0.1); + position: absolute; + right: 17px; + bottom: 16px; +} +.joyride-timer-indicator { + display: block; + width: 0; + height: inherit; + background: rgba(255,255,255,0.25); +} + +.joyride-close-tip { + position: absolute; + right: 10px; + top: 10px; + color: rgba(255,255,255,0.4) !important; + text-decoration: none; + font-family: Verdana, sans-serif; + font-size: 10px; + font-weight: bold; + border-bottom: none !important; +} + +.joyride-close-tip:hover { + color: rgba(255,255,255,0.9) !important; +} + +.joyride-modal-bg { + position: fixed; + height: 100%; + width: 100%; + background: rgb(0,0,0); + background: transparent; + background: rgba(0,0,0, 0.5); + -ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; + filter: alpha(opacity=50); + opacity: 0.5; + z-index: 100; + display: none; + top: 0; + left: 0; + cursor: pointer; +} + +.joyride-expose-wrapper { + background-color: #ffffff; + position: absolute; + z-index: 102; + -moz-box-shadow: 0px 0px 30px #ffffff; + -webkit-box-shadow: 0px 0px 30px #ffffff; + box-shadow: 0px 0px 30px #ffffff; +} + +.joyride-expose-cover { + background: transparent; + position: absolute; + z-index: 10000; + top: 0px; + left: 0px; +} diff --git a/src/Public/css/main.css b/src/Public/css/main.css deleted file mode 100644 index cac5a4a95..000000000 --- a/src/Public/css/main.css +++ /dev/null @@ -1,1620 +0,0 @@ -/* - * HTML5 Boilerplate - * - * What follows is the result of much research on cross-browser styling. - * Credit left inline and big thanks to Nicolas Gallagher, Jonathan Neal, - * Kroc Camen, and the H5BP dev community and team. - */ -/* ========================================================================== - Base styles: opinionated defaults - ========================================================================== */ -html, -button, -input, -select, -textarea { - color: #222; -} -body { - font-size: 1em; - line-height: 1.4; -} -/* - * Remove text-shadow in selection highlight: h5bp.com/i - * These selection declarations have to be separate. - * Customize the background color to match your design. - */ -::-moz-selection { - background: #b3d4fc; - text-shadow: none; -} -::selection { - background: #b3d4fc; - text-shadow: none; -} -/* - * A better looking default horizontal rule - */ -hr { - display: block; - height: 1px; - border: 0; - border-top: 1px solid #ccc; - margin: 1em 0; - padding: 0; -} -/* - * Remove the gap between images and the bottom of their containers: h5bp.com/i/440 - */ -img { - vertical-align: middle; -} -/* - * Allow only vertical resizing of textareas. - */ -textarea { - resize: vertical; -} -/* - * Smaller margin for boxes - */ -dl { - margin: 0.4em 0; -} -/* ========================================================================== - Chrome Frame prompt - ========================================================================== */ -.chromeframe { - margin: 0.2em 0; - background: #ccc; - color: #000; - padding: 0.2em 0; -} -/* ========================================================================== - Plugin styles - ========================================================================== */ -.countdownHolder { - width: 450px; - margin: 0 auto; - font: 40px/1.5 'Open Sans Condensed', sans-serif; - text-align: center; - letter-spacing: -3px; -} -.position { - display: inline-block; - height: 1.6em; - overflow: hidden; - position: relative; - width: 1.05em; -} -.digit { - position: absolute; - display: block; - width: 1em; - background-color: #444; - border-radius: 0.2em; - text-align: center; - color: #fff; - letter-spacing: -1px; -} -.digit.static { - box-shadow: 1px 1px 1px rgba(4, 4, 4, 0.35); - background-image: linear-gradient(bottom, #3a3a3a 50%, #444444 50%); - background-image: -o-linear-gradient(bottom, #3a3a3a 50%, #444444 50%); - background-image: -moz-linear-gradient(bottom, #3a3a3a 50%, #444444 50%); - background-image: -webkit-linear-gradient(bottom, #3a3a3a 50%, #444444 50%); - background-image: -ms-linear-gradient(bottom, #3a3a3a 50%, #444444 50%); - background-image: -webkit-gradient(linear, left bottom, left top, color-stop(0.5, #3a3a3a), color-stop(0.5, #444444)); -} -/** - * You can use these classes to hide parts - * of the countdown that you don't need. - */ -.countDays { - /* display:none !important;*/ -} -.countDiv0 { - /* display:none !important;*/ -} -.countDiv { - display: inline-block; - width: 16px; - height: 1.6em; - position: relative; -} -.countDiv:before, -.countDiv:after { - position: absolute; - width: 5px; - height: 5px; - background-color: #444; - border-radius: 50%; - left: 50%; - margin-left: -3px; - top: 0.5em; - box-shadow: 1px 1px 1px rgba(4, 4, 4, 0.5); - content: ''; -} -.countDiv:after { - top: 0.9em; -} -/* ========================================================================== - URY custom styles - ========================================================================== */ -.colour-links a { - color: #feb93a; - text-decoration: none; - -webkit-text-shadow: 1px 2px black; - -khtml-text-shadow: 1px 2px black; - -moz-text-shadow: 1px 2px black; - text-shadow: 1px 2px black; -} -.colour-links a:hover { - color: #ffd758; - text-decoration: underline; -} -.colour-links a:visited { - color: #f4af30; -} -.colour-links a:active { - color: #f4af30; -} -html { - height: 100%; - margin: 0px; - font-family: helvetica, arial, sans-serif; - background: #dddddd; -} -body { - margin: 22px auto 0; - width: 1050px; - background: url('/static/img/bg.png') #5c67a0 no-repeat center -170px; - -webkit-box-shadow: 0 0 15px 4px #333; - -moz-box-shadow: 0 0 15px 4px #333; - box-shadow: 0 0 15px 4px #333; -} -a { - color: #074dab; - text-decoration: none; -} -a:hover { - text-decoration: underline; -} -#pageHeader a, -#pageFooter a { - color: #feb93a; - text-decoration: none; - -webkit-text-shadow: 1px 2px black; - -khtml-text-shadow: 1px 2px black; - -moz-text-shadow: 1px 2px black; - text-shadow: 1px 2px black; -} -#pageHeader a:hover, -#pageFooter a:hover { - color: #ffd758; - text-decoration: underline; -} -#pageHeader a:visited, -#pageFooter a:visited { - color: #f4af30; -} -#pageHeader a:active, -#pageFooter a:active { - color: #f4af30; -} -#pageHeader a, -#pageFooter a { - color: #feb93a; - text-decoration: none; - -webkit-text-shadow: 1px 2px black; - -khtml-text-shadow: 1px 2px black; - -moz-text-shadow: 1px 2px black; - text-shadow: 1px 2px black; -} -#pageHeader a:hover, -#pageFooter a:hover { - color: #ffd758; - text-decoration: underline; -} -#pageHeader a:visited, -#pageFooter a:visited { - color: #f4af30; -} -#pageHeader a:active, -#pageFooter a:active { - color: #f4af30; -} -dl { - margin: 0; -} -#pageHeader { - height: 230px; - padding: 0 15px 0 0; - -webkit-text-shadow: 1px 2px black; - -khtml-text-shadow: 1px 2px black; - -moz-text-shadow: 1px 2px black; - text-shadow: 1px 2px black; - color: #ffffff; -} -#pageHeader #logo { - width: 430px; - display: inline-block; - text-align: center; - color: #fff; - text-decoration: none; -} -#pageHeader #showOnUp { - padding: 30px 20px 0; - width: 430px; -} -#pageHeader #showOnUp div { - float: none; -} -#pageHeader #showOnUp dl { - margin-top: 0; -} -#pageHeader .showPerson { - display: inline-block; -} -#pageHeader #countup { - margin-top: 20px; -} -#pageHeader #webcam { - display: inline-block; - width: 150px; - float: right; - text-align: center; - margin-top: 40px; - padding-top: 20px; -} -#pageHeader h1, -header h2 { - text-transform: uppercase; - margin: 0; -} -#pageFooter { - margin: 10px 0; - padding: 10px 10px; - color: #ffffff; -} -#pageFooter h4 { - margin: 0; -} -#pageFooter #copyright { - float: right; -} -#pageFooter #copyright p { - margin-top: 0; -} -#pageFooter ul { - list-style: none; - margin-top: 0; - float: left; -} -#pageFooter ul li { - display: inline-block; -} -#pageFooter ul li:after { - content: "|"; -} -#pageFooter ul li:last-of-type:after { - content: ""; -} -#pageFooter ul li a { - padding: 0 5px; -} -#pageFooter ul li:first-of-type a, -#pageFooter ul li:last-of-type a { - padding-left: 0; -} -#menu { - position: fixed; - top: 0; - left: 0; - height: 22px; - width: 100%; - text-align: center; - z-index: 9999; -} -#menu ul { - position: relative; - margin: 0 auto; - padding: 0; -} -#menu ul li { - display: inline-block; - padding: 0; -} -#menu ul li a { - color: #fff; - text-decoration: none; - font-size: 100%; - display: block; - padding: 0 20px; -} -#menu ul li a:visited { - color: #eee; -} -#menu ul li a:hover { - color: #fff; - background: #9da3c6; - background: rgba(255, 255, 255, 0.4); -} -#menu ul li a:active { - color: #f4af30; - background: #9da3c6; - background: rgba(255, 255, 255, 0.4); -} -#menu #miniLogo { - float: left; - position: relative; - left: 3px; - top: -1px; - z-index: 10; -} -#menu #socialBtn { - float: right; - position: relative; - top: -23px; - left: -2px; -} -#menu #socialBtn a { - text-decoration: none; -} -#socialBtn a:link, -#socialBtn a:visited { - opacity: 0.8; -} -#socialBtn a:active, -#socialBtn a:hover { - opacity: 1; -} -#grid { - padding: 15px 0 15px 12px; -} -#grid section, -#grid nav { - position: relative; -} -#grid section ul, -#grid nav ul, -#grid section ol, -#grid nav ol, -#grid section dl, -#grid nav dl { - overflow: auto; -} -#grid section h1, -#grid nav h1 { - font-size: 150%; - margin: 0; -} -#grid section h2, -#grid nav h2 { - font-size: 120%; - margin: 0; -} -#grid section h3, -#grid nav h3 { - margin: 0; -} -#grid section footer, -#grid nav footer { - position: absolute; - bottom: 0; - width: 100%; -} -#grid section footer .more, -#grid nav footer .more { - position: absolute; - bottom: 0; - right: 10px; - display: block; - width: 70px; - height: 20px; - text-align: center; - text-decoration: none; - color: #fff; - -moz-border-radius-topleft: 10px; - border-top-left-radius: 10px; -} -#grid > article, -#grid > header, -#grid #content-body { - margin-right: 12px; -} -/* - * Grid layout - */ -.box { - float: left; - margin: 0 10px 10px 0; - padding: 0; -} -.box > * { - padding: 0 5px; -} -.box.height-1 { - height: 225px; -} -.box.width-1 { - width: 325px; -} -.box.width-1#banner { - width: 335px; -} -.box.height-2 { - height: 465px; -} -.box.width-2 { - width: 670px; -} -.box.width-2#banner { - width: 680px; -} -.box.width-3 { - width: 1015px; -} -.box.width-3#banner { - width: 1025px; -} -/* - * Boxes - */ -/*.box { - background: #dddddd; - border: solid 5px #787c91; - border-top: none; -}*/ -.box h1 { - color: white; -} -#message { - border-color: #787c91; -} -#message h1 { - background: #787c91; -} -#message h1 { - background: #787c91; -} -#message #comments { - margin-left: 3px; - resize: none; - width: 304px; - height: 72px; -} -#message input[type="submit"] { - margin-left: 98px; -} -#message dt { - float: left; -} -#message dd { - margin-left: 50px; -} -#banner { - background: #874e81; - padding: 0; - border: none; - margin-bottom: 15px; -} -#banner .box-view { - padding: 0; -} -#schedule { - border-color: #2d5d9f; -} -#schedule h1 { - background: #2d5d9f; -} -#schedule h1 { - background: #2d5d9f; -} -#schedule dt { - float: left; -} -#schedule dd, -#schedule span { - margin-left: 70px; -} -#news { - border-color: #683146; -} -#news h1 { - background: #683146; -} -#news h1 { - background: #683146; -} -#speech { - border-color: #396c66; -} -#speech h1 { - background: #396c66; -} -#speech h1 { - background: #396c66; -} -#music, -#chart { - border-color: #57246f; -} -#music h1, -#chart h1 { - background: #57246f; -} -#music h1, -#chart h1 { - background: #57246f; -} -table.musicChart { - font-size: 13px; - width: 100%; -} -#photo { - background: #787c91; - height: 90px; - border-color: #787c91; -} -#podcast { - border-color: #2d5d9f; -} -#podcast h1 { - background: #2d5d9f; -} -#podcast h1 { - background: #2d5d9f; -} -#podcast .box-view h1 { - background: none; -} -#podcast .podTitle { - clear: both; - padding-top: 5px; -} -#podcast .podImg { - float: left; - width: 65px; - height: 65px; - margin: 0 5px 0 0; -} -#podcast .podImg img { - width: 65px; - height: 65px; -} -#video { - border-color: #683146; -} -#video h1 { - background: #683146; -} -#video h1 { - background: #683146; -} -#video .box-view { - padding: 0; -} -#video #youtube-footer { - position: relative; - margin: 0 10px; -} -#video #youtube-footer #youtube-pic { - display: inline-block; - text-align: center; - vertical-align: bottom; - width: 88px; -} -#video #youtube-footer #youtube-sub { - position: absolute; - top: 0; - right: 0; -} -#video #youtube-footer #youtube-lnk { - position: absolute; - bottom: 0; - right: 0; -} -#social { - border-color: #396c66; -} -#social h1 { - background: #396c66; -} -#social h1 { - background: #396c66; -} -#on_air { - width: 225px; - border: none; - background: none; - display: inline-block; - vertical-align: top; -} -.background-team-generic { - border-color: #c8d2ee; -} -.background-team-generic h1 { - background: #c8d2ee; -} -.background-team-filler { - border-color: #787c91; -} -.background-team-filler h1 { - background: #787c91; -} -.background-team-presenters { - border-color: #2d5d9f; -} -.background-team-presenters h1 { - background: #2d5d9f; -} -.background-team-music { - border-color: #57246f; -} -.background-team-music h1 { - background: #57246f; -} -.background-team-news { - border-color: #683146; -} -.background-team-news h1 { - background: #683146; -} -.background-team-speech { - border-color: #396c66; -} -.background-team-speech h1 { - background: #396c66; -} -.media-list-common { - list-style-type: none; - list-style-position: outside; - padding: 0; - padding-top: 5px; - margin: 0; -} -.media-list-common ul { - padding: 0; - margin: 0; -} -.media-list-common ul li { - /* Use this and the first/last-child stuff to separate items. */ - - margin-bottom: 5px; - position: relative; - top: 0; - border-radius: 5px; - overflow: hidden; -} -.media-list-common ul li > a { - display: block; - background: #363d5f; - padding: 5px; - text-decoration: none; - border: none; - color: #ffffff; -} -.media-list-common ul li > a:hover { - background: #5c67a0; -} -.media-list-common ul li:first-child { - margin-top: 0; -} -.media-list-common ul li:last-child { - margin-bottom: 0; -} -.media-list-common ul li h1 { - font-size: 120%; - margin: 0px; -} -.media-list-common ul li p.media-description { - margin-bottom: 17px; -} -.media-list-common ul li p.media-time { - position: absolute; - font-size: 12px; - bottom: 5px; - right: 5px; - margin: 0; - opacity: 0.7; -} -.media-list { - list-style-type: none; - list-style-position: outside; - padding: 0; - padding-top: 5px; - margin: 0; -} -.media-list ul { - padding: 0; - margin: 0; -} -.media-list ul li { - /* Use this and the first/last-child stuff to separate items. */ - - margin-bottom: 5px; - position: relative; - top: 0; - border-radius: 5px; - overflow: hidden; -} -.media-list ul li > a { - display: block; - background: #363d5f; - padding: 5px; - text-decoration: none; - border: none; - color: #ffffff; -} -.media-list ul li > a:hover { - background: #5c67a0; -} -.media-list ul li:first-child { - margin-top: 0; -} -.media-list ul li:last-child { - margin-bottom: 0; -} -.media-list ul li h1 { - font-size: 120%; - margin: 0px; -} -.media-list ul li p.media-description { - margin-bottom: 17px; -} -.media-list ul li p.media-time { - position: absolute; - font-size: 12px; - bottom: 5px; - right: 5px; - margin: 0; - opacity: 0.7; -} -.media-list .thumbnail { - width: 120px; - height: 120px; - position: absolute; - top: 5px; - left: 5px; -} -.media-list .thumbnail-flush { - min-height: 120px; - padding-left: 130px; -} -.mini-media-list { - list-style-type: none; - list-style-position: outside; - padding: 0; - padding-top: 5px; - margin: 0; -} -.mini-media-list ul { - padding: 0; - margin: 0; -} -.mini-media-list ul li { - /* Use this and the first/last-child stuff to separate items. */ - - margin-bottom: 5px; - position: relative; - top: 0; - border-radius: 5px; - overflow: hidden; -} -.mini-media-list ul li > a { - display: block; - background: #363d5f; - padding: 5px; - text-decoration: none; - border: none; - color: #ffffff; -} -.mini-media-list ul li > a:hover { - background: #5c67a0; -} -.mini-media-list ul li:first-child { - margin-top: 0; -} -.mini-media-list ul li:last-child { - margin-bottom: 0; -} -.mini-media-list ul li h1 { - font-size: 120%; - margin: 0px; -} -.mini-media-list ul li p.media-description { - margin-bottom: 17px; -} -.mini-media-list ul li p.media-time { - position: absolute; - font-size: 12px; - bottom: 5px; - right: 5px; - margin: 0; - opacity: 0.7; -} -.mini-media-list li h1 { - font-size: 100% !important; -} -.mini-media-list .thumbnail { - width: 48px; - height: 48px; - position: absolute; - top: 5px; - left: 5px; -} -.mini-media-list .thumbnail-flush { - min-height: 48px; - padding-left: 58px; -} -.media-list-background-text { - position: absolute; - bottom: -16px; - right: 0px; - z-index: 0; - font-size: 60px; - opacity: 0.2; - padding: 0; - margin: 0; -} -/* LISTS */ -aside nav ul, -section.actions ul { - list-style-type: none; - list-style-position: outside; - margin: 0; - padding: 0; -} -aside nav li, -section.actions li { - margin-bottom: 5px; -} -aside nav li > *, -section.actions li > * { - display: block; - padding: 5px; -} -aside nav li > *:before, -section.actions li > *:before { - content: "\00BB "; -} -aside nav li:last-child, -section.actions li:last-child { - margin-bottom: 0; -} -#podcast-area { - padding-top: 12px; - text-align: center; -} -form.embed { - display: inline; -} -.schedule-header { - position: relative; - top: 0; - left: 0; - height: 30px; - display: block; -} -.schedule-header nav { - height: 100%; -} -.schedule-header h2 { - position: absolute; - width: 100%; - text-align: center; - margin: 0; -} -.schedule-header-quicklinks { - position: absolute; - right: 0; - text-align: right; - top: -60px; -} -.schedule-header-quicklinks li { - padding: 0 5px; - display: inline; -} -.schedule-header-quicklinks li.rsep { - margin-right: 12px; -} -.schedule-header-prev { - position: absolute; - left: 5px; - bottom: 0px; -} -.schedule-header-next { - position: absolute; - right: 5px; - bottom: 0px; -} -.schedule-common { - color: #ffffff; -} -.schedule-common a { - color: #feb93a; - text-decoration: none; - -webkit-text-shadow: 1px 2px black; - -khtml-text-shadow: 1px 2px black; - -moz-text-shadow: 1px 2px black; - text-shadow: 1px 2px black; -} -.schedule-common a:hover { - color: #ffd758; - text-decoration: underline; -} -.schedule-common a:visited { - color: #f4af30; -} -.schedule-common a:active { - color: #f4af30; -} -.schedule-common a { - color: #feb93a; - text-decoration: none; - -webkit-text-shadow: 1px 2px black; - -khtml-text-shadow: 1px 2px black; - -moz-text-shadow: 1px 2px black; - text-shadow: 1px 2px black; -} -.schedule-common a:hover { - color: #ffd758; - text-decoration: underline; -} -.schedule-common a:visited { - color: #f4af30; -} -.schedule-common a:active { - color: #f4af30; -} -/* -- DAY SCHEDULE */ -ol#schedule-day { - list-style-type: none; - list-style-position: outside; - padding: 0; - padding-top: 5px; - margin: 0; - color: #ffffff; -} -ol#schedule-day ul { - padding: 0; - margin: 0; -} -ol#schedule-day ul li { - /* Use this and the first/last-child stuff to separate items. */ - - margin-bottom: 5px; - position: relative; - top: 0; - border-radius: 5px; - overflow: hidden; -} -ol#schedule-day ul li > a { - display: block; - background: #363d5f; - padding: 5px; - text-decoration: none; - border: none; - color: #ffffff; -} -ol#schedule-day ul li > a:hover { - background: #5c67a0; -} -ol#schedule-day ul li:first-child { - margin-top: 0; -} -ol#schedule-day ul li:last-child { - margin-bottom: 0; -} -ol#schedule-day ul li h1 { - font-size: 120%; - margin: 0px; -} -ol#schedule-day ul li p.media-description { - margin-bottom: 17px; -} -ol#schedule-day ul li p.media-time { - position: absolute; - font-size: 12px; - bottom: 5px; - right: 5px; - margin: 0; - opacity: 0.7; -} -ol#schedule-day .thumbnail { - width: 120px; - height: 120px; - position: absolute; - top: 5px; - left: 5px; -} -ol#schedule-day .thumbnail-flush { - min-height: 120px; - padding-left: 130px; -} -ol#schedule-day a { - color: #feb93a; - text-decoration: none; - -webkit-text-shadow: 1px 2px black; - -khtml-text-shadow: 1px 2px black; - -moz-text-shadow: 1px 2px black; - text-shadow: 1px 2px black; -} -ol#schedule-day a:hover { - color: #ffd758; - text-decoration: underline; -} -ol#schedule-day a:visited { - color: #f4af30; -} -ol#schedule-day a:active { - color: #f4af30; -} -ol#schedule-day a { - color: #feb93a; - text-decoration: none; - -webkit-text-shadow: 1px 2px black; - -khtml-text-shadow: 1px 2px black; - -moz-text-shadow: 1px 2px black; - text-shadow: 1px 2px black; -} -ol#schedule-day a:hover { - color: #ffd758; - text-decoration: underline; -} -ol#schedule-day a:visited { - color: #f4af30; -} -ol#schedule-day a:active { - color: #f4af30; -} -#schedule-day li { - position: relative; -} -#schedule-day li:first-child { - margin-top: 0; -} -#schedule-day li:last-child { - margin-bottom: 0; -} -#schedule-day li { - /* Colouring is done by the block-xyz classes. */ - -} -#schedule-day li p { - margin: 5px; -} -.schedule-day-show-time { - position: absolute; - top: 5px; - right: 5px; - margin: 0; - font-size: 24pt; - opacity: 0.8; -} -.schedule-day-show-title { - margin: 0; - font-size: 20pt; -} -.schedule-day-show-by-line { - margin: 0; - margin-top: -4px; - margin-left: 20px; - font-size: 18px; - font-style: italic; - opacity: 0.8; -} -.schedule-day-block-name { - position: absolute; - bottom: -16px; - right: 0px; - z-index: 0; - font-size: 60px; - opacity: 0.2; - padding: 0; - margin: 0; -} -/* -- WEEK SCHEDULE */ -table#schedule-week-table { - background: none repeat scroll 0% 0% rgba(0, 0, 0, 0.4); - table-layout: fixed; - margin: 0 auto; - padding-bottom: 5px; - position: relative; - right: 12px; - top: 15px; - width: 1050px; - border-collapse: separate; - border-spacing: 5px 1px; - color: #ffffff; -} -table#schedule-week-table thead tr th { - padding: 5px; -} -table#schedule-week-table td { - padding: 5px; - font-size: 14px; - text-align: left; - vertical-align: top; - width: 130px; -} -table#schedule-week-table a { - color: #feb93a; - text-decoration: none; - -webkit-text-shadow: 1px 2px black; - -khtml-text-shadow: 1px 2px black; - -moz-text-shadow: 1px 2px black; - text-shadow: 1px 2px black; -} -table#schedule-week-table a:hover { - color: #ffd758; - text-decoration: underline; -} -table#schedule-week-table a:visited { - color: #f4af30; -} -table#schedule-week-table a:active { - color: #f4af30; -} -table#schedule-week-table a { - color: #feb93a; - text-decoration: none; - -webkit-text-shadow: 1px 2px black; - -khtml-text-shadow: 1px 2px black; - -moz-text-shadow: 1px 2px black; - text-shadow: 1px 2px black; -} -table#schedule-week-table a:hover { - color: #ffd758; - text-decoration: underline; -} -table#schedule-week-table a:visited { - color: #f4af30; -} -table#schedule-week-table a:active { - color: #f4af30; -} -.schedule-week-time { - width: 30px; - padding: 5px; - vertical-align: top; -} -.schedule-week-hour { - border-top: 2px solid rgba(0, 0, 0, 0.4); - font-size: 20px; -} -.schedule-week-non-hour { - font-size: 16px; - color: rgba(255, 255, 255, 0.4); -} -.schedule-week-continued { - display: none; -} -/* BLOCKS */ -.background-team-generic { - border-color: #c8d2ee; -} -.background-team-generic h1 { - background: #c8d2ee; -} -.background-team-filler { - border-color: #787c91; -} -.background-team-filler h1 { - background: #787c91; -} -.background-team-presenters { - border-color: #2d5d9f; -} -.background-team-presenters h1 { - background: #2d5d9f; -} -.background-team-music { - border-color: #57246f; -} -.background-team-music h1 { - background: #57246f; -} -.background-team-news { - border-color: #683146; -} -.background-team-news h1 { - background: #683146; -} -.background-team-speech { - border-color: #396c66; -} -.background-team-speech h1 { - background: #396c66; -} -.block-default { - background: #71737C; -} -.block-jukebox { - background: #191C2C; -} -.block-special { - background: #34A5AD; -} -.block-flagship { - background: #2d5d9f; -} -.block-music { - background: #57246f; -} -.block-news { - background: #683146; -} -.block-speech { - background: #396c66; -} -/* SHOWDB */ -.schedule-showdb-hgroup > h2 { - margin: 0 0 0 0; -} -.schedule-showdb-hgroup > h3 { - margin: 0 0 0 20px; -} -/* BANNERS */ -img.banner-img { - width: 100%; - height: 100%; -} -/******************************************************************* - * * - * LESS for home page Twitter feed * - * * - * Please respect those coders that come after you by keeping this * - * tidy and consistent, and your class names meaningful even if * - * their style completely changes later. * - * * - * Based on the seaofclouds.com Twitter script CSS * - * * - * Thanks! * - * * - *******************************************************************/ -/* See main LESS file (main.less) for variables. */ -.colour-links a { - color: #feb93a; - text-decoration: none; - -webkit-text-shadow: 1px 2px black; - -khtml-text-shadow: 1px 2px black; - -moz-text-shadow: 1px 2px black; - text-shadow: 1px 2px black; -} -.colour-links a:hover { - color: #ffd758; - text-decoration: underline; -} -.colour-links a:visited { - color: #f4af30; -} -.colour-links a:active { - color: #f4af30; -} -.tweet .tweet_list, -.query .tweet_list { - list-style-type: none; - margin: 0; - padding: 0; - overflow-y: hidden; -} -.tweet .tweet_list li, -.query .tweet_list li { - overflow-y: auto; - overflow-x: hidden; - padding: 0; -} -.tweet .tweet_list .awesome, -.query .tweet_list .awesome, -.tweet .tweet_list .epic, -.query .tweet_list .epic { - text-transform: uppercase; -} -.tweet .tweet_list .tweet_even, -.query .tweet_list .tweet_even { - background-color: #363d5f; -} -.tweet .tweet_list .tweet_avatar, -.query .tweet_list .tweet_avatar { - padding-right: .5em; - float: left; -} -.tweet .tweet_list .tweet_avatar img, -.query .tweet_list .tweet_avatar img { - vertical-align: middle; -} -/* - * Blogs - */ -nav ul.breadcrumbs { - margin: 0; - padding: 0; -} -nav ul.breadcrumbs li { - display: inline-block; -} -nav ul.breadcrumbs li:after { - content: "\00BB"; -} -nav ul.breadcrumbs li:last-of-type:after { - content: ""; -} -.blog #sidebar { - padding: 0; -} -.pluginBoxContainer { - border: none !important; -} -#content-header h2, -article > header h1 { - font-size: 2em; - text-transform: none; - margin: 0; - border-bottom: 5px solid #363d5f; -} -article header, -article aside { - color: #ffffff; -} -article header a, -article aside a { - color: #feb93a; - text-decoration: none; - -webkit-text-shadow: 1px 2px black; - -khtml-text-shadow: 1px 2px black; - -moz-text-shadow: 1px 2px black; - text-shadow: 1px 2px black; -} -article header a:hover, -article aside a:hover { - color: #ffd758; - text-decoration: underline; -} -article header a:visited, -article aside a:visited { - color: #f4af30; -} -article header a:active, -article aside a:active { - color: #f4af30; -} -article header a, -article aside a { - color: #feb93a; - text-decoration: none; - -webkit-text-shadow: 1px 2px black; - -khtml-text-shadow: 1px 2px black; - -moz-text-shadow: 1px 2px black; - text-shadow: 1px 2px black; -} -article header a:hover, -article aside a:hover { - color: #ffd758; - text-decoration: underline; -} -article header a:visited, -article aside a:visited { - color: #f4af30; -} -article header a:active, -article aside a:active { - color: #f4af30; -} -#content-header { - color: #ffffff; -} -#content-body { - background: #dddddd; - padding: 10px 15px; -} -.article-body { - background: #dddddd; -} -/* ========================================================================== - URY layout classes - ========================================================================== */ -div.pageAside { - float: left; - width: 800px; -} -aside.rightAside { - float: right; - width: 200px; - padding-left: 25px; -} -div#left, -div#right { - display: table-cell; - width: 500px; -} -div#left { - margin-left: 0; - margin-right: auto; - padding-right: 10px; -} -div#right { - margin-left: auto; - margin-right: 0; - padding-left: 10px; -} -/* ========================================================================== - URY style classes - ========================================================================== */ -.transBG { - background: #363d5f; -} -.transHovBG { - background: #363d5f; -} -.transHovBG:hover { - background: #5c67a0; -} -a.transHovBG:hover { - text-decoration: none; - border: none; -} -.roundy { - -moz-border-radius: 5px; - border-radius: 5px; -} -.image-heading { - margin: 0; -} -/* ========================================================================== - Helper classes - ========================================================================== */ -/* - * Image replacement - */ -.ir { - background-color: transparent; - border: 0; - overflow: hidden; - /* IE 6/7 fallback */ - - *text-indent: -9999px; -} -.ir:before { - content: ""; - display: block; - width: 0; - height: 100%; -} -/* - * Hide from both screenreaders and browsers: h5bp.com/u - */ -.hidden { - display: none !important; - visibility: hidden; -} -/* - * Hide only visually, but have it available for screenreaders: h5bp.com/v - */ -.visuallyhidden { - border: 0; - clip: rect(0 0 0 0); - height: 1px; - margin: -1px; - overflow: hidden; - padding: 0; - position: absolute; - width: 1px; -} -/* - * Extends the .visuallyhidden class to allow the element to be focusable - * when navigated to via the keyboard: h5bp.com/p - */ -.visuallyhidden.focusable:active, -.visuallyhidden.focusable:focus { - clip: auto; - height: auto; - margin: 0; - overflow: visible; - position: static; - width: auto; -} -/* - * Hide visually and from screenreaders, but maintain layout - */ -.invisible { - visibility: hidden; -} -/* - * Clearfix: contain floats - * - * For modern browsers - * 1. The space content is one way to avoid an Opera bug when the - * `contenteditable` attribute is included anywhere else in the document. - * Otherwise it causes space to appear at the top and bottom of elements - * that receive the `clearfix` class. - * 2. The use of `table` rather than `block` is only necessary if using - * `:before` to contain the top-margins of child elements. - */ -.clearfix:before, -.clearfix:after { - content: " "; - /* 1 */ - - display: table; - /* 2 */ - -} -.clearfix:after { - clear: both; -} -/* - * For IE 6/7 only - * Include this rule to trigger hasLayout and contain floats. - */ -.clearfix { - *zoom: 1; -} -/* ========================================================================== - EXAMPLE Media Queries for Responsive Design. - Theses examples override the primary ('mobile first') styles. - Modify as content requires. - ========================================================================== */ -/* ========================================================================== - Print styles. - Inlined to avoid required HTTP connection: h5bp.com/r - ========================================================================== */ -@media print { - * { - background: transparent !important; - color: #000 !important; - /* Black prints faster: h5bp.com/s */ - - box-shadow: none !important; - text-shadow: none !important; - } - a, - a:visited { - text-decoration: underline; - } - a[href]:after { - content: " (" attr(href) ")"; - } - abbr[title]:after { - content: " (" attr(title) ")"; - } - /* - * Don't show links for images, or javascript/internal links - */ - .ir a:after, - a[href^="javascript:"]:after, - a[href^="#"]:after { - content: ""; - } - pre, - blockquote { - border: 1px solid #999; - page-break-inside: avoid; - } - thead { - display: table-header-group; - /* h5bp.com/t */ - - } - tr, - img { - page-break-inside: avoid; - } - img { - max-width: 100% !important; - } - @page { - margin: 0.5cm; - } - p, - h2, - h3 { - orphans: 3; - widows: 3; - } - h2, - h3 { - page-break-after: avoid; - } -} diff --git a/src/Public/css/myradio.webcamfocus.css b/src/Public/css/myradio.webcamfocus.css new file mode 100644 index 000000000..ccbbd4bcf --- /dev/null +++ b/src/Public/css/myradio.webcamfocus.css @@ -0,0 +1,11 @@ +.webcam-stream-container{ + cursor: pointer; + margin: 10px; +} + +.webcam-stream-container:hover{ + -webkit-box-shadow: 0px 0px 0px 3px red; + -moz-box-shadow: 0px 0px 0px 3px red; + box-shadow: 0px 0px 0px 3px red; + border-radius: 3px; +} \ No newline at end of file diff --git a/src/Public/css/normalise.css b/src/Public/css/normalise.css old mode 100755 new mode 100644 index 562891ab8..c2de8df94 --- a/src/Public/css/normalise.css +++ b/src/Public/css/normalise.css @@ -403,4 +403,4 @@ textarea { table { border-collapse: collapse; border-spacing: 0; -} \ No newline at end of file +} diff --git a/src/Public/css/planner.css b/src/Public/css/planner.css index 090d58c2a..fec806f75 100644 --- a/src/Public/css/planner.css +++ b/src/Public/css/planner.css @@ -1,75 +1,113 @@ +html { + height: 100%; +} + body { width: 98%; min-width: 1200px; - height: calc(100% - 22px); + height: calc(100% - 42px); + margin: 20px auto 0 auto; } -#notice { - position: fixed; - top: 22px; - left: 0; +.main-container, #baps-channel-container { + height: calc(100% - 60px); + width: 100%; } #grid { - height: calc(100% - 150px); - min-height: 580px; + height: calc(100% - 100px); + min-height: 280px; min-width: 1188px; position: relative; } + #bapswrapper { - height: calc(100% - 30px); + height: 100%; min-width: 1200px; } -#baps-channel-container { - height: 90%; + +#resource-add-container { + padding: 0; } #resource-add-header { - height: 88px; + background-color: #777; + height:92px; + padding-top: 5px; } -#central-dragdrop, #res-dragdrop { - height: 300px; +#resource-add-header .tt-hint { + margin: 0 0 0 2px; + height: 24px; + border: none; + width: 100%; +} +#resource-add-header .tt-dropdown-menu { + margin-left: 0; } -#lastfm-attr { + +#central-dragdrop, #res-dragdrop { + height: 150px; + margin: 0; position: relative; - top: calc(100% - 60px); - right: 1em; - text-align: right; } +#central-status, #res-status { + position: absolute; + bottom:0; + left: 0; + width: 100%; + min-height: 32px; + padding: 5px 15px; + margin-bottom: 0; +} +#res-type-sel { + margin: 0 2px 5px 1px; + width: calc(100% - 3px); +} +.result-container .alert { + width: 100%; + padding: 5px 15px 3px 15px; + margin: 5px 0; +} +.result-container .alert label { + margin-top: 6px; +} +.result-container .alert-dismissable { + padding-right: 35px; +} +/* Spin while saving tracks */ +.gly-spin { + animation: spin 2s infinite linear; +} +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(359deg); + } +} + #baps-channel-res { - width: calc(100% - 11px); height: calc(100% - 90px); } -#a-manage-library { +#baps-menu { float: right; margin: 0; position: relative; - right: 20px; - top: 0.5em; } - -#res-loading { - float: right; - font-size: smaller; - padding: 1px; - margin: 2px; +#baps-menu .btn-group { + margin: -2px 0 0 0; } -#res-filter-artist, #res-filter-track, #res-filter-name { - padding: 0; - width: calc(100% - 12px); +#baps-menu #showAlert { + display: inline-block; + padding: 6px 15px; + margin: 0 10px; } -#footer-tips { - position: absolute; - bottom: 0; - left: 0; +#res-filter-artist, #res-filter-track, #res-filter-name, #resource-add-header .twitter-typeahead { + padding: 0; width: 100%; } - - -.box.width-1 { - width: calc(23% - 10px); -} -.box.width-2 { - width: calc(30% - 10px); +#res-filter-name { + margin-top:24px; } .box.height-1 { height: 75%; @@ -79,13 +117,29 @@ body { margin-bottom: 0; } -ul.baps-channel { +ul.baps-channel, form.baps-channel { overflow-x: hidden; overflow-y: scroll; + padding: 0; + margin: 0; + background-color: #eee; + border: 1px solid #777; + position: static; /* override bootstrap relative, which breaks sortable */ } div.baps-channel { text-align: center; + background-color: #333; + color: #FFF; + border: 1px solid #777; + border-top: none; + cursor: default; + -webkit-user-select: none; + user-select: none; +} + +div.baps-channel a { + color: #FFF; } ul.baps-channel li { @@ -96,34 +150,207 @@ ul.baps-channel li { } ul.baps-channel li:hover { - background-color: burlywood; + background-color: #5bc0de; } ul.baps-channel li.selected { - background-color: #0070a3; + background-color: #428bca; } +select { + color: #333; + width: 100%; +} + +#track-manual-entry label { + width: 150px; +} +/** Iframe Popups **/ -div[id^=progress-bar] { - margin: 0 0.4em 0.2em; +#iframe { + background: none; + box-shadow: none; + width: 100%; + min-width: 0; + height: auto; + margin:0; } -/** Thin handled slider **/ -.ui-slider .ui-slider-handle { - width: .2em; + +/** SLIDER COMPONENT **/ + +.playout-slider-container { + margin-top: 5px; + width: 100%; + height: 26px; + text-align: left; } -.ui-slider-horizontal .ui-slider-handle { - margin-left: -.1em; + +.playout-slider-container .playout-slider { + background-color: #eee; + border: 1px solid #428bca; + border-radius: 3px; + width: 100%; + height: 100%; + position: relative; + cursor: pointer; } -a, a:active, a:visited { - color: #FFF; +.playout-slider div { + display: inline-block; + position: absolute; +} + +.playout-slider .playout-slider-cue, +.playout-slider .playout-slier-intro { + border-top-left-radius: 2px; + border-bottom-left-radius: 2px; +} + +.playout-slider .playout-slider-cue { + background-color: #d9534f; + top: 0; + height: 100%; +} + +.playout-slider .playout-slider-intro { + background-color: #5bc85c; + top: 3px; + height: calc(100% - 6px); } -.ui-state-error{ - display : block; +.playout-slider .playout-slider-position { + border-top: 12px solid rgba(66, 139, 202, 0.4); + border-bottom: 12px solid rgba(66, 139, 202, 0.4); + top: 0; + height: 4px; + max-width: 100%; +} + +.playout-slider .playout-slider-position .playout-slider-line { + background-color: #2376bb; + height: 4px; + width: 100%; + position: absolute; + top: -2px; +} + +.playout-slider .playout-slider-position .playout-handle { + position: absolute; + right: 0; + top: -13px; + height: 26px; + width: 3px; + background-color: #0a5a9c; +} + +.playout-slider:hover .playout-slider-position .playout-handle { + right: -2px; + width: 7px; +} + +.playout-slider .playout-slider-intro .playout-handle { + position: absolute; + right: 0; + top: -3px; + height: 24px; + width: 2px; + background-color: #5bc85c; +} + +.playout-slider .playout-slider-intro .playout-handle { + position: absolute; + right: 0; + top: -3px; + height: 24px; + width: 2px; + background-color: inherit; +} + +.playout-slider .playout-slider-intro:hover .playout-handle, +.playout-slider .playout-slider-intro:active .playout-handle { + height: 30px; +} + +.playout-slider .playout-slider-intro .playout-handle .playout-handle-circle { + width: 10px; + height: 10px; + position: absolute; + bottom: -8px; + left: -4px; +} + +.playout-slider .playout-slider-cue .playout-handle, +.playout-slider .playout-slider-cue .playout-handle { + position: absolute; + right: 0; + height: 30px; + width: 2px; + top: -5px; +} + +.playout-slider .playout-slider-intro:hover .playout-handle .playout-handle-circle, +.playout-slider .playout-slider-intro:active .playout-handle .playout-handle-circle { + border: 2px solid #5bc85c; + border-radius: 5px; +} + +.playout-slider .playout-slider-cue:hover .playout-handle, +.playout-slider .playout-slider-cue:active .playout-handle { + background-color: inherit; +} + +.playout-slider .playout-slider-cue .playout-handle .playout-handle-circle { + width: 10px; + height: 10px; + position: absolute; + top: -8px; + left: -4px; +} + +.playout-slider .playout-slider-cue:hover .playout-handle .playout-handle-circle, +.playout-slider .playout-slider-cue:active .playout-handle .playout-handle-circle { + border: 2px solid #d9534f; + border-radius: 5px; +} + + +.context-menu { + display: none; + position: absolute; + z-index: 100; +} + +.context-menu--active { + display: block; +} + +#import-show-selector, #import-season-selector, +#import-timeslot-selector, #import-channel-selector, +#import-channel-list, #import-import-to-channel-selector { + margin-bottom:5px; +} +#import-channel-list { + padding: 5px; + border-radius: 4px; +} +#import-channel-list p { + margin: 0 20px; +} +#import-channel-list li { + list-style: none; + padding-left: 5px; +} +#import-channel-list li.disabled { + background: lightpink; +} +#import-channel-list input[type=checkbox] { + margin-right: 5px; +} +#import-channel-filter-btns { + display: inline-block; +} +#showAlert a { + cursor: pointer; } -.ui-contextmenu { - z-index: 10000; -} \ No newline at end of file diff --git a/src/Public/css/sis.css b/src/Public/css/sis.css old mode 100755 new mode 100644 index 624254806..951368a38 --- a/src/Public/css/sis.css +++ b/src/Public/css/sis.css @@ -1,82 +1,42 @@ -body { - width: 98%; - min-width: 1200px; - height: calc(100% - 22px); - overflow-y: hidden; +.main-container.container { + width: calc(100% - 10px); } #notice { position: fixed; - top: 22px; + top: 50px; left: 0; } #grid { - height: calc(100% - 150px); - min-height: 580px; - min-width: 1188px; + min-width: 250px; position: relative; } -.h-link { - float: right; - margin: 0; - position: relative; - right: 15px; - top: 0.7em; -} +@media (min-width: 992px) { + @media (min-height: 900px) { + #grid { + height: 725px; + } -#hide-help { - float: right; -} + #grid > div, #grid > div > div.tab-content { + height: 100%; + } + } -.pluginpane { - float: left; - width: 300px; - margin-right: 5px -} -.pluginpane .plugin:first-of-type .pluginhead { - margin-top: 0; -} -.pluginhead { - font-weight:bold; - cursor:pointer; - height:1.5em -} -.pluginbody { - overflow-x: hidden; - font-size: 12px -} -.ui-accordion .ui-accordion-content { - padding: 1em 2em; - overflow-x: hidden; + .tab-content .tab-pane { + height: 100%; + overflow-y: auto; + } } +.hr { margin:.5em 0 0 0; } -.maincontainer { - overflow: hidden; - height: 100%; - margin-right: 10px; +#presenterinfo { + padding-top: 15px; } -.ui-tabs { - height: calc(100% - .4em - 2px); - padding: 0; -} -.ui-tabs .ui-tabs-panel { - overflow-y: auto; - overflow-x: hidden; - height: calc(100% - 2em - 0.5em - 48px); -} - -.ui-resizable-s { height: 10px; bottom: 0; } - -.list { text-align:left; list-style-position:outside; list-style-type:none; } -.infobox { width:350px; margin:auto; padding:1em; font-size:0.8em; text-align:justify; font-style:italic; } - -.errorbox { margin:auto; padding-bottom:1em; font-size:0.8em; font-style:italic; } -.hr { margin:.5em 0 0 0; } -#piss footer { +#presenterinfo footer { text-align: right; font-style: italic; } @@ -86,22 +46,32 @@ body { width: 100%; } .td-msgitem td:first-child { - width: 50px; + visibility: hidden; + width: 15px; +} +.td-msgitem.unread td:first-child { + visibility: visible; +} +.td-msgitem td:nth-child(2) { + width: 15px; + padding-right: 35px; } -table.messages tr.unread td:first-child { - background-image: url(../img/sis/6.png); - background-repeat: no-repeat; - background-position-y: 50%; +.td-msgitem .unread-dot { + border-radius: 100%; + height:1em; + width:1em; + background-color: #2aaef5; } + table.messages tr td:last-child { width: 100px; } -table { +table { margin: auto; - width: 90%; + width: 100%; border-collapse: separate; - border-spacing: 0.7em 0.5em; + border-spacing: 10px; } input.text { margin-bottom:12px; width:95%; padding: .4em; } @@ -109,27 +79,47 @@ fieldset { padding:0; border:0; margin-top:25px; } /** Selector **/ -#selector-buttons { - margin: 0 auto; - border-spacing:2em 0; +#selector-buttons { + margin: 0 auto; + border-spacing: 20px 0; margin-bottom: .5em; border-collapse: separate; + max-width: 250px; + } -.selbtn { height:2em; width:2em; } -.poweredon { cursor:pointer; } -.s4on { background-color: #6b00a3; } -.s4off { background-color: #5f0093; } -.s3on { background-color: #0f0; } -.s3off { background-color: #080; } -.s2on { background-color: #0044ba; } -.s2off { background-color: #003c76; } -.s1on { background-color: #f00; } -.s1off { background-color: #600; } -.poweredoff { background-color: #333; } - -#message-data, #tracklist-data { - display: none; +.selbtn { + height: 35px; + min-width: 30px; + background-color: #ddd; + text-align: center; + margin: 5px; + font-size: 1.2em; + padding: 5px 0; + color: #fff; + display: inline-block; } +.poweredon { cursor:pointer; } +.s1on, .s2on, .s3on, .s4on, .s5on, .s8on { + border: 5px solid gold; + height: 25px; + box-sizing: content-box; +} +.poweredon.s1on { background-color: #f00; } +.poweredon.s1off { background-color: #600; } +.poweredon.s2on { background-color: #0044ba; } +.poweredon.s2off { background-color: #003c76; } +.poweredon.s3on { background-color: #0f0; } +.poweredon.s3off { background-color: #080; } +.poweredon.s4on { background-color: #6b00a3; } +.poweredon.s4off { background-color: #5f0093; } +.poweredon.s5on { background-color: #b5009b; } +.poweredon.s5off { background-color: #650058; } +.poweredon.s8on { background-color: #ffa500; } +.poweredon.s8off { background-color: #9c6600; } + +.poweredoff { background-color: #ddd; } + + .td-msgitem, .delbutton { cursor: pointer; } @@ -138,13 +128,18 @@ fieldset { padding:0; border:0; margin-top:25px; } figure { margin: 0; } -.webcam-stream-container { - display: block; +figure.webcam-stream-container { + width: 70px; + display: inline-block; + vertical-align: top; } -.webcam-stream-container.live { +figure.webcam-stream-container.live { + width: 150px; float: right; } - +figure.webcam-stream-container img { + width: 100%; +} #customcam { margin-top: 0.5em; } @@ -152,24 +147,30 @@ figure { margin-right: 0.5em; } +.scheduleview .show { + font-size: 1.5em; +} -#schedule hgroup>* { - margin: 0; +.scheduleview .show-description { + font-size: .8em; } -#schedule-onair, .schedule-item { - margin-bottom: 2em; - position: relative; + +.twitter-typeahead { + display: block; + width: 100%; + margin-bottom: 1em; } -#schedule h2 { - margin-top: 0; - text-align: left; + +.myradiofrmfield { + display: block; + width: 80%; + margin-bottom: 1em; } -#schedule { - text-align: center; + +.twitter-typeahead .myradiofrmfield { + margin-bottom: 0; } -.showTime { - position: absolute; - left: 2em; - font-size: larger; - font-weight: 700; + +.tt-dropdown-menu { + width: 80%; } diff --git a/src/Public/css/style.css b/src/Public/css/style.css index 2df152dee..feb5310f7 100644 --- a/src/Public/css/style.css +++ b/src/Public/css/style.css @@ -1,3 +1,8 @@ +body:not(.nonav) { + background-color: #FFFFFF; + margin-top: 50px; /* Bootstrap navbar offset */ +} + #menu #navRight { float: right; position: relative; @@ -6,6 +11,199 @@ color: #fff; } +.main-container { + margin-bottom: 60px; +} + +.footer { + width: 100%; + padding: 8px 0; + border-top: .5em solid #f5f5f5; +} + +@media (min-height: 800px) and (min-width: 992px) { + .footer { + position: fixed; + left: 0; + bottom: 0; + min-height: 40px; + z-index: 50; + background-color: #f5f5f5; + border: none; + } +} + +/* Stuff to get the nav to collapse at 991px (when the menu collapses) + * Taken from http://stackoverflow.com/a/36289507/995325 */ +@media (max-width: 991px) { + .navbar-header { + float: none; + } + .navbar-left, .navbar-right { + float: none !important; + } + .navbar-toggle { + display: block; + } + .navbar-collapse { + border-top: 1px solid transparent; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1); + } + .navbar-fixed-top { + top: 0; + border-width: 0 0 1px; + } + .navbar-collapse.collapse { + display: none !important; + } + .navbar-nav { + float: none !important; + margin-top: 7.5px; + } + .navbar-nav > li { + float: none; + } + .navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + } + .collapse.in { + display: block !important; + } +} + +/** MyRadio bootstrap navbar overides taken from http://work.smarchal.com/twbscolor/css/2D425F2D425Fffffffe1e1e11 **/ +.navbar-ury { + background-color: #2D333C; + border-color: #2D425F; + border: none; +} +.navbar-ury-pink { + background-color: #fc03df; +} +.navbar-ury .navbar-brand { + color: #ffffff; + padding: 10px 15px; /** added to center the logo **/ + line-height: 30px; /** added to center the logo **/ + +} +.navbar-ury .navbar-brand.myradio-logo { + margin-left: 0 !important; +} +.navbar-ury .navbar-brand img { + max-height: 100%; +} +.navbar-ury .navbar-brand:hover, +.navbar-ury .navbar-brand:focus { + color: #e1e1e1; +} +.navbar-ury .navbar-brand.divider { + border-right: 1px white solid; + margin: 14px 0; + height: 22px; + padding: 0; +} +.navbar-ury .navbar-text { + color: #ffffff; +} +.navbar-ury .navbar-nav > li > a { + color: #ffffff; +} +.navbar-ury .navbar-nav > li.nav-img { + height: 50px; +} +.navbar-ury .navbar-nav > li.nav-img > a { + height: 100%; + padding: 10px 10px; +} +.navbar-ury .navbar-nav > li.nav-img > a > img { + height: 100%; +} +.navbar-ury .navbar-nav .caret { + margin-left: 5px; +} +.navbar-ury .navbar-nav .glyphicon { + margin-right: 5px; +} + +.navbar-ury .navbar-nav > li > a:hover, +.navbar-ury .navbar-nav > li > a:focus { + color: #e1e1e1; + background-color: #2D425F; +} +.navbar-ury .navbar-nav > li > .dropdown-menu { + background-color: #2D425F; + border-top: none; +} +.navbar-ury .navbar-nav > li > .dropdown-menu > li > a { + color: #ffffff; +} +.navbar-ury .navbar-nav > li > .dropdown-menu > li > a:hover, +.navbar-ury .navbar-nav > li > .dropdown-menu > li > a:focus { + color: #e1e1e1; + background-color: #2D425F; +} +.navbar-ury .navbar-nav > li > .dropdown-menu > li > .divider { + background-color: #2D425F; +} +.navbar-ury .navbar-nav .open .dropdown-menu > .active > a, +.navbar-ury .navbar-nav .open .dropdown-menu > .active > a:hover, +.navbar-ury .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #e1e1e1; + background-color: #2D425F; +} +.navbar-ury .navbar-nav > .active > a, +.navbar-ury .navbar-nav > .active > a:hover, +.navbar-ury .navbar-nav > .active > a:focus { + color: #e1e1e1; + background-color: #2D425F; +} +.navbar-ury .navbar-nav > .open > a, +.navbar-ury .navbar-nav > .open > a:hover, +.navbar-ury .navbar-nav > .open > a:focus { + color: #e1e1e1; + background-color: #2D425F !important; +} +.navbar-ury .navbar-toggle { + border-color: #2D425F; +} +.navbar-ury .navbar-toggle:hover, +.navbar-ury .navbar-toggle:focus { + background-color: #2D425F; +} +.navbar-ury .navbar-toggle .icon-bar { + background-color: #ffffff; +} +.navbar-ury .navbar-collapse, +.navbar-ury .navbar-form { + border-color: #ffffff; +} +.navbar-ury .navbar-link { + color: #ffffff; +} +.navbar-ury .navbar-link:hover { + color: #e1e1e1; +} +.navbar-ury .navbar-brand:hover { + color: #e1e1e1; + background-color: #2D425F !important; +} + +@media (max-width: 767px) { + .navbar-ury .navbar-nav .open .dropdown-menu > li > a { + color: #ffffff; + } + .navbar-ury .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-ury .navbar-nav .open .dropdown-menu > li > a:focus { + color: #e1e1e1; + } + .navbar-ury .navbar-nav .open .dropdown-menu > .active > a, + .navbar-ury .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-ury .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #e1e1e1; + background-color: #2D425F; + } +} /** MyRadio Menu Styles **/ .menu-column { @@ -26,25 +224,14 @@ } .menu-checklist-disabled a { color: #777; + text-decoration: line-through } #signin { display: inline-block; } -/** MyRadio News Alert Styles **/ -.myury-news-alert { - text-align: center; -} -.myury-news-alert ul { - text-align: left; -} -.myury-news-alert footer { - text-align: right; - padding-right: 10px; -} - -fieldset.myuryfrm label { +fieldset.myradioform label { width: 12em; } @@ -63,194 +250,186 @@ fieldset.myuryfrm label { display: block; } -/* css for timepicker */ -.ui-timepicker-div .ui-widget-header { margin-bottom: 8px; } -.ui-timepicker-div dl { text-align: left; } -.ui-timepicker-div dl dt { height: 25px; margin-bottom: -25px; } -.ui-timepicker-div dl dd { margin: 0 10px 10px 65px; } -.ui-timepicker-div td { font-size: 90%; } -.ui-tpicker-grid-label { background: none; border: none; margin: 0; padding: 0; } - /** MyRadio Timeline Styles **/ div#timelineContainer { - border-left:2px solid #333; - margin:20px auto; - width:900px; + border-left:2px solid #333; + margin:20px auto; + width:900px; text-align: left; } div.timelineToggle { - float:right; - margin-right:0; - white-space:nowrap; + float:right; + margin-right:0; + white-space:nowrap; position: relative; top: -50px; } a.expandAll { - color:#ccc!important; - cursor:pointer; - background:#000; - border:none; - -webkit-border-radius:4px; - -moz-border-radius:4px; - border-radius:4px; - font-size:12px; - padding:3px 5px; + color:#ccc!important; + cursor:pointer; + background:#000; + border:none; + -webkit-border-radius:4px; + -moz-border-radius:4px; + border-radius:4px; + font-size:12px; + padding:3px 5px; } a.expandAll:hover { - border:none!important; - color:#7DBADF!important; - cursor:pointer; + border:none!important; + color:#7DBADF!important; + cursor:pointer; } div.timelineMajor { - clear:left; - float:left; - margin:0 0 12px; - width:900px; -} - .timelineMajor h2 { - border-left: double #555; - color:#7097AF!important; - cursor: pointer; - font-family:Palatino,"Times New Roman", Times, serif; - font-size:3em!important; - font-weight:400!important; - margin:0 0 10px!important; - padding:4px 4px 4px 20px!important; - } - .timelineMajor h2 span { - background:#ccc; - border:none; - -webkit-border-radius:4px; - -moz-border-radius:4px; - border-radius:4px; - color:#131313; - letter-spacing:.1em; - line-height:1.7em; - padding:3px 5px 1px; - } - .timelineMajor h2 a:hover { - border-bottom:none; - color:#00baff!important; - } + clear:left; + float:left; + margin:0 0 12px; + width:900px; +} + .timelineMajor h2 { + border-left: double #555; + color:#7097AF!important; + cursor: pointer; + font-family:Palatino,"Times New Roman", Times, serif; + font-size:3em!important; + font-weight:400!important; + margin:0 0 10px!important; + padding:4px 4px 4px 20px!important; + } + .timelineMajor h2 span { + background:#ccc; + border:none; + -webkit-border-radius:4px; + -moz-border-radius:4px; + border-radius:4px; + color:#131313; + letter-spacing:.1em; + line-height:1.7em; + padding:3px 5px 1px; + } + .timelineMajor h2 a:hover { + border-bottom:none; + color:#00baff!important; + } dl.timelineMinor { - clear:left; - float:left!important; - margin:0 12px 0 1px!important; - padding:4px 4px 4px 0!important; - position:relative; - width:880px; + clear:left; + float:left!important; + margin:0 12px 0 1px!important; + padding:4px 4px 4px 0!important; + position:relative; + width:880px; top: -15px; } - .timelineMinor dt { - border-left: double #666; - clear:left; - font-size:1.6em!important; - list-style-type:none!important; - line-height:1.2em; - margin:0 0 12px!important; - padding:0 0 0 24px!important; - white-space:nowrap; - } - .timelineMinor dt a { - color:#999; - cursor:pointer; - } - .timelineMinor dt a.closed { - color:#999; - font-size:1em; - margin-left:0; - } - .timelineMinor dt a.open { - color:#7DBADF; - } - .timelineMinor dt a:hover { - color:#7DBADF; - } - .timelineMinor dd { - padding-left:24px; - width:100%; - } - .timelineMinor dd h3 { - color:#FFF; - clear:both; - float:left; - font-size:1.5em!important; - margin:0; - white-space:nowrap; - } + .timelineMinor dt { + border-left: double #666; + clear:left; + font-size:1.6em!important; + list-style-type:none!important; + line-height:1.2em; + margin:0 0 12px!important; + padding:0 0 0 24px!important; + white-space:nowrap; + } + .timelineMinor dt a { + color:#999; + cursor:pointer; + } + .timelineMinor dt a.closed { + color:#999; + font-size:1em; + margin-left:0; + } + .timelineMinor dt a.open { + color:#7DBADF; + } + .timelineMinor dt a:hover { + color:#7DBADF; + } + .timelineMinor dd { + padding-left:24px; + width:100%; + } + .timelineMinor dd h3 { + color:#FFF; + clear:both; + float:left; + font-size:1.5em!important; + margin:0; + white-space:nowrap; + } .timelineEvent p { - clear:left; - float:left; - line-height:1.5em!important; - margin:6px 0 10px; - width:500px; + clear:left; + float:left; + line-height:1.5em!important; + margin:6px 0 10px; + width:500px; } .timelineEvent h4 { - clear:left; - float:left; - font-size:1.4em!important; - font-weight:400; - margin:10px 0 0; - padding:0 0 0 20px!important; + clear:left; + float:left; + font-size:1.4em!important; + font-weight:400; + margin:10px 0 0; + padding:0 0 0 20px!important; } .timelineEvent blockquote { - clear:left; - float:left; - padding:0 30px; - width:400px; + clear:left; + float:left; + padding:0 30px; + width:400px; } .timelineEvent blockquote * { - float:none; - width:400px; + float:none; + width:400px; } .timelineEvent ul.moreInfo { - clear:left; - float:left; - line-height:1.2em; - list-style-type:none; - margin:0!important; - padding:0!important; - width:100%; + clear:left; + float:left; + line-height:1.2em; + list-style-type:none; + margin:0!important; + padding:0!important; + width:100%; } .timelineEvent ul.moreInfo li { - clear:left; - background:none!important; - font-size:1em!important; - line-height:1.5em; - margin:8px 0!important; - padding:0 0 0 20px!important; -} - .timelineEvent ul.moreInfo li a:link,.timelineEvent ul.moreInfo li a:visited { - color:#7097af!important; - } - .timelineEvent ul.moreInfo li a:hover { - color:#7DBADF!important; - } + clear:left; + background:none!important; + font-size:1em!important; + line-height:1.5em; + margin:8px 0!important; + padding:0 0 0 20px!important; +} + .timelineEvent ul.moreInfo li a:link,.timelineEvent ul.moreInfo li a:visited { + color:#7097af!important; + } + .timelineEvent ul.moreInfo li a:hover { + color:#7DBADF!important; + } .timelineEvent div.media { - float:right; - padding:0 0 12px; - width:300px; -} - .timelineEvent .media img { - border:2px solid #000; - margin:0; - } - .timelineEvent .media p { - font-size:1.2em; - margin:0!important; - padding:0!important; - } - .timelineEvent .media a:link,.timelineEvent .media a:visited { - border:none!important; - color:#ab221b!important; - } - .timelineEvent .media a:hover { - color:#7DBADF!important; - } - .timelineEvent .media p span.source { - font-style:italic; - } + float:right; + padding:0 0 12px; + width:300px; +} + .timelineEvent .media img { + border:2px solid #000; + margin:0; + } + .timelineEvent .media p { + font-size:1.2em; + margin:0!important; + padding:0!important; + } + .timelineEvent .media a:link,.timelineEvent .media a:visited { + border:none!important; + color:#ab221b!important; + } + .timelineEvent .media a:hover { + color:#7DBADF!important; + } + .timelineEvent .media p span.source { + font-style:italic; + } /** MyRadio Webcam Styles **/ .webcam-grid-container .webcam-stream { @@ -277,6 +456,10 @@ label.label-nofloat { } /** MyRadio Form Styles **/ +select { + color: #222!important; +} + .myradiofrm { padding: 0 10px 10px; border: dashed 1px; @@ -289,7 +472,7 @@ label.label-nofloat { clear: both; } .myradiofrmfield-container input, .myradiofrmfield-container textarea, .myradiofrmfield-container select { - width: 300px; + max-width: 90%; vertical-align: top; } .myradiofrmfield-container input[type="checkbox"] { @@ -321,7 +504,7 @@ label.label-nofloat { } .myradiofrmfield { - margin-left: 1.25em; + margin-left: 1.25em; } input.myradiofrmfield-weeklycheck { @@ -348,17 +531,58 @@ table.myradiofrmfield-weeklycheck td { padding: 0; } +.myradiofrm-file-upload-progress { + display: none; +} + +.tt-hint { + width: 300px; + height: 30px; + margin-top: -2px; + margin-left: 19px; + font-size: 14px; + color: #999 +} + +.tt-dropdown-menu { + width: 300px; + margin-top: 0px; + margin-left: 19px; + padding: 0 0 8px 0; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + border-radius: 0 0 8px 8px; + box-shadow: 0 5px 10px rgba(0,0,0,.2); +} + +.tt-suggestion { + padding: 3px 20px; + font-size: 14px; + line-height: 20px; + cursor: pointer; +} + +.tt-suggestion.tt-cursor { + color: #fff; + background-color: #0097cf; +} + +.tt-suggestion p { + margin: 0; +} + /* MyRadio Login Styles */ .chooseAuth label { - padding: 1em; + padding: 1em; } .chooseAuth input { - width: auto; - vertical-align: baseline; + width: auto; + vertical-align: baseline; } .chooseAuth h3 { - display: inline-block; - margin: 0 0 0 1.25em; + display: inline-block; + margin: 0 0 0 1.25em; } .ui-progressbar { @@ -401,10 +625,6 @@ table.myradiofrmfield-weeklycheck td { .clearfix { *zoom: 1; } -.ui-autocomplete { - max-height: 350px; - overflow-y: scroll; -} /** MyRadio Scheduler Styles **/ @@ -414,7 +634,7 @@ table.myradiofrmfield-weeklycheck td { margin-left: auto; } -#sched_show-grp-credits-container input, +#sched_show-grp-credits-container input, #sched_show-grp-credits-container select, #sched_season select, #sched_season .nobr input, @@ -453,3 +673,98 @@ div.assistant-officer img { div.officer .officer-title { height: 45px; } + +@media (min-width: 992px) { + div.scheduleview.view-day, div.scheduleview.hours-list { + display: table-cell; + width: calc(100% / 8); + } + div.scheduleview.view-day div.show time { + display: none; + } + div.scheduleview.hours-list div.hour { + height: 50px; + } +} + +div.scheduleview-view-week div.show-description, +div.scheduleview-view-week span.show-credits { + display: none; +} + +div.scheduleview div.show-description { + font-style: italic; + text-align: center; +} + +div.scheduleview-view-day div.show-title { + padding-left: 10%; +} + +div.scheduleview div.show-title { + display: inline-block; +} + +#schedule-preview { + min-height: 300px; +} + +div.scheduleview.header { + width: 100%; + text-align: center; +} + +@media (max-width: 999px) { + div.scheduleview.view-day { + display: block; + width: 100%; + } + div.scheduleview.view-day div.show time { + margin-right: 10px; + } + div.scheduleview.hours-list, div.scheduleview.view-day div.filler { + display: none; + } +} + +div.scheduleview .day-header { + text-align: center; + height: 50px; +} + +div.scheduleview.view-day div, div.scheduleview.hours-list div { + overflow: hidden; + border: 1px solid #000; + border-collapse: collapse; + padding: 5px; +} + +div.scheduleview-view-day div.nownext { + padding: 5px; +} + +div.scheduleview div.now-marker { + text-align: center; + padding: 1px; +} + +header.page-header { + margin: 10px 0 20px +} + +.events { + padding: 9px 19px; +} + +.events > h2 { + margin-top: 0.4em; +} + +.events .event h3 { + margin-top: 0.3em; + margin-bottom: 0.3em; +} + +.events .event { + padding: 5px 15px; +} diff --git a/src/Public/css/timelord.css b/src/Public/css/timelord.css deleted file mode 100644 index 1a8303ac6..000000000 --- a/src/Public/css/timelord.css +++ /dev/null @@ -1,137 +0,0 @@ -html { - padding: 0; - margin: 0; -} -body { - width: 1024px; - height: 768px; - background: #000; - color: #FFF; - font-family: "Source Sans Pro", 'sans-serif'; - overflow: hidden; - padding: 0; - position: absolute; - top: -22px; - left: calc(50% - 512px); - box-shadow: none; - text-align: center; -} - -#studio { - margin-top: .2em; - font-size: 4.5em; - font-weight: bold; - margin-bottom: -1em; -} -.black { - color: black; -} -.orange { - color: orange; -} -.studio1 { - color: red; -} -.studio2 { - color: #0044BA; -} -.studio3 { - color: #0F0; -} -.studio4 { - color: #bb00dc; -} -#time { - font-size: 13em; - margin-bottom: -.4em; -} -#date { - font-size: 5em; - margin:0; - padding:0; - margin-bottom:-.5em; - position: absolute; - top: 0; - right: 5px; - font-size: 2em; - font-weight: bold; -} - -#current-show, #next-show { - font-size: 3.8em; - margin-top: .3em; - font-variant: small-caps; - white-space: nowrap; - overflow: hidden; -} -#next-show { - font-size: 2.8em; -} -.bracket1 { - font-family: "Times New Roman", serif; - font-size: 140%; - letter-spacing: 2px; - text-shadow: 0 0 30px #17A; -} -.bracket2 { - font-family: "Times New Roman", serif; - font-size: 120%; - text-shadow: 0 0 30px #17A; -} - -#breaking-container { - display: none; - font-size: 5em; - color: #F00; - width: 100%; - height: 100%; - z-index: 9999; - background: none; -} - -#alert-status { - position: absolute; - bottom: 0; - width: 100%; -} - -#alert-status div { - width: 204.8px; - height: 70px; - border: 2px solid #CCC; - color: #CCC; - display: table-cell; - border-collapse: separate; - border-spacing: 10px; - margin: 15px; - text-align: center; - vertical-align: middle; - font-size: 2.5em; -} - -#alert-status div.worse { - border: 3px solid #F00; - color: #F00; - width: 60%; - font-weight: bold; - font-size: 6em; -} - -#alert-status div.bad { - border: 3px solid #F00; - color: #F00; -} - -#alert-status div.standby { - border: 3px solid orange; - color: orange; -} - -#alert-status div.good { - border: 3px solid #0F0; - color: #0F0; -} - -.news { - color: #2CDFFF; -} \ No newline at end of file diff --git a/src/Public/css/vendor/bootstrap-datetimepicker.min.css b/src/Public/css/vendor/bootstrap-datetimepicker.min.css new file mode 100644 index 000000000..cf838a3c2 --- /dev/null +++ b/src/Public/css/vendor/bootstrap-datetimepicker.min.css @@ -0,0 +1,5 @@ +/*! + * Datetimepicker for Bootstrap v3 +//! version : 3.1.3 + * https://github.com/Eonasdan/bootstrap-datetimepicker/ + */.bootstrap-datetimepicker-widget{top:0;left:0;width:250px;padding:4px;margin-top:1px;z-index:99999!important;border-radius:4px}.bootstrap-datetimepicker-widget.timepicker-sbs{width:600px}.bootstrap-datetimepicker-widget.bottom:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0,0,0,.2);position:absolute;top:-7px;left:7px}.bootstrap-datetimepicker-widget.bottom:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;top:-6px;left:8px}.bootstrap-datetimepicker-widget.top:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-top:7px solid #ccc;border-top-color:rgba(0,0,0,.2);position:absolute;bottom:-7px;left:6px}.bootstrap-datetimepicker-widget.top:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-top:6px solid #fff;position:absolute;bottom:-6px;left:7px}.bootstrap-datetimepicker-widget .dow{width:14.2857%}.bootstrap-datetimepicker-widget.pull-right:before{left:auto;right:6px}.bootstrap-datetimepicker-widget.pull-right:after{left:auto;right:7px}.bootstrap-datetimepicker-widget>ul{list-style-type:none;margin:0}.bootstrap-datetimepicker-widget a[data-action]{padding:6px 0}.bootstrap-datetimepicker-widget a[data-action]:active{box-shadow:none}.bootstrap-datetimepicker-widget .timepicker-hour,.bootstrap-datetimepicker-widget .timepicker-minute,.bootstrap-datetimepicker-widget .timepicker-second{width:54px;font-weight:700;font-size:1.2em;margin:0}.bootstrap-datetimepicker-widget button[data-action]{padding:6px}.bootstrap-datetimepicker-widget table[data-hour-format="12"] .separator{width:4px;padding:0;margin:0}.bootstrap-datetimepicker-widget .datepicker>div{display:none}.bootstrap-datetimepicker-widget .picker-switch{text-align:center}.bootstrap-datetimepicker-widget table{width:100%;margin:0}.bootstrap-datetimepicker-widget td,.bootstrap-datetimepicker-widget th{text-align:center;border-radius:4px}.bootstrap-datetimepicker-widget td{height:54px;line-height:54px;width:54px}.bootstrap-datetimepicker-widget td.cw{font-size:10px;height:20px;line-height:20px;color:#777}.bootstrap-datetimepicker-widget td.day{height:20px;line-height:20px;width:20px}.bootstrap-datetimepicker-widget td.day:hover,.bootstrap-datetimepicker-widget td.hour:hover,.bootstrap-datetimepicker-widget td.minute:hover,.bootstrap-datetimepicker-widget td.second:hover{background:#eee;cursor:pointer}.bootstrap-datetimepicker-widget td.old,.bootstrap-datetimepicker-widget td.new{color:#777}.bootstrap-datetimepicker-widget td.today{position:relative}.bootstrap-datetimepicker-widget td.today:before{content:'';display:inline-block;border-left:7px solid transparent;border-bottom:7px solid #428bca;border-top-color:rgba(0,0,0,.2);position:absolute;bottom:4px;right:4px}.bootstrap-datetimepicker-widget td.active,.bootstrap-datetimepicker-widget td.active:hover{background-color:#428bca;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.25)}.bootstrap-datetimepicker-widget td.active.today:before{border-bottom-color:#fff}.bootstrap-datetimepicker-widget td.disabled,.bootstrap-datetimepicker-widget td.disabled:hover{background:0 0;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget td span{display:inline-block;width:54px;height:54px;line-height:54px;margin:2px 1.5px;cursor:pointer;border-radius:4px}.bootstrap-datetimepicker-widget td span:hover{background:#eee}.bootstrap-datetimepicker-widget td span.active{background-color:#428bca;color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,.25)}.bootstrap-datetimepicker-widget td span.old{color:#777}.bootstrap-datetimepicker-widget td span.disabled,.bootstrap-datetimepicker-widget td span.disabled:hover{background:0 0;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget th{height:20px;line-height:20px;width:20px}.bootstrap-datetimepicker-widget th.picker-switch{width:145px}.bootstrap-datetimepicker-widget th.next,.bootstrap-datetimepicker-widget th.prev{font-size:21px}.bootstrap-datetimepicker-widget th.disabled,.bootstrap-datetimepicker-widget th.disabled:hover{background:0 0;color:#777;cursor:not-allowed}.bootstrap-datetimepicker-widget thead tr:first-child th{cursor:pointer}.bootstrap-datetimepicker-widget thead tr:first-child th:hover{background:#eee}.input-group.date .input-group-addon span{display:block;cursor:pointer;width:16px;height:16px}.bootstrap-datetimepicker-widget.left-oriented:before{left:auto;right:6px}.bootstrap-datetimepicker-widget.left-oriented:after{left:auto;right:7px}.bootstrap-datetimepicker-widget ul.list-unstyled li div.timepicker div.timepicker-picker table.table-condensed tbody>tr>td{padding:0!important}@media screen and (max-width:767px){.bootstrap-datetimepicker-widget.timepicker-sbs{width:283px}} \ No newline at end of file diff --git a/src/Public/css/vendor/bootstrap.css b/src/Public/css/vendor/bootstrap.css new file mode 100644 index 000000000..c6f3d2106 --- /dev/null +++ b/src/Public/css/vendor/bootstrap.css @@ -0,0 +1,6332 @@ +/*! + * Bootstrap v3.3.1 (http://getbootstrap.com) + * Copyright 2011-2014 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + */ + +/*! normalize.css v3.0.2 | MIT License | git.io/normalize */ +html { + font-family: sans-serif; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} +body { + margin: 0; +} +article, +aside, +details, +figcaption, +figure, +footer, +header, +hgroup, +main, +menu, +nav, +section, +summary { + display: block; +} +audio, +canvas, +progress, +video { + display: inline-block; + vertical-align: baseline; +} +audio:not([controls]) { + display: none; + height: 0; +} +[hidden], +template { + display: none; +} +a { + background-color: transparent; +} +a:active, +a:hover { + outline: 0; +} +abbr[title] { + border-bottom: 1px dotted; +} +b, +strong { + font-weight: bold; +} +dfn { + font-style: italic; +} +h1 { + margin: .67em 0; + font-size: 2em; +} +mark { + color: #000; + background: #ff0; +} +small { + font-size: 80%; +} +sub, +sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; +} +sup { + top: -.5em; +} +sub { + bottom: -.25em; +} +img { + border: 0; +} +svg:not(:root) { + overflow: hidden; +} +figure { + margin: 1em 40px; +} +hr { + height: 0; + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; +} +pre { + overflow: auto; +} +code, +kbd, +pre, +samp { + font-family: monospace, monospace; + font-size: 1em; +} +button, +input, +optgroup, +select, +textarea { + margin: 0; + font: inherit; + color: inherit; +} +button { + overflow: visible; +} +button, +select { + text-transform: none; +} +button, +html input[type="button"], +input[type="reset"], +input[type="submit"] { + -webkit-appearance: button; + cursor: pointer; +} +button[disabled], +html input[disabled] { + cursor: default; +} +button::-moz-focus-inner, +input::-moz-focus-inner { + padding: 0; + border: 0; +} +input { + line-height: normal; +} +input[type="checkbox"], +input[type="radio"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; + padding: 0; +} +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + height: auto; +} +input[type="search"] { + -webkit-box-sizing: content-box; + -moz-box-sizing: content-box; + box-sizing: content-box; + -webkit-appearance: textfield; +} +input[type="search"]::-webkit-search-cancel-button, +input[type="search"]::-webkit-search-decoration { + -webkit-appearance: none; +} +fieldset { + padding: .35em .625em .75em; + margin: 0 2px; + border: 1px solid #c0c0c0; +} +legend { + padding: 0; + border: 0; +} +textarea { + overflow: auto; +} +optgroup { + font-weight: bold; +} +table { + border-spacing: 0; + border-collapse: collapse; +} +td, +th { + padding: 0; +} +/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */ +@media print { + *, + *:before, + *:after { + color: #000 !important; + text-shadow: none !important; + background: transparent !important; + -webkit-box-shadow: none !important; + box-shadow: none !important; + } + a, + a:visited { + text-decoration: underline; + } + a[href]:after { + content: " (" attr(href) ")"; + } + abbr[title]:after { + content: " (" attr(title) ")"; + } + a[href^="#"]:after, + a[href^="javascript:"]:after { + content: ""; + } + pre, + blockquote { + border: 1px solid #999; + + page-break-inside: avoid; + } + thead { + display: table-header-group; + } + tr, + img { + page-break-inside: avoid; + } + img { + max-width: 100% !important; + } + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + h2, + h3 { + page-break-after: avoid; + } + select { + background: #fff !important; + } + .navbar { + display: none; + } + .btn > .caret, + .dropup > .btn > .caret { + border-top-color: #000 !important; + } + .label { + border: 1px solid #000; + } + .table { + border-collapse: collapse !important; + } + .table td, + .table th { + background-color: #fff !important; + } + .table-bordered th, + .table-bordered td { + border: 1px solid #ddd !important; + } +} +@font-face { + font-family: 'Glyphicons Halflings'; + + src: url('../fonts/glyphicons-halflings-regular.eot'); + src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); +} +.glyphicon { + position: relative; + top: 1px; + display: inline-block; + font-family: 'Glyphicons Halflings'; + font-style: normal; + font-weight: normal; + line-height: 1; + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +.glyphicon-asterisk:before { + content: "\2a"; +} +.glyphicon-plus:before { + content: "\2b"; +} +.glyphicon-euro:before, +.glyphicon-eur:before { + content: "\20ac"; +} +.glyphicon-minus:before { + content: "\2212"; +} +.glyphicon-cloud:before { + content: "\2601"; +} +.glyphicon-envelope:before { + content: "\2709"; +} +.glyphicon-pencil:before { + content: "\270f"; +} +.glyphicon-glass:before { + content: "\e001"; +} +.glyphicon-music:before { + content: "\e002"; +} +.glyphicon-search:before { + content: "\e003"; +} +.glyphicon-heart:before { + content: "\e005"; +} +.glyphicon-star:before { + content: "\e006"; +} +.glyphicon-star-empty:before { + content: "\e007"; +} +.glyphicon-user:before { + content: "\e008"; +} +.glyphicon-film:before { + content: "\e009"; +} +.glyphicon-th-large:before { + content: "\e010"; +} +.glyphicon-th:before { + content: "\e011"; +} +.glyphicon-th-list:before { + content: "\e012"; +} +.glyphicon-ok:before { + content: "\e013"; +} +.glyphicon-remove:before { + content: "\e014"; +} +.glyphicon-zoom-in:before { + content: "\e015"; +} +.glyphicon-zoom-out:before { + content: "\e016"; +} +.glyphicon-off:before { + content: "\e017"; +} +.glyphicon-signal:before { + content: "\e018"; +} +.glyphicon-cog:before { + content: "\e019"; +} +.glyphicon-trash:before { + content: "\e020"; +} +.glyphicon-home:before { + content: "\e021"; +} +.glyphicon-file:before { + content: "\e022"; +} +.glyphicon-time:before { + content: "\e023"; +} +.glyphicon-road:before { + content: "\e024"; +} +.glyphicon-download-alt:before { + content: "\e025"; +} +.glyphicon-download:before { + content: "\e026"; +} +.glyphicon-upload:before { + content: "\e027"; +} +.glyphicon-inbox:before { + content: "\e028"; +} +.glyphicon-play-circle:before { + content: "\e029"; +} +.glyphicon-repeat:before { + content: "\e030"; +} +.glyphicon-refresh:before { + content: "\e031"; +} +.glyphicon-list-alt:before { + content: "\e032"; +} +.glyphicon-lock:before { + content: "\e033"; +} +.glyphicon-flag:before { + content: "\e034"; +} +.glyphicon-headphones:before { + content: "\e035"; +} +.glyphicon-volume-off:before { + content: "\e036"; +} +.glyphicon-volume-down:before { + content: "\e037"; +} +.glyphicon-volume-up:before { + content: "\e038"; +} +.glyphicon-qrcode:before { + content: "\e039"; +} +.glyphicon-barcode:before { + content: "\e040"; +} +.glyphicon-tag:before { + content: "\e041"; +} +.glyphicon-tags:before { + content: "\e042"; +} +.glyphicon-book:before { + content: "\e043"; +} +.glyphicon-bookmark:before { + content: "\e044"; +} +.glyphicon-print:before { + content: "\e045"; +} +.glyphicon-camera:before { + content: "\e046"; +} +.glyphicon-font:before { + content: "\e047"; +} +.glyphicon-bold:before { + content: "\e048"; +} +.glyphicon-italic:before { + content: "\e049"; +} +.glyphicon-text-height:before { + content: "\e050"; +} +.glyphicon-text-width:before { + content: "\e051"; +} +.glyphicon-align-left:before { + content: "\e052"; +} +.glyphicon-align-center:before { + content: "\e053"; +} +.glyphicon-align-right:before { + content: "\e054"; +} +.glyphicon-align-justify:before { + content: "\e055"; +} +.glyphicon-list:before { + content: "\e056"; +} +.glyphicon-indent-left:before { + content: "\e057"; +} +.glyphicon-indent-right:before { + content: "\e058"; +} +.glyphicon-facetime-video:before { + content: "\e059"; +} +.glyphicon-picture:before { + content: "\e060"; +} +.glyphicon-map-marker:before { + content: "\e062"; +} +.glyphicon-adjust:before { + content: "\e063"; +} +.glyphicon-tint:before { + content: "\e064"; +} +.glyphicon-edit:before { + content: "\e065"; +} +.glyphicon-share:before { + content: "\e066"; +} +.glyphicon-check:before { + content: "\e067"; +} +.glyphicon-move:before { + content: "\e068"; +} +.glyphicon-step-backward:before { + content: "\e069"; +} +.glyphicon-fast-backward:before { + content: "\e070"; +} +.glyphicon-backward:before { + content: "\e071"; +} +.glyphicon-play:before { + content: "\e072"; +} +.glyphicon-pause:before { + content: "\e073"; +} +.glyphicon-stop:before { + content: "\e074"; +} +.glyphicon-forward:before { + content: "\e075"; +} +.glyphicon-fast-forward:before { + content: "\e076"; +} +.glyphicon-step-forward:before { + content: "\e077"; +} +.glyphicon-eject:before { + content: "\e078"; +} +.glyphicon-chevron-left:before { + content: "\e079"; +} +.glyphicon-chevron-right:before { + content: "\e080"; +} +.glyphicon-plus-sign:before { + content: "\e081"; +} +.glyphicon-minus-sign:before { + content: "\e082"; +} +.glyphicon-remove-sign:before { + content: "\e083"; +} +.glyphicon-ok-sign:before { + content: "\e084"; +} +.glyphicon-question-sign:before { + content: "\e085"; +} +.glyphicon-info-sign:before { + content: "\e086"; +} +.glyphicon-screenshot:before { + content: "\e087"; +} +.glyphicon-remove-circle:before { + content: "\e088"; +} +.glyphicon-ok-circle:before { + content: "\e089"; +} +.glyphicon-ban-circle:before { + content: "\e090"; +} +.glyphicon-arrow-left:before { + content: "\e091"; +} +.glyphicon-arrow-right:before { + content: "\e092"; +} +.glyphicon-arrow-up:before { + content: "\e093"; +} +.glyphicon-arrow-down:before { + content: "\e094"; +} +.glyphicon-share-alt:before { + content: "\e095"; +} +.glyphicon-resize-full:before { + content: "\e096"; +} +.glyphicon-resize-small:before { + content: "\e097"; +} +.glyphicon-exclamation-sign:before { + content: "\e101"; +} +.glyphicon-gift:before { + content: "\e102"; +} +.glyphicon-leaf:before { + content: "\e103"; +} +.glyphicon-fire:before { + content: "\e104"; +} +.glyphicon-eye-open:before { + content: "\e105"; +} +.glyphicon-eye-close:before { + content: "\e106"; +} +.glyphicon-warning-sign:before { + content: "\e107"; +} +.glyphicon-plane:before { + content: "\e108"; +} +.glyphicon-calendar:before { + content: "\e109"; +} +.glyphicon-random:before { + content: "\e110"; +} +.glyphicon-comment:before { + content: "\e111"; +} +.glyphicon-magnet:before { + content: "\e112"; +} +.glyphicon-chevron-up:before { + content: "\e113"; +} +.glyphicon-chevron-down:before { + content: "\e114"; +} +.glyphicon-retweet:before { + content: "\e115"; +} +.glyphicon-shopping-cart:before { + content: "\e116"; +} +.glyphicon-folder-close:before { + content: "\e117"; +} +.glyphicon-folder-open:before { + content: "\e118"; +} +.glyphicon-resize-vertical:before { + content: "\e119"; +} +.glyphicon-resize-horizontal:before { + content: "\e120"; +} +.glyphicon-hdd:before { + content: "\e121"; +} +.glyphicon-bullhorn:before { + content: "\e122"; +} +.glyphicon-bell:before { + content: "\e123"; +} +.glyphicon-certificate:before { + content: "\e124"; +} +.glyphicon-thumbs-up:before { + content: "\e125"; +} +.glyphicon-thumbs-down:before { + content: "\e126"; +} +.glyphicon-hand-right:before { + content: "\e127"; +} +.glyphicon-hand-left:before { + content: "\e128"; +} +.glyphicon-hand-up:before { + content: "\e129"; +} +.glyphicon-hand-down:before { + content: "\e130"; +} +.glyphicon-circle-arrow-right:before { + content: "\e131"; +} +.glyphicon-circle-arrow-left:before { + content: "\e132"; +} +.glyphicon-circle-arrow-up:before { + content: "\e133"; +} +.glyphicon-circle-arrow-down:before { + content: "\e134"; +} +.glyphicon-globe:before { + content: "\e135"; +} +.glyphicon-wrench:before { + content: "\e136"; +} +.glyphicon-tasks:before { + content: "\e137"; +} +.glyphicon-filter:before { + content: "\e138"; +} +.glyphicon-briefcase:before { + content: "\e139"; +} +.glyphicon-fullscreen:before { + content: "\e140"; +} +.glyphicon-dashboard:before { + content: "\e141"; +} +.glyphicon-paperclip:before { + content: "\e142"; +} +.glyphicon-heart-empty:before { + content: "\e143"; +} +.glyphicon-link:before { + content: "\e144"; +} +.glyphicon-phone:before { + content: "\e145"; +} +.glyphicon-pushpin:before { + content: "\e146"; +} +.glyphicon-usd:before { + content: "\e148"; +} +.glyphicon-gbp:before { + content: "\e149"; +} +.glyphicon-sort:before { + content: "\e150"; +} +.glyphicon-sort-by-alphabet:before { + content: "\e151"; +} +.glyphicon-sort-by-alphabet-alt:before { + content: "\e152"; +} +.glyphicon-sort-by-order:before { + content: "\e153"; +} +.glyphicon-sort-by-order-alt:before { + content: "\e154"; +} +.glyphicon-sort-by-attributes:before { + content: "\e155"; +} +.glyphicon-sort-by-attributes-alt:before { + content: "\e156"; +} +.glyphicon-unchecked:before { + content: "\e157"; +} +.glyphicon-expand:before { + content: "\e158"; +} +.glyphicon-collapse-down:before { + content: "\e159"; +} +.glyphicon-collapse-up:before { + content: "\e160"; +} +.glyphicon-log-in:before { + content: "\e161"; +} +.glyphicon-flash:before { + content: "\e162"; +} +.glyphicon-log-out:before { + content: "\e163"; +} +.glyphicon-new-window:before { + content: "\e164"; +} +.glyphicon-record:before { + content: "\e165"; +} +.glyphicon-save:before { + content: "\e166"; +} +.glyphicon-open:before { + content: "\e167"; +} +.glyphicon-saved:before { + content: "\e168"; +} +.glyphicon-import:before { + content: "\e169"; +} +.glyphicon-export:before { + content: "\e170"; +} +.glyphicon-send:before { + content: "\e171"; +} +.glyphicon-floppy-disk:before { + content: "\e172"; +} +.glyphicon-floppy-saved:before { + content: "\e173"; +} +.glyphicon-floppy-remove:before { + content: "\e174"; +} +.glyphicon-floppy-save:before { + content: "\e175"; +} +.glyphicon-floppy-open:before { + content: "\e176"; +} +.glyphicon-credit-card:before { + content: "\e177"; +} +.glyphicon-transfer:before { + content: "\e178"; +} +.glyphicon-cutlery:before { + content: "\e179"; +} +.glyphicon-header:before { + content: "\e180"; +} +.glyphicon-compressed:before { + content: "\e181"; +} +.glyphicon-earphone:before { + content: "\e182"; +} +.glyphicon-phone-alt:before { + content: "\e183"; +} +.glyphicon-tower:before { + content: "\e184"; +} +.glyphicon-stats:before { + content: "\e185"; +} +.glyphicon-sd-video:before { + content: "\e186"; +} +.glyphicon-hd-video:before { + content: "\e187"; +} +.glyphicon-subtitles:before { + content: "\e188"; +} +.glyphicon-sound-stereo:before { + content: "\e189"; +} +.glyphicon-sound-dolby:before { + content: "\e190"; +} +.glyphicon-sound-5-1:before { + content: "\e191"; +} +.glyphicon-sound-6-1:before { + content: "\e192"; +} +.glyphicon-sound-7-1:before { + content: "\e193"; +} +.glyphicon-copyright-mark:before { + content: "\e194"; +} +.glyphicon-registration-mark:before { + content: "\e195"; +} +.glyphicon-cloud-download:before { + content: "\e197"; +} +.glyphicon-cloud-upload:before { + content: "\e198"; +} +.glyphicon-tree-conifer:before { + content: "\e199"; +} +.glyphicon-tree-deciduous:before { + content: "\e200"; +} +* { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +*:before, +*:after { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +html { + font-size: 10px; + + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); +} +body { + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + line-height: 1.42857143; + color: #333; + background-color: #fff; +} +input, +button, +select, +textarea { + font-family: inherit; + font-size: inherit; + line-height: inherit; +} +a { + color: #337ab7; + text-decoration: none; +} +a:hover, +a:focus { + color: #23527c; + text-decoration: underline; +} +a:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +figure { + margin: 0; +} +img { + vertical-align: middle; +} +.img-responsive, +.thumbnail > img, +.thumbnail a > img, +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + display: block; + max-width: 100%; + height: auto; +} +.img-rounded { + border-radius: 6px; +} +.img-thumbnail { + display: inline-block; + max-width: 100%; + height: auto; + padding: 4px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: all .2s ease-in-out; + -o-transition: all .2s ease-in-out; + transition: all .2s ease-in-out; +} +.img-circle { + border-radius: 50%; +} +hr { + margin-top: 20px; + margin-bottom: 20px; + border: 0; + border-top: 1px solid #eee; +} +.sr-only { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + border: 0; +} +.sr-only-focusable:active, +.sr-only-focusable:focus { + position: static; + width: auto; + height: auto; + margin: 0; + overflow: visible; + clip: auto; +} +h1, +h2, +h3, +h4, +h5, +h6, +.h1, +.h2, +.h3, +.h4, +.h5, +.h6 { + font-family: inherit; + font-weight: 500; + line-height: 1.1; + color: inherit; +} +h1 small, +h2 small, +h3 small, +h4 small, +h5 small, +h6 small, +.h1 small, +.h2 small, +.h3 small, +.h4 small, +.h5 small, +.h6 small, +h1 .small, +h2 .small, +h3 .small, +h4 .small, +h5 .small, +h6 .small, +.h1 .small, +.h2 .small, +.h3 .small, +.h4 .small, +.h5 .small, +.h6 .small { + font-weight: normal; + line-height: 1; + color: #777; +} +h1, +.h1, +h2, +.h2, +h3, +.h3 { + margin-top: 20px; + margin-bottom: 10px; +} +h1 small, +.h1 small, +h2 small, +.h2 small, +h3 small, +.h3 small, +h1 .small, +.h1 .small, +h2 .small, +.h2 .small, +h3 .small, +.h3 .small { + font-size: 65%; +} +h4, +.h4, +h5, +.h5, +h6, +.h6 { + margin-top: 10px; + margin-bottom: 10px; +} +h4 small, +.h4 small, +h5 small, +.h5 small, +h6 small, +.h6 small, +h4 .small, +.h4 .small, +h5 .small, +.h5 .small, +h6 .small, +.h6 .small { + font-size: 75%; +} +h1, +.h1 { + font-size: 36px; +} +h2, +.h2 { + font-size: 30px; +} +h3, +.h3 { + font-size: 24px; +} +h4, +.h4 { + font-size: 18px; +} +h5, +.h5 { + font-size: 14px; +} +h6, +.h6 { + font-size: 12px; +} +p { + margin: 0 0 10px; +} +.lead { + margin-bottom: 20px; + font-size: 16px; + font-weight: 300; + line-height: 1.4; +} +@media (min-width: 768px) { + .lead { + font-size: 21px; + } +} +small, +.small { + font-size: 85%; +} +mark, +.mark { + padding: .2em; + background-color: #fcf8e3; +} +.text-left { + text-align: left; +} +.text-right { + text-align: right; +} +.text-center { + text-align: center; +} +.text-justify { + text-align: justify; +} +.text-nowrap { + white-space: nowrap; +} +.text-lowercase { + text-transform: lowercase; +} +.text-uppercase { + text-transform: uppercase; +} +.text-capitalize { + text-transform: capitalize; +} +.text-muted { + color: #777; +} +.text-primary { + color: #337ab7; +} +a.text-primary:hover { + color: #286090; +} +.text-success { + color: #3c763d; +} +a.text-success:hover { + color: #2b542c; +} +.text-info { + color: #31708f; +} +a.text-info:hover { + color: #245269; +} +.text-warning { + color: #8a6d3b; +} +a.text-warning:hover { + color: #66512c; +} +.text-danger { + color: #a94442; +} +a.text-danger:hover { + color: #843534; +} +.bg-primary { + color: #fff; + background-color: #337ab7; +} +a.bg-primary:hover { + background-color: #286090; +} +.bg-success { + background-color: #dff0d8; +} +a.bg-success:hover { + background-color: #c1e2b3; +} +.bg-info { + background-color: #d9edf7; +} +a.bg-info:hover { + background-color: #afd9ee; +} +.bg-warning { + background-color: #fcf8e3; +} +a.bg-warning:hover { + background-color: #f7ecb5; +} +.bg-danger { + background-color: #f2dede; +} +a.bg-danger:hover { + background-color: #e4b9b9; +} +.page-header { + padding-bottom: 9px; + margin: 40px 0 20px; + border-bottom: 1px solid #eee; +} +ul, +ol { + margin-top: 0; + margin-bottom: 10px; +} +ul ul, +ol ul, +ul ol, +ol ol { + margin-bottom: 0; +} +.list-unstyled { + padding-left: 0; + list-style: none; +} +.list-inline { + padding-left: 0; + margin-left: -5px; + list-style: none; +} +.list-inline > li { + display: inline-block; + padding-right: 5px; + padding-left: 5px; +} +dl { + margin-top: 0; + margin-bottom: 20px; +} +dt, +dd { + line-height: 1.42857143; +} +dt { + font-weight: bold; +} +dd { + margin-left: 0; +} +@media (min-width: 768px) { + .dl-horizontal dt { + float: left; + width: 160px; + overflow: hidden; + clear: left; + text-align: right; + text-overflow: ellipsis; + white-space: nowrap; + } + .dl-horizontal dd { + margin-left: 180px; + } +} +abbr[title], +abbr[data-original-title] { + cursor: help; + border-bottom: 1px dotted #777; +} +.initialism { + font-size: 90%; + text-transform: uppercase; +} +blockquote { + padding: 10px 20px; + margin: 0 0 20px; + font-size: 17.5px; + border-left: 5px solid #eee; +} +blockquote p:last-child, +blockquote ul:last-child, +blockquote ol:last-child { + margin-bottom: 0; +} +blockquote footer, +blockquote small, +blockquote .small { + display: block; + font-size: 80%; + line-height: 1.42857143; + color: #777; +} +blockquote footer:before, +blockquote small:before, +blockquote .small:before { + content: '\2014 \00A0'; +} +.blockquote-reverse, +blockquote.pull-right { + padding-right: 15px; + padding-left: 0; + text-align: right; + border-right: 5px solid #eee; + border-left: 0; +} +.blockquote-reverse footer:before, +blockquote.pull-right footer:before, +.blockquote-reverse small:before, +blockquote.pull-right small:before, +.blockquote-reverse .small:before, +blockquote.pull-right .small:before { + content: ''; +} +.blockquote-reverse footer:after, +blockquote.pull-right footer:after, +.blockquote-reverse small:after, +blockquote.pull-right small:after, +.blockquote-reverse .small:after, +blockquote.pull-right .small:after { + content: '\00A0 \2014'; +} +address { + margin-bottom: 20px; + font-style: normal; + line-height: 1.42857143; +} +code, +kbd, +pre, +samp { + font-family: Menlo, Monaco, Consolas, "Courier New", monospace; +} +code { + padding: 2px 4px; + font-size: 90%; + color: #c7254e; + background-color: #f9f2f4; + border-radius: 4px; +} +kbd { + padding: 2px 4px; + font-size: 90%; + color: #fff; + background-color: #333; + border-radius: 3px; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .25); +} +kbd kbd { + padding: 0; + font-size: 100%; + font-weight: bold; + -webkit-box-shadow: none; + box-shadow: none; +} +pre { + display: block; + padding: 9.5px; + margin: 0 0 10px; + font-size: 13px; + line-height: 1.42857143; + color: #333; + word-break: break-all; + word-wrap: break-word; + background-color: #f5f5f5; + border: 1px solid #ccc; + border-radius: 4px; +} +pre code { + padding: 0; + font-size: inherit; + color: inherit; + white-space: pre-wrap; + background-color: transparent; + border-radius: 0; +} +.pre-scrollable { + max-height: 340px; + overflow-y: scroll; +} +.container { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +@media (min-width: 768px) { + .container { + width: 750px; + } +} +@media (min-width: 992px) { + .container { + width: 970px; + } +} +@media (min-width: 1200px) { + .container { + width: 1170px; + } +} +.container-fluid { + padding-right: 15px; + padding-left: 15px; + margin-right: auto; + margin-left: auto; +} +.row { + margin-right: -15px; + margin-left: -15px; +} +.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 { + position: relative; + min-height: 1px; + padding-right: 15px; + padding-left: 15px; +} +.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 { + float: left; +} +.col-xs-12 { + width: 100%; +} +.col-xs-11 { + width: 91.66666667%; +} +.col-xs-10 { + width: 83.33333333%; +} +.col-xs-9 { + width: 75%; +} +.col-xs-8 { + width: 66.66666667%; +} +.col-xs-7 { + width: 58.33333333%; +} +.col-xs-6 { + width: 50%; +} +.col-xs-5 { + width: 41.66666667%; +} +.col-xs-4 { + width: 33.33333333%; +} +.col-xs-3 { + width: 25%; +} +.col-xs-2 { + width: 16.66666667%; +} +.col-xs-1 { + width: 8.33333333%; +} +.col-xs-pull-12 { + right: 100%; +} +.col-xs-pull-11 { + right: 91.66666667%; +} +.col-xs-pull-10 { + right: 83.33333333%; +} +.col-xs-pull-9 { + right: 75%; +} +.col-xs-pull-8 { + right: 66.66666667%; +} +.col-xs-pull-7 { + right: 58.33333333%; +} +.col-xs-pull-6 { + right: 50%; +} +.col-xs-pull-5 { + right: 41.66666667%; +} +.col-xs-pull-4 { + right: 33.33333333%; +} +.col-xs-pull-3 { + right: 25%; +} +.col-xs-pull-2 { + right: 16.66666667%; +} +.col-xs-pull-1 { + right: 8.33333333%; +} +.col-xs-pull-0 { + right: auto; +} +.col-xs-push-12 { + left: 100%; +} +.col-xs-push-11 { + left: 91.66666667%; +} +.col-xs-push-10 { + left: 83.33333333%; +} +.col-xs-push-9 { + left: 75%; +} +.col-xs-push-8 { + left: 66.66666667%; +} +.col-xs-push-7 { + left: 58.33333333%; +} +.col-xs-push-6 { + left: 50%; +} +.col-xs-push-5 { + left: 41.66666667%; +} +.col-xs-push-4 { + left: 33.33333333%; +} +.col-xs-push-3 { + left: 25%; +} +.col-xs-push-2 { + left: 16.66666667%; +} +.col-xs-push-1 { + left: 8.33333333%; +} +.col-xs-push-0 { + left: auto; +} +.col-xs-offset-12 { + margin-left: 100%; +} +.col-xs-offset-11 { + margin-left: 91.66666667%; +} +.col-xs-offset-10 { + margin-left: 83.33333333%; +} +.col-xs-offset-9 { + margin-left: 75%; +} +.col-xs-offset-8 { + margin-left: 66.66666667%; +} +.col-xs-offset-7 { + margin-left: 58.33333333%; +} +.col-xs-offset-6 { + margin-left: 50%; +} +.col-xs-offset-5 { + margin-left: 41.66666667%; +} +.col-xs-offset-4 { + margin-left: 33.33333333%; +} +.col-xs-offset-3 { + margin-left: 25%; +} +.col-xs-offset-2 { + margin-left: 16.66666667%; +} +.col-xs-offset-1 { + margin-left: 8.33333333%; +} +.col-xs-offset-0 { + margin-left: 0; +} +@media (min-width: 768px) { + .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 { + float: left; + } + .col-sm-12 { + width: 100%; + } + .col-sm-11 { + width: 91.66666667%; + } + .col-sm-10 { + width: 83.33333333%; + } + .col-sm-9 { + width: 75%; + } + .col-sm-8 { + width: 66.66666667%; + } + .col-sm-7 { + width: 58.33333333%; + } + .col-sm-6 { + width: 50%; + } + .col-sm-5 { + width: 41.66666667%; + } + .col-sm-4 { + width: 33.33333333%; + } + .col-sm-3 { + width: 25%; + } + .col-sm-2 { + width: 16.66666667%; + } + .col-sm-1 { + width: 8.33333333%; + } + .col-sm-pull-12 { + right: 100%; + } + .col-sm-pull-11 { + right: 91.66666667%; + } + .col-sm-pull-10 { + right: 83.33333333%; + } + .col-sm-pull-9 { + right: 75%; + } + .col-sm-pull-8 { + right: 66.66666667%; + } + .col-sm-pull-7 { + right: 58.33333333%; + } + .col-sm-pull-6 { + right: 50%; + } + .col-sm-pull-5 { + right: 41.66666667%; + } + .col-sm-pull-4 { + right: 33.33333333%; + } + .col-sm-pull-3 { + right: 25%; + } + .col-sm-pull-2 { + right: 16.66666667%; + } + .col-sm-pull-1 { + right: 8.33333333%; + } + .col-sm-pull-0 { + right: auto; + } + .col-sm-push-12 { + left: 100%; + } + .col-sm-push-11 { + left: 91.66666667%; + } + .col-sm-push-10 { + left: 83.33333333%; + } + .col-sm-push-9 { + left: 75%; + } + .col-sm-push-8 { + left: 66.66666667%; + } + .col-sm-push-7 { + left: 58.33333333%; + } + .col-sm-push-6 { + left: 50%; + } + .col-sm-push-5 { + left: 41.66666667%; + } + .col-sm-push-4 { + left: 33.33333333%; + } + .col-sm-push-3 { + left: 25%; + } + .col-sm-push-2 { + left: 16.66666667%; + } + .col-sm-push-1 { + left: 8.33333333%; + } + .col-sm-push-0 { + left: auto; + } + .col-sm-offset-12 { + margin-left: 100%; + } + .col-sm-offset-11 { + margin-left: 91.66666667%; + } + .col-sm-offset-10 { + margin-left: 83.33333333%; + } + .col-sm-offset-9 { + margin-left: 75%; + } + .col-sm-offset-8 { + margin-left: 66.66666667%; + } + .col-sm-offset-7 { + margin-left: 58.33333333%; + } + .col-sm-offset-6 { + margin-left: 50%; + } + .col-sm-offset-5 { + margin-left: 41.66666667%; + } + .col-sm-offset-4 { + margin-left: 33.33333333%; + } + .col-sm-offset-3 { + margin-left: 25%; + } + .col-sm-offset-2 { + margin-left: 16.66666667%; + } + .col-sm-offset-1 { + margin-left: 8.33333333%; + } + .col-sm-offset-0 { + margin-left: 0; + } +} +@media (min-width: 992px) { + .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 { + float: left; + } + .col-md-12 { + width: 100%; + } + .col-md-11 { + width: 91.66666667%; + } + .col-md-10 { + width: 83.33333333%; + } + .col-md-9 { + width: 75%; + } + .col-md-8 { + width: 66.66666667%; + } + .col-md-7 { + width: 58.33333333%; + } + .col-md-6 { + width: 50%; + } + .col-md-5 { + width: 41.66666667%; + } + .col-md-4 { + width: 33.33333333%; + } + .col-md-3 { + width: 25%; + } + .col-md-2 { + width: 16.66666667%; + } + .col-md-1 { + width: 8.33333333%; + } + .col-md-pull-12 { + right: 100%; + } + .col-md-pull-11 { + right: 91.66666667%; + } + .col-md-pull-10 { + right: 83.33333333%; + } + .col-md-pull-9 { + right: 75%; + } + .col-md-pull-8 { + right: 66.66666667%; + } + .col-md-pull-7 { + right: 58.33333333%; + } + .col-md-pull-6 { + right: 50%; + } + .col-md-pull-5 { + right: 41.66666667%; + } + .col-md-pull-4 { + right: 33.33333333%; + } + .col-md-pull-3 { + right: 25%; + } + .col-md-pull-2 { + right: 16.66666667%; + } + .col-md-pull-1 { + right: 8.33333333%; + } + .col-md-pull-0 { + right: auto; + } + .col-md-push-12 { + left: 100%; + } + .col-md-push-11 { + left: 91.66666667%; + } + .col-md-push-10 { + left: 83.33333333%; + } + .col-md-push-9 { + left: 75%; + } + .col-md-push-8 { + left: 66.66666667%; + } + .col-md-push-7 { + left: 58.33333333%; + } + .col-md-push-6 { + left: 50%; + } + .col-md-push-5 { + left: 41.66666667%; + } + .col-md-push-4 { + left: 33.33333333%; + } + .col-md-push-3 { + left: 25%; + } + .col-md-push-2 { + left: 16.66666667%; + } + .col-md-push-1 { + left: 8.33333333%; + } + .col-md-push-0 { + left: auto; + } + .col-md-offset-12 { + margin-left: 100%; + } + .col-md-offset-11 { + margin-left: 91.66666667%; + } + .col-md-offset-10 { + margin-left: 83.33333333%; + } + .col-md-offset-9 { + margin-left: 75%; + } + .col-md-offset-8 { + margin-left: 66.66666667%; + } + .col-md-offset-7 { + margin-left: 58.33333333%; + } + .col-md-offset-6 { + margin-left: 50%; + } + .col-md-offset-5 { + margin-left: 41.66666667%; + } + .col-md-offset-4 { + margin-left: 33.33333333%; + } + .col-md-offset-3 { + margin-left: 25%; + } + .col-md-offset-2 { + margin-left: 16.66666667%; + } + .col-md-offset-1 { + margin-left: 8.33333333%; + } + .col-md-offset-0 { + margin-left: 0; + } +} +@media (min-width: 1200px) { + .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 { + float: left; + } + .col-lg-12 { + width: 100%; + } + .col-lg-11 { + width: 91.66666667%; + } + .col-lg-10 { + width: 83.33333333%; + } + .col-lg-9 { + width: 75%; + } + .col-lg-8 { + width: 66.66666667%; + } + .col-lg-7 { + width: 58.33333333%; + } + .col-lg-6 { + width: 50%; + } + .col-lg-5 { + width: 41.66666667%; + } + .col-lg-4 { + width: 33.33333333%; + } + .col-lg-3 { + width: 25%; + } + .col-lg-2 { + width: 16.66666667%; + } + .col-lg-1 { + width: 8.33333333%; + } + .col-lg-pull-12 { + right: 100%; + } + .col-lg-pull-11 { + right: 91.66666667%; + } + .col-lg-pull-10 { + right: 83.33333333%; + } + .col-lg-pull-9 { + right: 75%; + } + .col-lg-pull-8 { + right: 66.66666667%; + } + .col-lg-pull-7 { + right: 58.33333333%; + } + .col-lg-pull-6 { + right: 50%; + } + .col-lg-pull-5 { + right: 41.66666667%; + } + .col-lg-pull-4 { + right: 33.33333333%; + } + .col-lg-pull-3 { + right: 25%; + } + .col-lg-pull-2 { + right: 16.66666667%; + } + .col-lg-pull-1 { + right: 8.33333333%; + } + .col-lg-pull-0 { + right: auto; + } + .col-lg-push-12 { + left: 100%; + } + .col-lg-push-11 { + left: 91.66666667%; + } + .col-lg-push-10 { + left: 83.33333333%; + } + .col-lg-push-9 { + left: 75%; + } + .col-lg-push-8 { + left: 66.66666667%; + } + .col-lg-push-7 { + left: 58.33333333%; + } + .col-lg-push-6 { + left: 50%; + } + .col-lg-push-5 { + left: 41.66666667%; + } + .col-lg-push-4 { + left: 33.33333333%; + } + .col-lg-push-3 { + left: 25%; + } + .col-lg-push-2 { + left: 16.66666667%; + } + .col-lg-push-1 { + left: 8.33333333%; + } + .col-lg-push-0 { + left: auto; + } + .col-lg-offset-12 { + margin-left: 100%; + } + .col-lg-offset-11 { + margin-left: 91.66666667%; + } + .col-lg-offset-10 { + margin-left: 83.33333333%; + } + .col-lg-offset-9 { + margin-left: 75%; + } + .col-lg-offset-8 { + margin-left: 66.66666667%; + } + .col-lg-offset-7 { + margin-left: 58.33333333%; + } + .col-lg-offset-6 { + margin-left: 50%; + } + .col-lg-offset-5 { + margin-left: 41.66666667%; + } + .col-lg-offset-4 { + margin-left: 33.33333333%; + } + .col-lg-offset-3 { + margin-left: 25%; + } + .col-lg-offset-2 { + margin-left: 16.66666667%; + } + .col-lg-offset-1 { + margin-left: 8.33333333%; + } + .col-lg-offset-0 { + margin-left: 0; + } +} +table { + background-color: transparent; +} +caption { + padding-top: 8px; + padding-bottom: 8px; + color: #777; + text-align: left; +} +th { + text-align: left; +} +.table { + width: 100%; + max-width: 100%; + margin-bottom: 20px; +} +.table > thead > tr > th, +.table > tbody > tr > th, +.table > tfoot > tr > th, +.table > thead > tr > td, +.table > tbody > tr > td, +.table > tfoot > tr > td { + padding: 8px; + line-height: 1.42857143; + vertical-align: top; + border-top: 1px solid #ddd; +} +.table > thead > tr > th { + vertical-align: bottom; + border-bottom: 2px solid #ddd; +} +.table > caption + thead > tr:first-child > th, +.table > colgroup + thead > tr:first-child > th, +.table > thead:first-child > tr:first-child > th, +.table > caption + thead > tr:first-child > td, +.table > colgroup + thead > tr:first-child > td, +.table > thead:first-child > tr:first-child > td { + border-top: 0; +} +.table > tbody + tbody { + border-top: 2px solid #ddd; +} +.table .table { + background-color: #fff; +} +.table-condensed > thead > tr > th, +.table-condensed > tbody > tr > th, +.table-condensed > tfoot > tr > th, +.table-condensed > thead > tr > td, +.table-condensed > tbody > tr > td, +.table-condensed > tfoot > tr > td { + padding: 5px; +} +.table-bordered { + border: 1px solid #ddd; +} +.table-bordered > thead > tr > th, +.table-bordered > tbody > tr > th, +.table-bordered > tfoot > tr > th, +.table-bordered > thead > tr > td, +.table-bordered > tbody > tr > td, +.table-bordered > tfoot > tr > td { + border: 1px solid #ddd; +} +.table-bordered > thead > tr > th, +.table-bordered > thead > tr > td { + border-bottom-width: 2px; +} +.table-striped > tbody > tr:nth-child(odd) { + background-color: #f9f9f9; +} +.table-hover > tbody > tr:hover { + background-color: #f5f5f5; +} +table col[class*="col-"] { + position: static; + display: table-column; + float: none; +} +table td[class*="col-"], +table th[class*="col-"] { + position: static; + display: table-cell; + float: none; +} +.table > thead > tr > td.active, +.table > tbody > tr > td.active, +.table > tfoot > tr > td.active, +.table > thead > tr > th.active, +.table > tbody > tr > th.active, +.table > tfoot > tr > th.active, +.table > thead > tr.active > td, +.table > tbody > tr.active > td, +.table > tfoot > tr.active > td, +.table > thead > tr.active > th, +.table > tbody > tr.active > th, +.table > tfoot > tr.active > th { + background-color: #f5f5f5; +} +.table-hover > tbody > tr > td.active:hover, +.table-hover > tbody > tr > th.active:hover, +.table-hover > tbody > tr.active:hover > td, +.table-hover > tbody > tr:hover > .active, +.table-hover > tbody > tr.active:hover > th { + background-color: #e8e8e8; +} +.table > thead > tr > td.success, +.table > tbody > tr > td.success, +.table > tfoot > tr > td.success, +.table > thead > tr > th.success, +.table > tbody > tr > th.success, +.table > tfoot > tr > th.success, +.table > thead > tr.success > td, +.table > tbody > tr.success > td, +.table > tfoot > tr.success > td, +.table > thead > tr.success > th, +.table > tbody > tr.success > th, +.table > tfoot > tr.success > th { + background-color: #dff0d8; +} +.table-hover > tbody > tr > td.success:hover, +.table-hover > tbody > tr > th.success:hover, +.table-hover > tbody > tr.success:hover > td, +.table-hover > tbody > tr:hover > .success, +.table-hover > tbody > tr.success:hover > th { + background-color: #d0e9c6; +} +.table > thead > tr > td.info, +.table > tbody > tr > td.info, +.table > tfoot > tr > td.info, +.table > thead > tr > th.info, +.table > tbody > tr > th.info, +.table > tfoot > tr > th.info, +.table > thead > tr.info > td, +.table > tbody > tr.info > td, +.table > tfoot > tr.info > td, +.table > thead > tr.info > th, +.table > tbody > tr.info > th, +.table > tfoot > tr.info > th { + background-color: #d9edf7; +} +.table-hover > tbody > tr > td.info:hover, +.table-hover > tbody > tr > th.info:hover, +.table-hover > tbody > tr.info:hover > td, +.table-hover > tbody > tr:hover > .info, +.table-hover > tbody > tr.info:hover > th { + background-color: #c4e3f3; +} +.table > thead > tr > td.warning, +.table > tbody > tr > td.warning, +.table > tfoot > tr > td.warning, +.table > thead > tr > th.warning, +.table > tbody > tr > th.warning, +.table > tfoot > tr > th.warning, +.table > thead > tr.warning > td, +.table > tbody > tr.warning > td, +.table > tfoot > tr.warning > td, +.table > thead > tr.warning > th, +.table > tbody > tr.warning > th, +.table > tfoot > tr.warning > th { + background-color: #fcf8e3; +} +.table-hover > tbody > tr > td.warning:hover, +.table-hover > tbody > tr > th.warning:hover, +.table-hover > tbody > tr.warning:hover > td, +.table-hover > tbody > tr:hover > .warning, +.table-hover > tbody > tr.warning:hover > th { + background-color: #faf2cc; +} +.table > thead > tr > td.danger, +.table > tbody > tr > td.danger, +.table > tfoot > tr > td.danger, +.table > thead > tr > th.danger, +.table > tbody > tr > th.danger, +.table > tfoot > tr > th.danger, +.table > thead > tr.danger > td, +.table > tbody > tr.danger > td, +.table > tfoot > tr.danger > td, +.table > thead > tr.danger > th, +.table > tbody > tr.danger > th, +.table > tfoot > tr.danger > th { + background-color: #f2dede; +} +.table-hover > tbody > tr > td.danger:hover, +.table-hover > tbody > tr > th.danger:hover, +.table-hover > tbody > tr.danger:hover > td, +.table-hover > tbody > tr:hover > .danger, +.table-hover > tbody > tr.danger:hover > th { + background-color: #ebcccc; +} +.table-responsive { + min-height: .01%; + overflow-x: auto; +} +@media screen and (max-width: 767px) { + .table-responsive { + width: 100%; + margin-bottom: 15px; + overflow-y: hidden; + -ms-overflow-style: -ms-autohiding-scrollbar; + border: 1px solid #ddd; + } + .table-responsive > .table { + margin-bottom: 0; + } + .table-responsive > .table > thead > tr > th, + .table-responsive > .table > tbody > tr > th, + .table-responsive > .table > tfoot > tr > th, + .table-responsive > .table > thead > tr > td, + .table-responsive > .table > tbody > tr > td, + .table-responsive > .table > tfoot > tr > td { + white-space: nowrap; + } + .table-responsive > .table-bordered { + border: 0; + } + .table-responsive > .table-bordered > thead > tr > th:first-child, + .table-responsive > .table-bordered > tbody > tr > th:first-child, + .table-responsive > .table-bordered > tfoot > tr > th:first-child, + .table-responsive > .table-bordered > thead > tr > td:first-child, + .table-responsive > .table-bordered > tbody > tr > td:first-child, + .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; + } + .table-responsive > .table-bordered > thead > tr > th:last-child, + .table-responsive > .table-bordered > tbody > tr > th:last-child, + .table-responsive > .table-bordered > tfoot > tr > th:last-child, + .table-responsive > .table-bordered > thead > tr > td:last-child, + .table-responsive > .table-bordered > tbody > tr > td:last-child, + .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; + } + .table-responsive > .table-bordered > tbody > tr:last-child > th, + .table-responsive > .table-bordered > tfoot > tr:last-child > th, + .table-responsive > .table-bordered > tbody > tr:last-child > td, + .table-responsive > .table-bordered > tfoot > tr:last-child > td { + border-bottom: 0; + } +} +fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; +} +legend { + display: block; + width: 100%; + padding: 0; + margin-bottom: 20px; + font-size: 21px; + line-height: inherit; + color: #333; + border: 0; + border-bottom: 1px solid #e5e5e5; +} +label { + display: inline-block; + max-width: 100%; + margin-bottom: 5px; + font-weight: bold; +} +input[type="search"] { + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +input[type="radio"], +input[type="checkbox"] { + margin: 4px 0 0; + margin-top: 1px \9; + line-height: normal; +} +input[type="file"] { + display: block; +} +input[type="range"] { + display: block; + width: 100%; +} +select[multiple], +select[size] { + height: auto; +} +input[type="file"]:focus, +input[type="radio"]:focus, +input[type="checkbox"]:focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +output { + display: block; + padding-top: 7px; + font-size: 14px; + line-height: 1.42857143; + color: #555; +} +.form-control { + display: block; + width: 100%; + height: 34px; + padding: 6px 12px; + font-size: 14px; + line-height: 1.42857143; + color: #555; + background-color: #fff; + background-image: none; + border: 1px solid #ccc; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + -webkit-transition: border-color ease-in-out .15s, -webkit-box-shadow ease-in-out .15s; + -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; + transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s; +} +.form-control:focus { + border-color: #66afe9; + outline: 0; + -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, .6); +} +.form-control::-moz-placeholder { + color: #999; + opacity: 1; +} +.form-control:-ms-input-placeholder { + color: #999; +} +.form-control::-webkit-input-placeholder { + color: #999; +} +.form-control[disabled], +.form-control[readonly], +fieldset[disabled] .form-control { + cursor: not-allowed; + background-color: #eee; + opacity: 1; +} +textarea.form-control { + height: auto; +} +input[type="search"] { + -webkit-appearance: none; +} +@media screen and (-webkit-min-device-pixel-ratio: 0) { + input[type="date"], + input[type="time"], + input[type="datetime-local"], + input[type="month"] { + line-height: 34px; + } + input[type="date"].input-sm, + input[type="time"].input-sm, + input[type="datetime-local"].input-sm, + input[type="month"].input-sm { + line-height: 30px; + } + input[type="date"].input-lg, + input[type="time"].input-lg, + input[type="datetime-local"].input-lg, + input[type="month"].input-lg { + line-height: 46px; + } +} +.form-group { + margin-bottom: 15px; +} +.radio, +.checkbox { + position: relative; + display: block; + margin-top: 10px; + margin-bottom: 10px; +} +.radio label, +.checkbox label { + min-height: 20px; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + cursor: pointer; +} +.radio input[type="radio"], +.radio-inline input[type="radio"], +.checkbox input[type="checkbox"], +.checkbox-inline input[type="checkbox"] { + position: absolute; + margin-top: 4px \9; + margin-left: -20px; +} +.radio + .radio, +.checkbox + .checkbox { + margin-top: -5px; +} +.radio-inline, +.checkbox-inline { + display: inline-block; + padding-left: 20px; + margin-bottom: 0; + font-weight: normal; + vertical-align: middle; + cursor: pointer; +} +.radio-inline + .radio-inline, +.checkbox-inline + .checkbox-inline { + margin-top: 0; + margin-left: 10px; +} +input[type="radio"][disabled], +input[type="checkbox"][disabled], +input[type="radio"].disabled, +input[type="checkbox"].disabled, +fieldset[disabled] input[type="radio"], +fieldset[disabled] input[type="checkbox"] { + cursor: not-allowed; +} +.radio-inline.disabled, +.checkbox-inline.disabled, +fieldset[disabled] .radio-inline, +fieldset[disabled] .checkbox-inline { + cursor: not-allowed; +} +.radio.disabled label, +.checkbox.disabled label, +fieldset[disabled] .radio label, +fieldset[disabled] .checkbox label { + cursor: not-allowed; +} +.form-control-static { + padding-top: 7px; + padding-bottom: 7px; + margin-bottom: 0; +} +.form-control-static.input-lg, +.form-control-static.input-sm { + padding-right: 0; + padding-left: 0; +} +.input-sm, +.form-group-sm .form-control { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-sm, +select.form-group-sm .form-control { + height: 30px; + line-height: 30px; +} +textarea.input-sm, +textarea.form-group-sm .form-control, +select[multiple].input-sm, +select[multiple].form-group-sm .form-control { + height: auto; +} +.input-lg, +.form-group-lg .form-control { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} +select.input-lg, +select.form-group-lg .form-control { + height: 46px; + line-height: 46px; +} +textarea.input-lg, +textarea.form-group-lg .form-control, +select[multiple].input-lg, +select[multiple].form-group-lg .form-control { + height: auto; +} +.has-feedback { + position: relative; +} +.has-feedback .form-control { + padding-right: 42.5px; +} +.form-control-feedback { + position: absolute; + top: 0; + right: 0; + z-index: 2; + display: block; + width: 34px; + height: 34px; + line-height: 34px; + text-align: center; + pointer-events: none; +} +.input-lg + .form-control-feedback { + width: 46px; + height: 46px; + line-height: 46px; +} +.input-sm + .form-control-feedback { + width: 30px; + height: 30px; + line-height: 30px; +} +.has-success .help-block, +.has-success .control-label, +.has-success .radio, +.has-success .checkbox, +.has-success .radio-inline, +.has-success .checkbox-inline, +.has-success.radio label, +.has-success.checkbox label, +.has-success.radio-inline label, +.has-success.checkbox-inline label { + color: #3c763d; +} +.has-success .form-control { + border-color: #3c763d; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-success .form-control:focus { + border-color: #2b542c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #67b168; +} +.has-success .input-group-addon { + color: #3c763d; + background-color: #dff0d8; + border-color: #3c763d; +} +.has-success .form-control-feedback { + color: #3c763d; +} +.has-warning .help-block, +.has-warning .control-label, +.has-warning .radio, +.has-warning .checkbox, +.has-warning .radio-inline, +.has-warning .checkbox-inline, +.has-warning.radio label, +.has-warning.checkbox label, +.has-warning.radio-inline label, +.has-warning.checkbox-inline label { + color: #8a6d3b; +} +.has-warning .form-control { + border-color: #8a6d3b; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-warning .form-control:focus { + border-color: #66512c; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #c0a16b; +} +.has-warning .input-group-addon { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #8a6d3b; +} +.has-warning .form-control-feedback { + color: #8a6d3b; +} +.has-error .help-block, +.has-error .control-label, +.has-error .radio, +.has-error .checkbox, +.has-error .radio-inline, +.has-error .checkbox-inline, +.has-error.radio label, +.has-error.checkbox label, +.has-error.radio-inline label, +.has-error.checkbox-inline label { + color: #a94442; +} +.has-error .form-control { + border-color: #a94442; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075); +} +.has-error .form-control:focus { + border-color: #843534; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .075), 0 0 6px #ce8483; +} +.has-error .input-group-addon { + color: #a94442; + background-color: #f2dede; + border-color: #a94442; +} +.has-error .form-control-feedback { + color: #a94442; +} +.has-feedback label ~ .form-control-feedback { + top: 25px; +} +.has-feedback label.sr-only ~ .form-control-feedback { + top: 0; +} +.help-block { + display: block; + margin-top: 5px; + margin-bottom: 10px; + color: #737373; +} +@media (min-width: 768px) { + .form-inline .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .form-inline .form-control-static { + display: inline-block; + } + .form-inline .input-group { + display: inline-table; + vertical-align: middle; + } + .form-inline .input-group .input-group-addon, + .form-inline .input-group .input-group-btn, + .form-inline .input-group .form-control { + width: auto; + } + .form-inline .input-group > .form-control { + width: 100%; + } + .form-inline .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio, + .form-inline .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .form-inline .radio label, + .form-inline .checkbox label { + padding-left: 0; + } + .form-inline .radio input[type="radio"], + .form-inline .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .form-inline .has-feedback .form-control-feedback { + top: 0; + } +} +.form-horizontal .radio, +.form-horizontal .checkbox, +.form-horizontal .radio-inline, +.form-horizontal .checkbox-inline { + padding-top: 7px; + margin-top: 0; + margin-bottom: 0; +} +.form-horizontal .radio, +.form-horizontal .checkbox { + min-height: 27px; +} +.form-horizontal .form-group { + margin-right: -15px; + margin-left: -15px; +} +@media (min-width: 768px) { + .form-horizontal .control-label { + padding-top: 7px; + margin-bottom: 0; + text-align: right; + } +} +.form-horizontal .has-feedback .form-control-feedback { + right: 15px; +} +@media (min-width: 768px) { + .form-horizontal .form-group-lg .control-label { + padding-top: 14.3px; + } +} +@media (min-width: 768px) { + .form-horizontal .form-group-sm .control-label { + padding-top: 6px; + } +} +.btn { + display: inline-block; + padding: 6px 12px; + margin-bottom: 0; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: center; + white-space: nowrap; + vertical-align: middle; + -ms-touch-action: manipulation; + touch-action: manipulation; + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.btn:focus, +.btn:active:focus, +.btn.active:focus, +.btn.focus, +.btn:active.focus, +.btn.active.focus { + outline: thin dotted; + outline: 5px auto -webkit-focus-ring-color; + outline-offset: -2px; +} +.btn:hover, +.btn:focus, +.btn.focus { + color: #333; + text-decoration: none; +} +.btn:active, +.btn.active { + background-image: none; + outline: 0; + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); +} +.btn.disabled, +.btn[disabled], +fieldset[disabled] .btn { + pointer-events: none; + cursor: not-allowed; + filter: alpha(opacity=65); + -webkit-box-shadow: none; + box-shadow: none; + opacity: .65; +} +.btn-default { + color: #333; + background-color: #fff; + border-color: #ccc; +} +.btn-default:hover, +.btn-default:focus, +.btn-default.focus, +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + color: #333; + background-color: #e6e6e6; + border-color: #adadad; +} +.btn-default:active, +.btn-default.active, +.open > .dropdown-toggle.btn-default { + background-image: none; +} +.btn-default.disabled, +.btn-default[disabled], +fieldset[disabled] .btn-default, +.btn-default.disabled:hover, +.btn-default[disabled]:hover, +fieldset[disabled] .btn-default:hover, +.btn-default.disabled:focus, +.btn-default[disabled]:focus, +fieldset[disabled] .btn-default:focus, +.btn-default.disabled.focus, +.btn-default[disabled].focus, +fieldset[disabled] .btn-default.focus, +.btn-default.disabled:active, +.btn-default[disabled]:active, +fieldset[disabled] .btn-default:active, +.btn-default.disabled.active, +.btn-default[disabled].active, +fieldset[disabled] .btn-default.active { + background-color: #fff; + border-color: #ccc; +} +.btn-default .badge { + color: #fff; + background-color: #333; +} +.btn-primary { + color: #fff; + background-color: #337ab7; + border-color: #2e6da4; +} +.btn-primary:hover, +.btn-primary:focus, +.btn-primary.focus, +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + color: #fff; + background-color: #286090; + border-color: #204d74; +} +.btn-primary:active, +.btn-primary.active, +.open > .dropdown-toggle.btn-primary { + background-image: none; +} +.btn-primary.disabled, +.btn-primary[disabled], +fieldset[disabled] .btn-primary, +.btn-primary.disabled:hover, +.btn-primary[disabled]:hover, +fieldset[disabled] .btn-primary:hover, +.btn-primary.disabled:focus, +.btn-primary[disabled]:focus, +fieldset[disabled] .btn-primary:focus, +.btn-primary.disabled.focus, +.btn-primary[disabled].focus, +fieldset[disabled] .btn-primary.focus, +.btn-primary.disabled:active, +.btn-primary[disabled]:active, +fieldset[disabled] .btn-primary:active, +.btn-primary.disabled.active, +.btn-primary[disabled].active, +fieldset[disabled] .btn-primary.active { + background-color: #337ab7; + border-color: #2e6da4; +} +.btn-primary .badge { + color: #337ab7; + background-color: #fff; +} +.btn-success { + color: #fff; + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success:hover, +.btn-success:focus, +.btn-success.focus, +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + color: #fff; + background-color: #449d44; + border-color: #398439; +} +.btn-success:active, +.btn-success.active, +.open > .dropdown-toggle.btn-success { + background-image: none; +} +.btn-success.disabled, +.btn-success[disabled], +fieldset[disabled] .btn-success, +.btn-success.disabled:hover, +.btn-success[disabled]:hover, +fieldset[disabled] .btn-success:hover, +.btn-success.disabled:focus, +.btn-success[disabled]:focus, +fieldset[disabled] .btn-success:focus, +.btn-success.disabled.focus, +.btn-success[disabled].focus, +fieldset[disabled] .btn-success.focus, +.btn-success.disabled:active, +.btn-success[disabled]:active, +fieldset[disabled] .btn-success:active, +.btn-success.disabled.active, +.btn-success[disabled].active, +fieldset[disabled] .btn-success.active { + background-color: #5cb85c; + border-color: #4cae4c; +} +.btn-success .badge { + color: #5cb85c; + background-color: #fff; +} +.btn-info { + color: #fff; + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info:hover, +.btn-info:focus, +.btn-info.focus, +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + color: #fff; + background-color: #31b0d5; + border-color: #269abc; +} +.btn-info:active, +.btn-info.active, +.open > .dropdown-toggle.btn-info { + background-image: none; +} +.btn-info.disabled, +.btn-info[disabled], +fieldset[disabled] .btn-info, +.btn-info.disabled:hover, +.btn-info[disabled]:hover, +fieldset[disabled] .btn-info:hover, +.btn-info.disabled:focus, +.btn-info[disabled]:focus, +fieldset[disabled] .btn-info:focus, +.btn-info.disabled.focus, +.btn-info[disabled].focus, +fieldset[disabled] .btn-info.focus, +.btn-info.disabled:active, +.btn-info[disabled]:active, +fieldset[disabled] .btn-info:active, +.btn-info.disabled.active, +.btn-info[disabled].active, +fieldset[disabled] .btn-info.active { + background-color: #5bc0de; + border-color: #46b8da; +} +.btn-info .badge { + color: #5bc0de; + background-color: #fff; +} +.btn-warning { + color: #fff; + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning:hover, +.btn-warning:focus, +.btn-warning.focus, +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + color: #fff; + background-color: #ec971f; + border-color: #d58512; +} +.btn-warning:active, +.btn-warning.active, +.open > .dropdown-toggle.btn-warning { + background-image: none; +} +.btn-warning.disabled, +.btn-warning[disabled], +fieldset[disabled] .btn-warning, +.btn-warning.disabled:hover, +.btn-warning[disabled]:hover, +fieldset[disabled] .btn-warning:hover, +.btn-warning.disabled:focus, +.btn-warning[disabled]:focus, +fieldset[disabled] .btn-warning:focus, +.btn-warning.disabled.focus, +.btn-warning[disabled].focus, +fieldset[disabled] .btn-warning.focus, +.btn-warning.disabled:active, +.btn-warning[disabled]:active, +fieldset[disabled] .btn-warning:active, +.btn-warning.disabled.active, +.btn-warning[disabled].active, +fieldset[disabled] .btn-warning.active { + background-color: #f0ad4e; + border-color: #eea236; +} +.btn-warning .badge { + color: #f0ad4e; + background-color: #fff; +} +.btn-danger { + color: #fff; + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger:hover, +.btn-danger:focus, +.btn-danger.focus, +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + color: #fff; + background-color: #c9302c; + border-color: #ac2925; +} +.btn-danger:active, +.btn-danger.active, +.open > .dropdown-toggle.btn-danger { + background-image: none; +} +.btn-danger.disabled, +.btn-danger[disabled], +fieldset[disabled] .btn-danger, +.btn-danger.disabled:hover, +.btn-danger[disabled]:hover, +fieldset[disabled] .btn-danger:hover, +.btn-danger.disabled:focus, +.btn-danger[disabled]:focus, +fieldset[disabled] .btn-danger:focus, +.btn-danger.disabled.focus, +.btn-danger[disabled].focus, +fieldset[disabled] .btn-danger.focus, +.btn-danger.disabled:active, +.btn-danger[disabled]:active, +fieldset[disabled] .btn-danger:active, +.btn-danger.disabled.active, +.btn-danger[disabled].active, +fieldset[disabled] .btn-danger.active { + background-color: #d9534f; + border-color: #d43f3a; +} +.btn-danger .badge { + color: #d9534f; + background-color: #fff; +} +.btn-link { + font-weight: normal; + color: #337ab7; + border-radius: 0; +} +.btn-link, +.btn-link:active, +.btn-link.active, +.btn-link[disabled], +fieldset[disabled] .btn-link { + background-color: transparent; + -webkit-box-shadow: none; + box-shadow: none; +} +.btn-link, +.btn-link:hover, +.btn-link:focus, +.btn-link:active { + border-color: transparent; +} +.btn-link:hover, +.btn-link:focus { + color: #23527c; + text-decoration: underline; + background-color: transparent; +} +.btn-link[disabled]:hover, +fieldset[disabled] .btn-link:hover, +.btn-link[disabled]:focus, +fieldset[disabled] .btn-link:focus { + color: #777; + text-decoration: none; +} +.btn-lg, +.btn-group-lg > .btn { + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} +.btn-sm, +.btn-group-sm > .btn { + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-xs, +.btn-group-xs > .btn { + padding: 1px 5px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +.btn-block { + display: block; + width: 100%; +} +.btn-block + .btn-block { + margin-top: 5px; +} +input[type="submit"].btn-block, +input[type="reset"].btn-block, +input[type="button"].btn-block { + width: 100%; +} +.fade { + opacity: 0; + -webkit-transition: opacity .15s linear; + -o-transition: opacity .15s linear; + transition: opacity .15s linear; +} +.fade.in { + opacity: 1; +} +.collapse { + display: none; + visibility: hidden; +} +.collapse.in { + display: block; + visibility: visible; +} +tr.collapse.in { + display: table-row; +} +tbody.collapse.in { + display: table-row-group; +} +.collapsing { + position: relative; + height: 0; + overflow: hidden; + -webkit-transition-timing-function: ease; + -o-transition-timing-function: ease; + transition-timing-function: ease; + -webkit-transition-duration: .35s; + -o-transition-duration: .35s; + transition-duration: .35s; + -webkit-transition-property: height, visibility; + -o-transition-property: height, visibility; + transition-property: height, visibility; +} +.caret { + display: inline-block; + width: 0; + height: 0; + margin-left: 2px; + vertical-align: middle; + border-top: 4px solid; + border-right: 4px solid transparent; + border-left: 4px solid transparent; +} +.dropdown { + position: relative; +} +.dropdown-toggle:focus { + outline: 0; +} +.dropdown-menu { + position: absolute; + top: 100%; + left: 0; + z-index: 1000; + display: none; + float: left; + min-width: 160px; + padding: 5px 0; + margin: 2px 0 0; + font-size: 14px; + text-align: left; + list-style: none; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, .15); + border-radius: 4px; + -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, .175); + box-shadow: 0 6px 12px rgba(0, 0, 0, .175); +} +.dropdown-menu.pull-right { + right: 0; + left: auto; +} +.dropdown-menu .divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.dropdown-menu > li > a { + display: block; + padding: 3px 20px; + clear: both; + font-weight: normal; + line-height: 1.42857143; + color: #333; + white-space: nowrap; +} +.dropdown-menu > li > a:hover, +.dropdown-menu > li > a:focus { + color: #262626; + text-decoration: none; + background-color: #f5f5f5; +} +.dropdown-menu > .active > a, +.dropdown-menu > .active > a:hover, +.dropdown-menu > .active > a:focus { + color: #fff; + text-decoration: none; + background-color: #337ab7; + outline: 0; +} +.dropdown-menu > .disabled > a, +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + color: #777; +} +.dropdown-menu > .disabled > a:hover, +.dropdown-menu > .disabled > a:focus { + text-decoration: none; + cursor: not-allowed; + background-color: transparent; + background-image: none; + filter: progid:DXImageTransform.Microsoft.gradient(enabled = false); +} +.open > .dropdown-menu { + display: block; +} +.open > a { + outline: 0; +} +.dropdown-menu-right { + right: 0; + left: auto; +} +.dropdown-menu-left { + right: auto; + left: 0; +} +.dropdown-header { + display: block; + padding: 3px 20px; + font-size: 12px; + line-height: 1.42857143; + color: #777; + white-space: nowrap; +} +.dropdown-backdrop { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 990; +} +.pull-right > .dropdown-menu { + right: 0; + left: auto; +} +.dropup .caret, +.navbar-fixed-bottom .dropdown .caret { + content: ""; + border-top: 0; + border-bottom: 4px solid; +} +.dropup .dropdown-menu, +.navbar-fixed-bottom .dropdown .dropdown-menu { + top: auto; + bottom: 100%; + margin-bottom: 1px; +} +@media (min-width: 768px) { + .navbar-right .dropdown-menu { + right: 0; + left: auto; + } + .navbar-right .dropdown-menu-left { + right: auto; + left: 0; + } +} +.btn-group, +.btn-group-vertical { + position: relative; + display: inline-block; + vertical-align: middle; +} +.btn-group > .btn, +.btn-group-vertical > .btn { + position: relative; + float: left; +} +.btn-group > .btn:hover, +.btn-group-vertical > .btn:hover, +.btn-group > .btn:focus, +.btn-group-vertical > .btn:focus, +.btn-group > .btn:active, +.btn-group-vertical > .btn:active, +.btn-group > .btn.active, +.btn-group-vertical > .btn.active { + z-index: 2; +} +.btn-group .btn + .btn, +.btn-group .btn + .btn-group, +.btn-group .btn-group + .btn, +.btn-group .btn-group + .btn-group { + margin-left: -1px; +} +.btn-toolbar { + margin-left: -5px; +} +.btn-toolbar .btn-group, +.btn-toolbar .input-group { + float: left; +} +.btn-toolbar > .btn, +.btn-toolbar > .btn-group, +.btn-toolbar > .input-group { + margin-left: 5px; +} +.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) { + border-radius: 0; +} +.btn-group > .btn:first-child { + margin-left: 0; +} +.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn:last-child:not(:first-child), +.btn-group > .dropdown-toggle:not(:first-child) { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group > .btn-group { + float: left; +} +.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group > .btn-group:first-child > .btn:last-child, +.btn-group > .btn-group:first-child > .dropdown-toggle { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.btn-group > .btn-group:last-child > .btn:first-child { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group .dropdown-toggle:active, +.btn-group.open .dropdown-toggle { + outline: 0; +} +.btn-group > .btn + .dropdown-toggle { + padding-right: 8px; + padding-left: 8px; +} +.btn-group > .btn-lg + .dropdown-toggle { + padding-right: 12px; + padding-left: 12px; +} +.btn-group.open .dropdown-toggle { + -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); + box-shadow: inset 0 3px 5px rgba(0, 0, 0, .125); +} +.btn-group.open .dropdown-toggle.btn-link { + -webkit-box-shadow: none; + box-shadow: none; +} +.btn .caret { + margin-left: 0; +} +.btn-lg .caret { + border-width: 5px 5px 0; + border-bottom-width: 0; +} +.dropup .btn-lg .caret { + border-width: 0 5px 5px; +} +.btn-group-vertical > .btn, +.btn-group-vertical > .btn-group, +.btn-group-vertical > .btn-group > .btn { + display: block; + float: none; + width: 100%; + max-width: 100%; +} +.btn-group-vertical > .btn-group > .btn { + float: none; +} +.btn-group-vertical > .btn + .btn, +.btn-group-vertical > .btn + .btn-group, +.btn-group-vertical > .btn-group + .btn, +.btn-group-vertical > .btn-group + .btn-group { + margin-top: -1px; + margin-left: 0; +} +.btn-group-vertical > .btn:not(:first-child):not(:last-child) { + border-radius: 0; +} +.btn-group-vertical > .btn:first-child:not(:last-child) { + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn:last-child:not(:first-child) { + border-top-left-radius: 0; + border-top-right-radius: 0; + border-bottom-left-radius: 4px; +} +.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn { + border-radius: 0; +} +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child, +.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle { + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child { + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.btn-group-justified { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: separate; +} +.btn-group-justified > .btn, +.btn-group-justified > .btn-group { + display: table-cell; + float: none; + width: 1%; +} +.btn-group-justified > .btn-group .btn { + width: 100%; +} +.btn-group-justified > .btn-group .dropdown-menu { + left: auto; +} +[data-toggle="buttons"] > .btn input[type="radio"], +[data-toggle="buttons"] > .btn-group > .btn input[type="radio"], +[data-toggle="buttons"] > .btn input[type="checkbox"], +[data-toggle="buttons"] > .btn-group > .btn input[type="checkbox"] { + position: absolute; + clip: rect(0, 0, 0, 0); + pointer-events: none; +} +.input-group { + position: relative; + display: table; + border-collapse: separate; +} +.input-group[class*="col-"] { + float: none; + padding-right: 0; + padding-left: 0; +} +.input-group .form-control { + position: relative; + z-index: 2; + float: left; + width: 100%; + margin-bottom: 0; +} +.input-group-lg > .form-control, +.input-group-lg > .input-group-addon, +.input-group-lg > .input-group-btn > .btn { + height: 46px; + padding: 10px 16px; + font-size: 18px; + line-height: 1.33; + border-radius: 6px; +} +select.input-group-lg > .form-control, +select.input-group-lg > .input-group-addon, +select.input-group-lg > .input-group-btn > .btn { + height: 46px; + line-height: 46px; +} +textarea.input-group-lg > .form-control, +textarea.input-group-lg > .input-group-addon, +textarea.input-group-lg > .input-group-btn > .btn, +select[multiple].input-group-lg > .form-control, +select[multiple].input-group-lg > .input-group-addon, +select[multiple].input-group-lg > .input-group-btn > .btn { + height: auto; +} +.input-group-sm > .form-control, +.input-group-sm > .input-group-addon, +.input-group-sm > .input-group-btn > .btn { + height: 30px; + padding: 5px 10px; + font-size: 12px; + line-height: 1.5; + border-radius: 3px; +} +select.input-group-sm > .form-control, +select.input-group-sm > .input-group-addon, +select.input-group-sm > .input-group-btn > .btn { + height: 30px; + line-height: 30px; +} +textarea.input-group-sm > .form-control, +textarea.input-group-sm > .input-group-addon, +textarea.input-group-sm > .input-group-btn > .btn, +select[multiple].input-group-sm > .form-control, +select[multiple].input-group-sm > .input-group-addon, +select[multiple].input-group-sm > .input-group-btn > .btn { + height: auto; +} +.input-group-addon, +.input-group-btn, +.input-group .form-control { + display: table-cell; +} +.input-group-addon:not(:first-child):not(:last-child), +.input-group-btn:not(:first-child):not(:last-child), +.input-group .form-control:not(:first-child):not(:last-child) { + border-radius: 0; +} +.input-group-addon, +.input-group-btn { + width: 1%; + white-space: nowrap; + vertical-align: middle; +} +.input-group-addon { + padding: 6px 12px; + font-size: 14px; + font-weight: normal; + line-height: 1; + color: #555; + text-align: center; + background-color: #eee; + border: 1px solid #ccc; + border-radius: 4px; +} +.input-group-addon.input-sm { + padding: 5px 10px; + font-size: 12px; + border-radius: 3px; +} +.input-group-addon.input-lg { + padding: 10px 16px; + font-size: 18px; + border-radius: 6px; +} +.input-group-addon input[type="radio"], +.input-group-addon input[type="checkbox"] { + margin-top: 0; +} +.input-group .form-control:first-child, +.input-group-addon:first-child, +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group > .btn, +.input-group-btn:first-child > .dropdown-toggle, +.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle), +.input-group-btn:last-child > .btn-group:not(:last-child) > .btn { + border-top-right-radius: 0; + border-bottom-right-radius: 0; +} +.input-group-addon:first-child { + border-right: 0; +} +.input-group .form-control:last-child, +.input-group-addon:last-child, +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group > .btn, +.input-group-btn:last-child > .dropdown-toggle, +.input-group-btn:first-child > .btn:not(:first-child), +.input-group-btn:first-child > .btn-group:not(:first-child) > .btn { + border-top-left-radius: 0; + border-bottom-left-radius: 0; +} +.input-group-addon:last-child { + border-left: 0; +} +.input-group-btn { + position: relative; + font-size: 0; + white-space: nowrap; +} +.input-group-btn > .btn { + position: relative; +} +.input-group-btn > .btn + .btn { + margin-left: -1px; +} +.input-group-btn > .btn:hover, +.input-group-btn > .btn:focus, +.input-group-btn > .btn:active { + z-index: 2; +} +.input-group-btn:first-child > .btn, +.input-group-btn:first-child > .btn-group { + margin-right: -1px; +} +.input-group-btn:last-child > .btn, +.input-group-btn:last-child > .btn-group { + margin-left: -1px; +} +.nav { + padding-left: 0; + margin-bottom: 0; + list-style: none; +} +.nav > li { + position: relative; + display: block; +} +.nav > li > a { + position: relative; + display: block; + padding: 10px 15px; +} +.nav > li > a:hover, +.nav > li > a:focus { + text-decoration: none; + background-color: #eee; +} +.nav > li.disabled > a { + color: #777; +} +.nav > li.disabled > a:hover, +.nav > li.disabled > a:focus { + color: #777; + text-decoration: none; + cursor: not-allowed; + background-color: transparent; +} +.nav .open > a, +.nav .open > a:hover, +.nav .open > a:focus { + background-color: #eee; + border-color: #337ab7; +} +.nav .nav-divider { + height: 1px; + margin: 9px 0; + overflow: hidden; + background-color: #e5e5e5; +} +.nav > li > a > img { + max-width: none; +} +.nav-tabs { + border-bottom: 1px solid #ddd; +} +.nav-tabs > li { + float: left; + margin-bottom: -1px; +} +.nav-tabs > li > a { + margin-right: 2px; + line-height: 1.42857143; + border: 1px solid transparent; + border-radius: 4px 4px 0 0; +} +.nav-tabs > li > a:hover { + border-color: #eee #eee #ddd; +} +.nav-tabs > li.active > a, +.nav-tabs > li.active > a:hover, +.nav-tabs > li.active > a:focus { + color: #555; + cursor: default; + background-color: #fff; + border: 1px solid #ddd; + border-bottom-color: transparent; +} +.nav-tabs.nav-justified { + width: 100%; + border-bottom: 0; +} +.nav-tabs.nav-justified > li { + float: none; +} +.nav-tabs.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} +.nav-tabs.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-tabs.nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs.nav-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs.nav-justified > .active > a, +.nav-tabs.nav-justified > .active > a:hover, +.nav-tabs.nav-justified > .active > a:focus { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs.nav-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs.nav-justified > .active > a, + .nav-tabs.nav-justified > .active > a:hover, + .nav-tabs.nav-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.nav-pills > li { + float: left; +} +.nav-pills > li > a { + border-radius: 4px; +} +.nav-pills > li + li { + margin-left: 2px; +} +.nav-pills > li.active > a, +.nav-pills > li.active > a:hover, +.nav-pills > li.active > a:focus { + color: #fff; + background-color: #337ab7; +} +.nav-stacked > li { + float: none; +} +.nav-stacked > li + li { + margin-top: 2px; + margin-left: 0; +} +.nav-justified { + width: 100%; +} +.nav-justified > li { + float: none; +} +.nav-justified > li > a { + margin-bottom: 5px; + text-align: center; +} +.nav-justified > .dropdown .dropdown-menu { + top: auto; + left: auto; +} +@media (min-width: 768px) { + .nav-justified > li { + display: table-cell; + width: 1%; + } + .nav-justified > li > a { + margin-bottom: 0; + } +} +.nav-tabs-justified { + border-bottom: 0; +} +.nav-tabs-justified > li > a { + margin-right: 0; + border-radius: 4px; +} +.nav-tabs-justified > .active > a, +.nav-tabs-justified > .active > a:hover, +.nav-tabs-justified > .active > a:focus { + border: 1px solid #ddd; +} +@media (min-width: 768px) { + .nav-tabs-justified > li > a { + border-bottom: 1px solid #ddd; + border-radius: 4px 4px 0 0; + } + .nav-tabs-justified > .active > a, + .nav-tabs-justified > .active > a:hover, + .nav-tabs-justified > .active > a:focus { + border-bottom-color: #fff; + } +} +.tab-content > .tab-pane { + display: none; + visibility: hidden; +} +.tab-content > .active { + display: block; + visibility: visible; +} +.nav-tabs .dropdown-menu { + margin-top: -1px; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.navbar { + position: relative; + min-height: 50px; + margin-bottom: 20px; + border: 1px solid transparent; +} +@media (min-width: 768px) { + .navbar { + border-radius: 4px; + } +} +@media (min-width: 768px) { + .navbar-header { + float: left; + } +} +.navbar-collapse { + padding-right: 15px; + padding-left: 15px; + overflow-x: visible; + -webkit-overflow-scrolling: touch; + border-top: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1); +} +.navbar-collapse.in { + overflow-y: auto; +} +@media (min-width: 768px) { + .navbar-collapse { + width: auto; + border-top: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-collapse.collapse { + display: block !important; + height: auto !important; + padding-bottom: 0; + overflow: visible !important; + visibility: visible !important; + } + .navbar-collapse.in { + overflow-y: visible; + } + .navbar-fixed-top .navbar-collapse, + .navbar-static-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + padding-right: 0; + padding-left: 0; + } +} +.navbar-fixed-top .navbar-collapse, +.navbar-fixed-bottom .navbar-collapse { + max-height: 340px; +} +@media (max-device-width: 480px) and (orientation: landscape) { + .navbar-fixed-top .navbar-collapse, + .navbar-fixed-bottom .navbar-collapse { + max-height: 200px; + } +} +.container > .navbar-header, +.container-fluid > .navbar-header, +.container > .navbar-collapse, +.container-fluid > .navbar-collapse { + margin-right: -15px; + margin-left: -15px; +} +@media (min-width: 768px) { + .container > .navbar-header, + .container-fluid > .navbar-header, + .container > .navbar-collapse, + .container-fluid > .navbar-collapse { + margin-right: 0; + margin-left: 0; + } +} +.navbar-static-top { + z-index: 1000; + border-width: 0 0 1px; +} +@media (min-width: 768px) { + .navbar-static-top { + border-radius: 0; + } +} +.navbar-fixed-top, +.navbar-fixed-bottom { + position: fixed; + right: 0; + left: 0; + z-index: 1030; +} +@media (min-width: 768px) { + .navbar-fixed-top, + .navbar-fixed-bottom { + border-radius: 0; + } +} +.navbar-fixed-top { + top: 0; + border-width: 0 0 1px; +} +.navbar-fixed-bottom { + bottom: 0; + margin-bottom: 0; + border-width: 1px 0 0; +} +.navbar-brand { + float: left; + height: 50px; + padding: 15px 15px; + font-size: 18px; + line-height: 20px; +} +.navbar-brand:hover, +.navbar-brand:focus { + text-decoration: none; +} +.navbar-brand > img { + display: block; +} +@media (min-width: 768px) { + .navbar > .container .navbar-brand, + .navbar > .container-fluid .navbar-brand { + margin-left: -15px; + } +} +.navbar-toggle { + position: relative; + float: right; + padding: 9px 10px; + margin-top: 8px; + margin-right: 15px; + margin-bottom: 8px; + background-color: transparent; + background-image: none; + border: 1px solid transparent; + border-radius: 4px; +} +.navbar-toggle:focus { + outline: 0; +} +.navbar-toggle .icon-bar { + display: block; + width: 22px; + height: 2px; + border-radius: 1px; +} +.navbar-toggle .icon-bar + .icon-bar { + margin-top: 4px; +} +@media (min-width: 768px) { + .navbar-toggle { + display: none; + } +} +.navbar-nav { + margin: 7.5px -15px; +} +.navbar-nav > li > a { + padding-top: 10px; + padding-bottom: 10px; + line-height: 20px; +} +@media (max-width: 767px) { + .navbar-nav .open .dropdown-menu { + position: static; + float: none; + width: auto; + margin-top: 0; + background-color: transparent; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } + .navbar-nav .open .dropdown-menu > li > a, + .navbar-nav .open .dropdown-menu .dropdown-header { + padding: 5px 15px 5px 25px; + } + .navbar-nav .open .dropdown-menu > li > a { + line-height: 20px; + } + .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-nav .open .dropdown-menu > li > a:focus { + background-image: none; + } +} +@media (min-width: 768px) { + .navbar-nav { + float: left; + margin: 0; + } + .navbar-nav > li { + float: left; + } + .navbar-nav > li > a { + padding-top: 15px; + padding-bottom: 15px; + } +} +.navbar-form { + padding: 10px 15px; + margin-top: 8px; + margin-right: -15px; + margin-bottom: 8px; + margin-left: -15px; + border-top: 1px solid transparent; + border-bottom: 1px solid transparent; + -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, .1), 0 1px 0 rgba(255, 255, 255, .1); +} +@media (min-width: 768px) { + .navbar-form .form-group { + display: inline-block; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .form-control { + display: inline-block; + width: auto; + vertical-align: middle; + } + .navbar-form .form-control-static { + display: inline-block; + } + .navbar-form .input-group { + display: inline-table; + vertical-align: middle; + } + .navbar-form .input-group .input-group-addon, + .navbar-form .input-group .input-group-btn, + .navbar-form .input-group .form-control { + width: auto; + } + .navbar-form .input-group > .form-control { + width: 100%; + } + .navbar-form .control-label { + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio, + .navbar-form .checkbox { + display: inline-block; + margin-top: 0; + margin-bottom: 0; + vertical-align: middle; + } + .navbar-form .radio label, + .navbar-form .checkbox label { + padding-left: 0; + } + .navbar-form .radio input[type="radio"], + .navbar-form .checkbox input[type="checkbox"] { + position: relative; + margin-left: 0; + } + .navbar-form .has-feedback .form-control-feedback { + top: 0; + } +} +@media (max-width: 767px) { + .navbar-form .form-group { + margin-bottom: 5px; + } + .navbar-form .form-group:last-child { + margin-bottom: 0; + } +} +@media (min-width: 768px) { + .navbar-form { + width: auto; + padding-top: 0; + padding-bottom: 0; + margin-right: 0; + margin-left: 0; + border: 0; + -webkit-box-shadow: none; + box-shadow: none; + } +} +.navbar-nav > li > .dropdown-menu { + margin-top: 0; + border-top-left-radius: 0; + border-top-right-radius: 0; +} +.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu { + border-top-left-radius: 4px; + border-top-right-radius: 4px; + border-bottom-right-radius: 0; + border-bottom-left-radius: 0; +} +.navbar-btn { + margin-top: 8px; + margin-bottom: 8px; +} +.navbar-btn.btn-sm { + margin-top: 10px; + margin-bottom: 10px; +} +.navbar-btn.btn-xs { + margin-top: 14px; + margin-bottom: 14px; +} +.navbar-text { + margin-top: 15px; + margin-bottom: 15px; +} +@media (min-width: 768px) { + .navbar-text { + float: left; + margin-right: 15px; + margin-left: 15px; + } +} +@media (min-width: 768px) { + .navbar-left { + float: left !important; + } + .navbar-right { + float: right !important; + margin-right: -15px; + } + .navbar-right ~ .navbar-right { + margin-right: 0; + } +} +.navbar-default { + background-color: #f8f8f8; + border-color: #e7e7e7; +} +.navbar-default .navbar-brand { + color: #777; +} +.navbar-default .navbar-brand:hover, +.navbar-default .navbar-brand:focus { + color: #5e5e5e; + background-color: transparent; +} +.navbar-default .navbar-text { + color: #777; +} +.navbar-default .navbar-nav > li > a { + color: #777; +} +.navbar-default .navbar-nav > li > a:hover, +.navbar-default .navbar-nav > li > a:focus { + color: #333; + background-color: transparent; +} +.navbar-default .navbar-nav > .active > a, +.navbar-default .navbar-nav > .active > a:hover, +.navbar-default .navbar-nav > .active > a:focus { + color: #555; + background-color: #e7e7e7; +} +.navbar-default .navbar-nav > .disabled > a, +.navbar-default .navbar-nav > .disabled > a:hover, +.navbar-default .navbar-nav > .disabled > a:focus { + color: #ccc; + background-color: transparent; +} +.navbar-default .navbar-toggle { + border-color: #ddd; +} +.navbar-default .navbar-toggle:hover, +.navbar-default .navbar-toggle:focus { + background-color: #ddd; +} +.navbar-default .navbar-toggle .icon-bar { + background-color: #888; +} +.navbar-default .navbar-collapse, +.navbar-default .navbar-form { + border-color: #e7e7e7; +} +.navbar-default .navbar-nav > .open > a, +.navbar-default .navbar-nav > .open > a:hover, +.navbar-default .navbar-nav > .open > a:focus { + color: #555; + background-color: #e7e7e7; +} +@media (max-width: 767px) { + .navbar-default .navbar-nav .open .dropdown-menu > li > a { + color: #777; + } + .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus { + color: #333; + background-color: transparent; + } + .navbar-default .navbar-nav .open .dropdown-menu > .active > a, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #555; + background-color: #e7e7e7; + } + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #ccc; + background-color: transparent; + } +} +.navbar-default .navbar-link { + color: #777; +} +.navbar-default .navbar-link:hover { + color: #333; +} +.navbar-default .btn-link { + color: #777; +} +.navbar-default .btn-link:hover, +.navbar-default .btn-link:focus { + color: #333; +} +.navbar-default .btn-link[disabled]:hover, +fieldset[disabled] .navbar-default .btn-link:hover, +.navbar-default .btn-link[disabled]:focus, +fieldset[disabled] .navbar-default .btn-link:focus { + color: #ccc; +} +.navbar-inverse { + background-color: #222; + border-color: #080808; +} +.navbar-inverse .navbar-brand { + color: #9d9d9d; +} +.navbar-inverse .navbar-brand:hover, +.navbar-inverse .navbar-brand:focus { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-text { + color: #9d9d9d; +} +.navbar-inverse .navbar-nav > li > a { + color: #9d9d9d; +} +.navbar-inverse .navbar-nav > li > a:hover, +.navbar-inverse .navbar-nav > li > a:focus { + color: #fff; + background-color: transparent; +} +.navbar-inverse .navbar-nav > .active > a, +.navbar-inverse .navbar-nav > .active > a:hover, +.navbar-inverse .navbar-nav > .active > a:focus { + color: #fff; + background-color: #080808; +} +.navbar-inverse .navbar-nav > .disabled > a, +.navbar-inverse .navbar-nav > .disabled > a:hover, +.navbar-inverse .navbar-nav > .disabled > a:focus { + color: #444; + background-color: transparent; +} +.navbar-inverse .navbar-toggle { + border-color: #333; +} +.navbar-inverse .navbar-toggle:hover, +.navbar-inverse .navbar-toggle:focus { + background-color: #333; +} +.navbar-inverse .navbar-toggle .icon-bar { + background-color: #fff; +} +.navbar-inverse .navbar-collapse, +.navbar-inverse .navbar-form { + border-color: #101010; +} +.navbar-inverse .navbar-nav > .open > a, +.navbar-inverse .navbar-nav > .open > a:hover, +.navbar-inverse .navbar-nav > .open > a:focus { + color: #fff; + background-color: #080808; +} +@media (max-width: 767px) { + .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header { + border-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu .divider { + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a { + color: #9d9d9d; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus { + color: #fff; + background-color: transparent; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus { + color: #fff; + background-color: #080808; + } + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover, + .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus { + color: #444; + background-color: transparent; + } +} +.navbar-inverse .navbar-link { + color: #9d9d9d; +} +.navbar-inverse .navbar-link:hover { + color: #fff; +} +.navbar-inverse .btn-link { + color: #9d9d9d; +} +.navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link:focus { + color: #fff; +} +.navbar-inverse .btn-link[disabled]:hover, +fieldset[disabled] .navbar-inverse .btn-link:hover, +.navbar-inverse .btn-link[disabled]:focus, +fieldset[disabled] .navbar-inverse .btn-link:focus { + color: #444; +} +.breadcrumb { + padding: 8px 15px; + margin-bottom: 20px; + list-style: none; + background-color: #f5f5f5; + border-radius: 4px; +} +.breadcrumb > li { + display: inline-block; +} +.breadcrumb > li + li:before { + padding: 0 5px; + color: #ccc; + content: "/\00a0"; +} +.breadcrumb > .active { + color: #777; +} +.pagination { + display: inline-block; + padding-left: 0; + margin: 20px 0; + border-radius: 4px; +} +.pagination > li { + display: inline; +} +.pagination > li > a, +.pagination > li > span { + position: relative; + float: left; + padding: 6px 12px; + margin-left: -1px; + line-height: 1.42857143; + color: #337ab7; + text-decoration: none; + background-color: #fff; + border: 1px solid #ddd; +} +.pagination > li:first-child > a, +.pagination > li:first-child > span { + margin-left: 0; + border-top-left-radius: 4px; + border-bottom-left-radius: 4px; +} +.pagination > li:last-child > a, +.pagination > li:last-child > span { + border-top-right-radius: 4px; + border-bottom-right-radius: 4px; +} +.pagination > li > a:hover, +.pagination > li > span:hover, +.pagination > li > a:focus, +.pagination > li > span:focus { + color: #23527c; + background-color: #eee; + border-color: #ddd; +} +.pagination > .active > a, +.pagination > .active > span, +.pagination > .active > a:hover, +.pagination > .active > span:hover, +.pagination > .active > a:focus, +.pagination > .active > span:focus { + z-index: 2; + color: #fff; + cursor: default; + background-color: #337ab7; + border-color: #337ab7; +} +.pagination > .disabled > span, +.pagination > .disabled > span:hover, +.pagination > .disabled > span:focus, +.pagination > .disabled > a, +.pagination > .disabled > a:hover, +.pagination > .disabled > a:focus { + color: #777; + cursor: not-allowed; + background-color: #fff; + border-color: #ddd; +} +.pagination-lg > li > a, +.pagination-lg > li > span { + padding: 10px 16px; + font-size: 18px; +} +.pagination-lg > li:first-child > a, +.pagination-lg > li:first-child > span { + border-top-left-radius: 6px; + border-bottom-left-radius: 6px; +} +.pagination-lg > li:last-child > a, +.pagination-lg > li:last-child > span { + border-top-right-radius: 6px; + border-bottom-right-radius: 6px; +} +.pagination-sm > li > a, +.pagination-sm > li > span { + padding: 5px 10px; + font-size: 12px; +} +.pagination-sm > li:first-child > a, +.pagination-sm > li:first-child > span { + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; +} +.pagination-sm > li:last-child > a, +.pagination-sm > li:last-child > span { + border-top-right-radius: 3px; + border-bottom-right-radius: 3px; +} +.pager { + padding-left: 0; + margin: 20px 0; + text-align: center; + list-style: none; +} +.pager li { + display: inline; +} +.pager li > a, +.pager li > span { + display: inline-block; + padding: 5px 14px; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 15px; +} +.pager li > a:hover, +.pager li > a:focus { + text-decoration: none; + background-color: #eee; +} +.pager .next > a, +.pager .next > span { + float: right; +} +.pager .previous > a, +.pager .previous > span { + float: left; +} +.pager .disabled > a, +.pager .disabled > a:hover, +.pager .disabled > a:focus, +.pager .disabled > span { + color: #777; + cursor: not-allowed; + background-color: #fff; +} +.label { + display: inline; + padding: .2em .6em .3em; + font-size: 75%; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + border-radius: .25em; +} +a.label:hover, +a.label:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} +.label:empty { + display: none; +} +.btn .label { + position: relative; + top: -1px; +} +.label-default { + background-color: #777; +} +.label-default[href]:hover, +.label-default[href]:focus { + background-color: #5e5e5e; +} +.label-primary { + background-color: #337ab7; +} +.label-primary[href]:hover, +.label-primary[href]:focus { + background-color: #286090; +} +.label-success { + background-color: #5cb85c; +} +.label-success[href]:hover, +.label-success[href]:focus { + background-color: #449d44; +} +.label-info { + background-color: #5bc0de; +} +.label-info[href]:hover, +.label-info[href]:focus { + background-color: #31b0d5; +} +.label-warning { + background-color: #f0ad4e; +} +.label-warning[href]:hover, +.label-warning[href]:focus { + background-color: #ec971f; +} +.label-danger { + background-color: #d9534f; +} +.label-danger[href]:hover, +.label-danger[href]:focus { + background-color: #c9302c; +} +.badge { + display: inline-block; + min-width: 10px; + padding: 3px 7px; + font-size: 12px; + font-weight: bold; + line-height: 1; + color: #fff; + text-align: center; + white-space: nowrap; + vertical-align: baseline; + background-color: #777; + border-radius: 10px; +} +.badge:empty { + display: none; +} +.btn .badge { + position: relative; + top: -1px; +} +.btn-xs .badge { + top: 0; + padding: 1px 5px; +} +a.badge:hover, +a.badge:focus { + color: #fff; + text-decoration: none; + cursor: pointer; +} +.list-group-item.active > .badge, +.nav-pills > .active > a > .badge { + color: #337ab7; + background-color: #fff; +} +.list-group-item > .badge { + float: right; +} +.list-group-item > .badge + .badge { + margin-right: 5px; +} +.nav-pills > li > a > .badge { + margin-left: 3px; +} +.jumbotron { + padding: 30px 15px; + margin-bottom: 30px; + color: inherit; + background-color: #eee; +} +.jumbotron h1, +.jumbotron .h1 { + color: inherit; +} +.jumbotron p { + margin-bottom: 15px; + font-size: 21px; + font-weight: 200; +} +.jumbotron > hr { + border-top-color: #d5d5d5; +} +.container .jumbotron, +.container-fluid .jumbotron { + border-radius: 6px; +} +.jumbotron .container { + max-width: 100%; +} +@media screen and (min-width: 768px) { + .jumbotron { + padding: 48px 0; + } + .container .jumbotron, + .container-fluid .jumbotron { + padding-right: 60px; + padding-left: 60px; + } + .jumbotron h1, + .jumbotron .h1 { + font-size: 63px; + } +} +.thumbnail { + display: block; + padding: 4px; + margin-bottom: 20px; + line-height: 1.42857143; + background-color: #fff; + border: 1px solid #ddd; + border-radius: 4px; + -webkit-transition: border .2s ease-in-out; + -o-transition: border .2s ease-in-out; + transition: border .2s ease-in-out; +} +.thumbnail > img, +.thumbnail a > img { + margin-right: auto; + margin-left: auto; +} +a.thumbnail:hover, +a.thumbnail:focus, +a.thumbnail.active { + border-color: #337ab7; +} +.thumbnail .caption { + padding: 9px; + color: #333; +} +.alert { + padding: 15px; + margin-bottom: 20px; + border: 1px solid transparent; + border-radius: 4px; +} +.alert h4 { + margin-top: 0; + color: inherit; +} +.alert .alert-link { + font-weight: bold; +} +.alert > p, +.alert > ul { + margin-bottom: 0; +} +.alert > p + p { + margin-top: 5px; +} +.alert-dismissable, +.alert-dismissible { + padding-right: 35px; +} +.alert-dismissable .close, +.alert-dismissible .close { + position: relative; + top: -2px; + right: -21px; + color: inherit; +} +.alert-success { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} +.alert-success hr { + border-top-color: #c9e2b3; +} +.alert-success .alert-link { + color: #2b542c; +} +.alert-info { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} +.alert-info hr { + border-top-color: #a6e1ec; +} +.alert-info .alert-link { + color: #245269; +} +.alert-warning { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} +.alert-warning hr { + border-top-color: #f7e1b5; +} +.alert-warning .alert-link { + color: #66512c; +} +.alert-danger { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} +.alert-danger hr { + border-top-color: #e4b9c0; +} +.alert-danger .alert-link { + color: #843534; +} +@-webkit-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@-o-keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +@keyframes progress-bar-stripes { + from { + background-position: 40px 0; + } + to { + background-position: 0 0; + } +} +.progress { + height: 20px; + margin-bottom: 20px; + overflow: hidden; + background-color: #f5f5f5; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); + box-shadow: inset 0 1px 2px rgba(0, 0, 0, .1); +} +.progress-bar { + float: left; + width: 0; + height: 100%; + font-size: 12px; + line-height: 20px; + color: #fff; + text-align: center; + background-color: #337ab7; + -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); + box-shadow: inset 0 -1px 0 rgba(0, 0, 0, .15); + -webkit-transition: width .6s ease; + -o-transition: width .6s ease; + transition: width .6s ease; +} +.progress-striped .progress-bar, +.progress-bar-striped { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + background-size: 40px 40px; +} +.progress.active .progress-bar, +.progress-bar.active { + -webkit-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; +} +.progress-bar-success { + background-color: #5cb85c; +} +.progress-striped .progress-bar-success { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-info { + background-color: #5bc0de; +} +.progress-striped .progress-bar-info { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-warning { + background-color: #f0ad4e; +} +.progress-striped .progress-bar-warning { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.progress-bar-danger { + background-color: #d9534f; +} +.progress-striped .progress-bar-danger { + background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(255, 255, 255, .15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, .15) 50%, rgba(255, 255, 255, .15) 75%, transparent 75%, transparent); +} +.media { + margin-top: 15px; +} +.media:first-child { + margin-top: 0; +} +.media-right, +.media > .pull-right { + padding-left: 10px; +} +.media-left, +.media > .pull-left { + padding-right: 10px; +} +.media-left, +.media-right, +.media-body { + display: table-cell; + vertical-align: top; +} +.media-middle { + vertical-align: middle; +} +.media-bottom { + vertical-align: bottom; +} +.media-heading { + margin-top: 0; + margin-bottom: 5px; +} +.media-list { + padding-left: 0; + list-style: none; +} +.list-group { + padding-left: 0; + margin-bottom: 20px; +} +.list-group-item { + position: relative; + display: block; + padding: 10px 15px; + margin-bottom: -1px; + background-color: #fff; + border: 1px solid #ddd; +} +.list-group-item:first-child { + border-top-left-radius: 4px; + border-top-right-radius: 4px; +} +.list-group-item:last-child { + margin-bottom: 0; + border-bottom-right-radius: 4px; + border-bottom-left-radius: 4px; +} +a.list-group-item { + color: #555; +} +a.list-group-item .list-group-item-heading { + color: #333; +} +a.list-group-item:hover, +a.list-group-item:focus { + color: #555; + text-decoration: none; + background-color: #f5f5f5; +} +.list-group-item.disabled, +.list-group-item.disabled:hover, +.list-group-item.disabled:focus { + color: #777; + cursor: not-allowed; + background-color: #eee; +} +.list-group-item.disabled .list-group-item-heading, +.list-group-item.disabled:hover .list-group-item-heading, +.list-group-item.disabled:focus .list-group-item-heading { + color: inherit; +} +.list-group-item.disabled .list-group-item-text, +.list-group-item.disabled:hover .list-group-item-text, +.list-group-item.disabled:focus .list-group-item-text { + color: #777; +} +.list-group-item.active, +.list-group-item.active:hover, +.list-group-item.active:focus { + z-index: 2; + color: #fff; + background-color: #337ab7; + border-color: #337ab7; +} +.list-group-item.active .list-group-item-heading, +.list-group-item.active:hover .list-group-item-heading, +.list-group-item.active:focus .list-group-item-heading, +.list-group-item.active .list-group-item-heading > small, +.list-group-item.active:hover .list-group-item-heading > small, +.list-group-item.active:focus .list-group-item-heading > small, +.list-group-item.active .list-group-item-heading > .small, +.list-group-item.active:hover .list-group-item-heading > .small, +.list-group-item.active:focus .list-group-item-heading > .small { + color: inherit; +} +.list-group-item.active .list-group-item-text, +.list-group-item.active:hover .list-group-item-text, +.list-group-item.active:focus .list-group-item-text { + color: #c7ddef; +} +.list-group-item-success { + color: #3c763d; + background-color: #dff0d8; +} +a.list-group-item-success { + color: #3c763d; +} +a.list-group-item-success .list-group-item-heading { + color: inherit; +} +a.list-group-item-success:hover, +a.list-group-item-success:focus { + color: #3c763d; + background-color: #d0e9c6; +} +a.list-group-item-success.active, +a.list-group-item-success.active:hover, +a.list-group-item-success.active:focus { + color: #fff; + background-color: #3c763d; + border-color: #3c763d; +} +.list-group-item-info { + color: #31708f; + background-color: #d9edf7; +} +a.list-group-item-info { + color: #31708f; +} +a.list-group-item-info .list-group-item-heading { + color: inherit; +} +a.list-group-item-info:hover, +a.list-group-item-info:focus { + color: #31708f; + background-color: #c4e3f3; +} +a.list-group-item-info.active, +a.list-group-item-info.active:hover, +a.list-group-item-info.active:focus { + color: #fff; + background-color: #31708f; + border-color: #31708f; +} +.list-group-item-warning { + color: #8a6d3b; + background-color: #fcf8e3; +} +a.list-group-item-warning { + color: #8a6d3b; +} +a.list-group-item-warning .list-group-item-heading { + color: inherit; +} +a.list-group-item-warning:hover, +a.list-group-item-warning:focus { + color: #8a6d3b; + background-color: #faf2cc; +} +a.list-group-item-warning.active, +a.list-group-item-warning.active:hover, +a.list-group-item-warning.active:focus { + color: #fff; + background-color: #8a6d3b; + border-color: #8a6d3b; +} +.list-group-item-danger { + color: #a94442; + background-color: #f2dede; +} +a.list-group-item-danger { + color: #a94442; +} +a.list-group-item-danger .list-group-item-heading { + color: inherit; +} +a.list-group-item-danger:hover, +a.list-group-item-danger:focus { + color: #a94442; + background-color: #ebcccc; +} +a.list-group-item-danger.active, +a.list-group-item-danger.active:hover, +a.list-group-item-danger.active:focus { + color: #fff; + background-color: #a94442; + border-color: #a94442; +} +.list-group-item-heading { + margin-top: 0; + margin-bottom: 5px; +} +.list-group-item-text { + margin-bottom: 0; + line-height: 1.3; +} +.panel { + margin-bottom: 20px; + background-color: #fff; + border: 1px solid transparent; + border-radius: 4px; + -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, .05); + box-shadow: 0 1px 1px rgba(0, 0, 0, .05); +} +.panel-body { + padding: 15px; +} +.panel-heading { + padding: 10px 15px; + border-bottom: 1px solid transparent; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel-heading > .dropdown .dropdown-toggle { + color: inherit; +} +.panel-title { + margin-top: 0; + margin-bottom: 0; + font-size: 16px; + color: inherit; +} +.panel-title > a { + color: inherit; +} +.panel-footer { + padding: 10px 15px; + background-color: #f5f5f5; + border-top: 1px solid #ddd; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .list-group, +.panel > .panel-collapse > .list-group { + margin-bottom: 0; +} +.panel > .list-group .list-group-item, +.panel > .panel-collapse > .list-group .list-group-item { + border-width: 1px 0; + border-radius: 0; +} +.panel > .list-group:first-child .list-group-item:first-child, +.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child { + border-top: 0; + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .list-group:last-child .list-group-item:last-child, +.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child { + border-bottom: 0; + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel-heading + .list-group .list-group-item:first-child { + border-top-width: 0; +} +.list-group + .panel-footer { + border-top-width: 0; +} +.panel > .table, +.panel > .table-responsive > .table, +.panel > .panel-collapse > .table { + margin-bottom: 0; +} +.panel > .table caption, +.panel > .table-responsive > .table caption, +.panel > .panel-collapse > .table caption { + padding-right: 15px; + padding-left: 15px; +} +.panel > .table:first-child, +.panel > .table-responsive:first-child > .table:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child { + border-top-left-radius: 3px; + border-top-right-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child { + border-top-left-radius: 3px; +} +.panel > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child, +.panel > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child, +.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child, +.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child { + border-top-right-radius: 3px; +} +.panel > .table:last-child, +.panel > .table-responsive:last-child > .table:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child { + border-bottom-right-radius: 3px; + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child { + border-bottom-left-radius: 3px; +} +.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child, +.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child, +.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child, +.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child { + border-bottom-right-radius: 3px; +} +.panel > .panel-body + .table, +.panel > .panel-body + .table-responsive, +.panel > .table + .panel-body, +.panel > .table-responsive + .panel-body { + border-top: 1px solid #ddd; +} +.panel > .table > tbody:first-child > tr:first-child th, +.panel > .table > tbody:first-child > tr:first-child td { + border-top: 0; +} +.panel > .table-bordered, +.panel > .table-responsive > .table-bordered { + border: 0; +} +.panel > .table-bordered > thead > tr > th:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:first-child, +.panel > .table-bordered > tbody > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child, +.panel > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child, +.panel > .table-bordered > thead > tr > td:first-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:first-child, +.panel > .table-bordered > tbody > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child, +.panel > .table-bordered > tfoot > tr > td:first-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child { + border-left: 0; +} +.panel > .table-bordered > thead > tr > th:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > th:last-child, +.panel > .table-bordered > tbody > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child, +.panel > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child, +.panel > .table-bordered > thead > tr > td:last-child, +.panel > .table-responsive > .table-bordered > thead > tr > td:last-child, +.panel > .table-bordered > tbody > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child, +.panel > .table-bordered > tfoot > tr > td:last-child, +.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child { + border-right: 0; +} +.panel > .table-bordered > thead > tr:first-child > td, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > td, +.panel > .table-bordered > tbody > tr:first-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td, +.panel > .table-bordered > thead > tr:first-child > th, +.panel > .table-responsive > .table-bordered > thead > tr:first-child > th, +.panel > .table-bordered > tbody > tr:first-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th { + border-bottom: 0; +} +.panel > .table-bordered > tbody > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td, +.panel > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td, +.panel > .table-bordered > tbody > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th, +.panel > .table-bordered > tfoot > tr:last-child > th, +.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th { + border-bottom: 0; +} +.panel > .table-responsive { + margin-bottom: 0; + border: 0; +} +.panel-group { + margin-bottom: 20px; +} +.panel-group .panel { + margin-bottom: 0; + border-radius: 4px; +} +.panel-group .panel + .panel { + margin-top: 5px; +} +.panel-group .panel-heading { + border-bottom: 0; +} +.panel-group .panel-heading + .panel-collapse > .panel-body, +.panel-group .panel-heading + .panel-collapse > .list-group { + border-top: 1px solid #ddd; +} +.panel-group .panel-footer { + border-top: 0; +} +.panel-group .panel-footer + .panel-collapse .panel-body { + border-bottom: 1px solid #ddd; +} +.panel-default { + border-color: #ddd; +} +.panel-default > .panel-heading { + color: #333; + background-color: #f5f5f5; + border-color: #ddd; +} +.panel-default > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ddd; +} +.panel-default > .panel-heading .badge { + color: #f5f5f5; + background-color: #333; +} +.panel-default > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ddd; +} +.panel-primary { + border-color: #337ab7; +} +.panel-primary > .panel-heading { + color: #fff; + background-color: #337ab7; + border-color: #337ab7; +} +.panel-primary > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #337ab7; +} +.panel-primary > .panel-heading .badge { + color: #337ab7; + background-color: #fff; +} +.panel-primary > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #337ab7; +} +.panel-success { + border-color: #d6e9c6; +} +.panel-success > .panel-heading { + color: #3c763d; + background-color: #dff0d8; + border-color: #d6e9c6; +} +.panel-success > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #d6e9c6; +} +.panel-success > .panel-heading .badge { + color: #dff0d8; + background-color: #3c763d; +} +.panel-success > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #d6e9c6; +} +.panel-info { + border-color: #bce8f1; +} +.panel-info > .panel-heading { + color: #31708f; + background-color: #d9edf7; + border-color: #bce8f1; +} +.panel-info > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #bce8f1; +} +.panel-info > .panel-heading .badge { + color: #d9edf7; + background-color: #31708f; +} +.panel-info > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #bce8f1; +} +.panel-warning { + border-color: #faebcc; +} +.panel-warning > .panel-heading { + color: #8a6d3b; + background-color: #fcf8e3; + border-color: #faebcc; +} +.panel-warning > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #faebcc; +} +.panel-warning > .panel-heading .badge { + color: #fcf8e3; + background-color: #8a6d3b; +} +.panel-warning > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #faebcc; +} +.panel-danger { + border-color: #ebccd1; +} +.panel-danger > .panel-heading { + color: #a94442; + background-color: #f2dede; + border-color: #ebccd1; +} +.panel-danger > .panel-heading + .panel-collapse > .panel-body { + border-top-color: #ebccd1; +} +.panel-danger > .panel-heading .badge { + color: #f2dede; + background-color: #a94442; +} +.panel-danger > .panel-footer + .panel-collapse > .panel-body { + border-bottom-color: #ebccd1; +} +.embed-responsive { + position: relative; + display: block; + height: 0; + padding: 0; + overflow: hidden; +} +.embed-responsive .embed-responsive-item, +.embed-responsive iframe, +.embed-responsive embed, +.embed-responsive object, +.embed-responsive video { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 100%; + height: 100%; + border: 0; +} +.embed-responsive.embed-responsive-16by9 { + padding-bottom: 56.25%; +} +.embed-responsive.embed-responsive-4by3 { + padding-bottom: 75%; +} +.well { + min-height: 20px; + padding: 19px; + margin-bottom: 20px; + background-color: #f5f5f5; + border: 1px solid #e3e3e3; + border-radius: 4px; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, .05); +} +.well blockquote { + border-color: #ddd; + border-color: rgba(0, 0, 0, .15); +} +.well-lg { + padding: 24px; + border-radius: 6px; +} +.well-sm { + padding: 9px; + border-radius: 3px; +} +.close { + float: right; + font-size: 21px; + font-weight: bold; + line-height: 1; + color: #000; + text-shadow: 0 1px 0 #fff; + filter: alpha(opacity=20); + opacity: .2; +} +.close:hover, +.close:focus { + color: #000; + text-decoration: none; + cursor: pointer; + filter: alpha(opacity=50); + opacity: .5; +} +button.close { + -webkit-appearance: none; + padding: 0; + cursor: pointer; + background: transparent; + border: 0; +} +.modal-open { + overflow: hidden; +} +.modal { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 1040; + display: none; + overflow: hidden; + -webkit-overflow-scrolling: touch; + outline: 0; +} +.modal.fade .modal-dialog { + -webkit-transition: -webkit-transform .3s ease-out; + -o-transition: -o-transform .3s ease-out; + transition: transform .3s ease-out; + -webkit-transform: translate(0, -25%); + -ms-transform: translate(0, -25%); + -o-transform: translate(0, -25%); + transform: translate(0, -25%); +} +.modal.in .modal-dialog { + -webkit-transform: translate(0, 0); + -ms-transform: translate(0, 0); + -o-transform: translate(0, 0); + transform: translate(0, 0); +} +.modal-open .modal { + overflow-x: hidden; + overflow-y: auto; +} +.modal-dialog { + position: relative; + width: auto; + margin: 10px; +} +.modal-content { + position: relative; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #999; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 6px; + outline: 0; + -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, .5); + box-shadow: 0 3px 9px rgba(0, 0, 0, .5); +} +.modal-backdrop { + position: absolute; + top: 0; + right: 0; + left: 0; + background-color: #000; +} +.modal-backdrop.fade { + filter: alpha(opacity=0); + opacity: 0; +} +.modal-backdrop.in { + filter: alpha(opacity=50); + opacity: .5; +} +.modal-header { + min-height: 16.42857143px; + padding: 15px; + border-bottom: 1px solid #e5e5e5; +} +.modal-header .close { + margin-top: -2px; +} +.modal-title { + margin: 0; + line-height: 1.42857143; +} +.modal-body { + position: relative; + padding: 15px; +} +.modal-footer { + padding: 15px; + text-align: right; + border-top: 1px solid #e5e5e5; +} +.modal-footer .btn + .btn { + margin-bottom: 0; + margin-left: 5px; +} +.modal-footer .btn-group .btn + .btn { + margin-left: -1px; +} +.modal-footer .btn-block + .btn-block { + margin-left: 0; +} +.modal-scrollbar-measure { + position: absolute; + top: -9999px; + width: 50px; + height: 50px; + overflow: scroll; +} +@media (min-width: 768px) { + .modal-dialog { + width: 600px; + margin: 30px auto; + } + .modal-content { + -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, .5); + box-shadow: 0 5px 15px rgba(0, 0, 0, .5); + } + .modal-sm { + width: 300px; + } +} +@media (min-width: 992px) { + .modal-lg { + width: 900px; + } +} +.tooltip { + position: absolute; + z-index: 1070; + display: block; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 12px; + font-weight: normal; + line-height: 1.4; + visibility: visible; + filter: alpha(opacity=0); + opacity: 0; +} +.tooltip.in { + filter: alpha(opacity=90); + opacity: .9; +} +.tooltip.top { + padding: 5px 0; + margin-top: -3px; +} +.tooltip.right { + padding: 0 5px; + margin-left: 3px; +} +.tooltip.bottom { + padding: 5px 0; + margin-top: 3px; +} +.tooltip.left { + padding: 0 5px; + margin-left: -3px; +} +.tooltip-inner { + max-width: 200px; + padding: 3px 8px; + color: #fff; + text-align: center; + text-decoration: none; + background-color: #000; + border-radius: 4px; +} +.tooltip-arrow { + position: absolute; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.tooltip.top .tooltip-arrow { + bottom: 0; + left: 50%; + margin-left: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-left .tooltip-arrow { + right: 5px; + bottom: 0; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.top-right .tooltip-arrow { + bottom: 0; + left: 5px; + margin-bottom: -5px; + border-width: 5px 5px 0; + border-top-color: #000; +} +.tooltip.right .tooltip-arrow { + top: 50%; + left: 0; + margin-top: -5px; + border-width: 5px 5px 5px 0; + border-right-color: #000; +} +.tooltip.left .tooltip-arrow { + top: 50%; + right: 0; + margin-top: -5px; + border-width: 5px 0 5px 5px; + border-left-color: #000; +} +.tooltip.bottom .tooltip-arrow { + top: 0; + left: 50%; + margin-left: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-left .tooltip-arrow { + top: 0; + right: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.tooltip.bottom-right .tooltip-arrow { + top: 0; + left: 5px; + margin-top: -5px; + border-width: 0 5px 5px; + border-bottom-color: #000; +} +.popover { + position: absolute; + top: 0; + left: 0; + z-index: 1060; + display: none; + max-width: 276px; + padding: 1px; + font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; + font-size: 14px; + font-weight: normal; + line-height: 1.42857143; + text-align: left; + white-space: normal; + background-color: #fff; + -webkit-background-clip: padding-box; + background-clip: padding-box; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, .2); + border-radius: 6px; + -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, .2); + box-shadow: 0 5px 10px rgba(0, 0, 0, .2); +} +.popover.top { + margin-top: -10px; +} +.popover.right { + margin-left: 10px; +} +.popover.bottom { + margin-top: 10px; +} +.popover.left { + margin-left: -10px; +} +.popover-title { + padding: 8px 14px; + margin: 0; + font-size: 14px; + background-color: #f7f7f7; + border-bottom: 1px solid #ebebeb; + border-radius: 5px 5px 0 0; +} +.popover-content { + padding: 9px 14px; +} +.popover > .arrow, +.popover > .arrow:after { + position: absolute; + display: block; + width: 0; + height: 0; + border-color: transparent; + border-style: solid; +} +.popover > .arrow { + border-width: 11px; +} +.popover > .arrow:after { + content: ""; + border-width: 10px; +} +.popover.top > .arrow { + bottom: -11px; + left: 50%; + margin-left: -11px; + border-top-color: #999; + border-top-color: rgba(0, 0, 0, .25); + border-bottom-width: 0; +} +.popover.top > .arrow:after { + bottom: 1px; + margin-left: -10px; + content: " "; + border-top-color: #fff; + border-bottom-width: 0; +} +.popover.right > .arrow { + top: 50%; + left: -11px; + margin-top: -11px; + border-right-color: #999; + border-right-color: rgba(0, 0, 0, .25); + border-left-width: 0; +} +.popover.right > .arrow:after { + bottom: -10px; + left: 1px; + content: " "; + border-right-color: #fff; + border-left-width: 0; +} +.popover.bottom > .arrow { + top: -11px; + left: 50%; + margin-left: -11px; + border-top-width: 0; + border-bottom-color: #999; + border-bottom-color: rgba(0, 0, 0, .25); +} +.popover.bottom > .arrow:after { + top: 1px; + margin-left: -10px; + content: " "; + border-top-width: 0; + border-bottom-color: #fff; +} +.popover.left > .arrow { + top: 50%; + right: -11px; + margin-top: -11px; + border-right-width: 0; + border-left-color: #999; + border-left-color: rgba(0, 0, 0, .25); +} +.popover.left > .arrow:after { + right: 1px; + bottom: -10px; + content: " "; + border-right-width: 0; + border-left-color: #fff; +} +.carousel { + position: relative; +} +.carousel-inner { + position: relative; + width: 100%; + overflow: hidden; +} +.carousel-inner > .item { + position: relative; + display: none; + -webkit-transition: .6s ease-in-out left; + -o-transition: .6s ease-in-out left; + transition: .6s ease-in-out left; +} +.carousel-inner > .item > img, +.carousel-inner > .item > a > img { + line-height: 1; +} +@media all and (transform-3d), (-webkit-transform-3d) { + .carousel-inner > .item { + -webkit-transition: -webkit-transform .6s ease-in-out; + -o-transition: -o-transform .6s ease-in-out; + transition: transform .6s ease-in-out; + + -webkit-backface-visibility: hidden; + backface-visibility: hidden; + -webkit-perspective: 1000; + perspective: 1000; + } + .carousel-inner > .item.next, + .carousel-inner > .item.active.right { + left: 0; + -webkit-transform: translate3d(100%, 0, 0); + transform: translate3d(100%, 0, 0); + } + .carousel-inner > .item.prev, + .carousel-inner > .item.active.left { + left: 0; + -webkit-transform: translate3d(-100%, 0, 0); + transform: translate3d(-100%, 0, 0); + } + .carousel-inner > .item.next.left, + .carousel-inner > .item.prev.right, + .carousel-inner > .item.active { + left: 0; + -webkit-transform: translate3d(0, 0, 0); + transform: translate3d(0, 0, 0); + } +} +.carousel-inner > .active, +.carousel-inner > .next, +.carousel-inner > .prev { + display: block; +} +.carousel-inner > .active { + left: 0; +} +.carousel-inner > .next, +.carousel-inner > .prev { + position: absolute; + top: 0; + width: 100%; +} +.carousel-inner > .next { + left: 100%; +} +.carousel-inner > .prev { + left: -100%; +} +.carousel-inner > .next.left, +.carousel-inner > .prev.right { + left: 0; +} +.carousel-inner > .active.left { + left: -100%; +} +.carousel-inner > .active.right { + left: 100%; +} +.carousel-control { + position: absolute; + top: 0; + bottom: 0; + left: 0; + width: 15%; + font-size: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, .6); + filter: alpha(opacity=50); + opacity: .5; +} +.carousel-control.left { + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .5)), to(rgba(0, 0, 0, .0001))); + background-image: linear-gradient(to right, rgba(0, 0, 0, .5) 0%, rgba(0, 0, 0, .0001) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1); + background-repeat: repeat-x; +} +.carousel-control.right { + right: 0; + left: auto; + background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + background-image: -o-linear-gradient(left, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + background-image: -webkit-gradient(linear, left top, right top, from(rgba(0, 0, 0, .0001)), to(rgba(0, 0, 0, .5))); + background-image: linear-gradient(to right, rgba(0, 0, 0, .0001) 0%, rgba(0, 0, 0, .5) 100%); + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1); + background-repeat: repeat-x; +} +.carousel-control:hover, +.carousel-control:focus { + color: #fff; + text-decoration: none; + filter: alpha(opacity=90); + outline: 0; + opacity: .9; +} +.carousel-control .icon-prev, +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-left, +.carousel-control .glyphicon-chevron-right { + position: absolute; + top: 50%; + z-index: 5; + display: inline-block; +} +.carousel-control .icon-prev, +.carousel-control .glyphicon-chevron-left { + left: 50%; + margin-left: -10px; +} +.carousel-control .icon-next, +.carousel-control .glyphicon-chevron-right { + right: 50%; + margin-right: -10px; +} +.carousel-control .icon-prev, +.carousel-control .icon-next { + width: 20px; + height: 20px; + margin-top: -10px; + font-family: serif; +} +.carousel-control .icon-prev:before { + content: '\2039'; +} +.carousel-control .icon-next:before { + content: '\203a'; +} +.carousel-indicators { + position: absolute; + bottom: 10px; + left: 50%; + z-index: 15; + width: 60%; + padding-left: 0; + margin-left: -30%; + text-align: center; + list-style: none; +} +.carousel-indicators li { + display: inline-block; + width: 10px; + height: 10px; + margin: 1px; + text-indent: -999px; + cursor: pointer; + background-color: #000 \9; + background-color: rgba(0, 0, 0, 0); + border: 1px solid #fff; + border-radius: 10px; +} +.carousel-indicators .active { + width: 12px; + height: 12px; + margin: 0; + background-color: #fff; +} +.carousel-caption { + position: absolute; + right: 15%; + bottom: 20px; + left: 15%; + z-index: 10; + padding-top: 20px; + padding-bottom: 20px; + color: #fff; + text-align: center; + text-shadow: 0 1px 2px rgba(0, 0, 0, .6); +} +.carousel-caption .btn { + text-shadow: none; +} +@media screen and (min-width: 768px) { + .carousel-control .glyphicon-chevron-left, + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-prev, + .carousel-control .icon-next { + width: 30px; + height: 30px; + margin-top: -15px; + font-size: 30px; + } + .carousel-control .glyphicon-chevron-left, + .carousel-control .icon-prev { + margin-left: -15px; + } + .carousel-control .glyphicon-chevron-right, + .carousel-control .icon-next { + margin-right: -15px; + } + .carousel-caption { + right: 20%; + left: 20%; + padding-bottom: 30px; + } + .carousel-indicators { + bottom: 20px; + } +} +.clearfix:before, +.clearfix:after, +.dl-horizontal dd:before, +.dl-horizontal dd:after, +.container:before, +.container:after, +.container-fluid:before, +.container-fluid:after, +.row:before, +.row:after, +.form-horizontal .form-group:before, +.form-horizontal .form-group:after, +.btn-toolbar:before, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:before, +.btn-group-vertical > .btn-group:after, +.nav:before, +.nav:after, +.navbar:before, +.navbar:after, +.navbar-header:before, +.navbar-header:after, +.navbar-collapse:before, +.navbar-collapse:after, +.pager:before, +.pager:after, +.panel-body:before, +.panel-body:after, +.modal-footer:before, +.modal-footer:after { + display: table; + content: " "; +} +.clearfix:after, +.dl-horizontal dd:after, +.container:after, +.container-fluid:after, +.row:after, +.form-horizontal .form-group:after, +.btn-toolbar:after, +.btn-group-vertical > .btn-group:after, +.nav:after, +.navbar:after, +.navbar-header:after, +.navbar-collapse:after, +.pager:after, +.panel-body:after, +.modal-footer:after { + clear: both; +} +.center-block { + display: block; + margin-right: auto; + margin-left: auto; +} +.pull-right { + float: right !important; +} +.pull-left { + float: left !important; +} +.hide { + display: none !important; +} +.show { + display: block !important; +} +.invisible { + visibility: hidden; +} +.text-hide { + font: 0/0 a; + color: transparent; + text-shadow: none; + background-color: transparent; + border: 0; +} +.hidden { + display: none !important; + visibility: hidden !important; +} +.affix { + position: fixed; +} +@-ms-viewport { + width: device-width; +} +.visible-xs, +.visible-sm, +.visible-md, +.visible-lg { + display: none !important; +} +.visible-xs-block, +.visible-xs-inline, +.visible-xs-inline-block, +.visible-sm-block, +.visible-sm-inline, +.visible-sm-inline-block, +.visible-md-block, +.visible-md-inline, +.visible-md-inline-block, +.visible-lg-block, +.visible-lg-inline, +.visible-lg-inline-block { + display: none !important; +} +@media (max-width: 767px) { + .visible-xs { + display: block !important; + } + table.visible-xs { + display: table; + } + tr.visible-xs { + display: table-row !important; + } + th.visible-xs, + td.visible-xs { + display: table-cell !important; + } +} +@media (max-width: 767px) { + .visible-xs-block { + display: block !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline { + display: inline !important; + } +} +@media (max-width: 767px) { + .visible-xs-inline-block { + display: inline-block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm { + display: block !important; + } + table.visible-sm { + display: table; + } + tr.visible-sm { + display: table-row !important; + } + th.visible-sm, + td.visible-sm { + display: table-cell !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-block { + display: block !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline { + display: inline !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .visible-sm-inline-block { + display: inline-block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md { + display: block !important; + } + table.visible-md { + display: table; + } + tr.visible-md { + display: table-row !important; + } + th.visible-md, + td.visible-md { + display: table-cell !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-block { + display: block !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline { + display: inline !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .visible-md-inline-block { + display: inline-block !important; + } +} +@media (min-width: 1200px) { + .visible-lg { + display: block !important; + } + table.visible-lg { + display: table; + } + tr.visible-lg { + display: table-row !important; + } + th.visible-lg, + td.visible-lg { + display: table-cell !important; + } +} +@media (min-width: 1200px) { + .visible-lg-block { + display: block !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline { + display: inline !important; + } +} +@media (min-width: 1200px) { + .visible-lg-inline-block { + display: inline-block !important; + } +} +@media (max-width: 767px) { + .hidden-xs { + display: none !important; + } +} +@media (min-width: 768px) and (max-width: 991px) { + .hidden-sm { + display: none !important; + } +} +@media (min-width: 992px) and (max-width: 1199px) { + .hidden-md { + display: none !important; + } +} +@media (min-width: 1200px) { + .hidden-lg { + display: none !important; + } +} +.visible-print { + display: none !important; +} +@media print { + .visible-print { + display: block !important; + } + table.visible-print { + display: table; + } + tr.visible-print { + display: table-row !important; + } + th.visible-print, + td.visible-print { + display: table-cell !important; + } +} +.visible-print-block { + display: none !important; +} +@media print { + .visible-print-block { + display: block !important; + } +} +.visible-print-inline { + display: none !important; +} +@media print { + .visible-print-inline { + display: inline !important; + } +} +.visible-print-inline-block { + display: none !important; +} +@media print { + .visible-print-inline-block { + display: inline-block !important; + } +} +@media print { + .hidden-print { + display: none !important; + } +} +/*# sourceMappingURL=bootstrap.css.map */ diff --git a/src/Public/css/vendor/bootstrap.css.map b/src/Public/css/vendor/bootstrap.css.map new file mode 100644 index 000000000..a02f6ba0a --- /dev/null +++ b/src/Public/css/vendor/bootstrap.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["bootstrap.css","less/normalize.less","less/print.less","less/glyphicons.less","less/scaffolding.less","less/mixins/vendor-prefixes.less","less/mixins/tab-focus.less","less/mixins/image.less","less/type.less","less/mixins/text-emphasis.less","less/mixins/background-variant.less","less/mixins/text-overflow.less","less/code.less","less/grid.less","less/mixins/grid.less","less/mixins/grid-framework.less","less/tables.less","less/mixins/table-row.less","less/forms.less","less/mixins/forms.less","less/buttons.less","less/mixins/buttons.less","less/mixins/opacity.less","less/component-animations.less","less/dropdowns.less","less/mixins/nav-divider.less","less/mixins/reset-filter.less","less/button-groups.less","less/mixins/border-radius.less","less/input-groups.less","less/navs.less","less/navbar.less","less/mixins/nav-vertical-align.less","less/utilities.less","less/breadcrumbs.less","less/pagination.less","less/mixins/pagination.less","less/pager.less","less/labels.less","less/mixins/labels.less","less/badges.less","less/jumbotron.less","less/thumbnails.less","less/alerts.less","less/mixins/alerts.less","less/progress-bars.less","less/mixins/gradients.less","less/mixins/progress-bar.less","less/media.less","less/list-group.less","less/mixins/list-group.less","less/panels.less","less/mixins/panels.less","less/responsive-embed.less","less/wells.less","less/close.less","less/modals.less","less/tooltip.less","less/popovers.less","less/carousel.less","less/mixins/clearfix.less","less/mixins/center-block.less","less/mixins/hide-text.less","less/responsive-utilities.less","less/mixins/responsive-visibility.less"],"names":[],"mappings":"AAAA,6DAA4D;ACQ5D;EACE,yBAAA;EACA,4BAAA;EACA,gCAAA;EDND;ACaD;EACE,WAAA;EDXD;ACwBD;;;;;;;;;;;;;EAaE,gBAAA;EDtBD;AC8BD;;;;EAIE,uBAAA;EACA,0BAAA;ED5BD;ACoCD;EACE,eAAA;EACA,WAAA;EDlCD;AC0CD;;EAEE,eAAA;EDxCD;ACkDD;EACE,+BAAA;EDhDD;ACuDD;;EAEE,YAAA;EDrDD;AC+DD;EACE,2BAAA;ED7DD;ACoED;;EAEE,mBAAA;EDlED;ACyED;EACE,oBAAA;EDvED;AC+ED;EACE,gBAAA;EACA,kBAAA;ED7ED;ACoFD;EACE,kBAAA;EACA,aAAA;EDlFD;ACyFD;EACE,gBAAA;EDvFD;AC8FD;;EAEE,gBAAA;EACA,gBAAA;EACA,oBAAA;EACA,0BAAA;ED5FD;AC+FD;EACE,aAAA;ED7FD;ACgGD;EACE,iBAAA;ED9FD;ACwGD;EACE,WAAA;EDtGD;AC6GD;EACE,kBAAA;ED3GD;ACqHD;EACE,kBAAA;EDnHD;AC0HD;EACE,8BAAA;EACA,iCAAA;UAAA,yBAAA;EACA,WAAA;EDxHD;AC+HD;EACE,gBAAA;ED7HD;ACoID;;;;EAIE,mCAAA;EACA,gBAAA;EDlID;ACoJD;;;;;EAKE,gBAAA;EACA,eAAA;EACA,WAAA;EDlJD;ACyJD;EACE,mBAAA;EDvJD;ACiKD;;EAEE,sBAAA;ED/JD;AC0KD;;;;EAIE,4BAAA;EACA,iBAAA;EDxKD;AC+KD;;EAEE,iBAAA;ED7KD;ACoLD;;EAEE,WAAA;EACA,YAAA;EDlLD;AC0LD;EACE,qBAAA;EDxLD;ACmMD;;EAEE,gCAAA;KAAA,6BAAA;UAAA,wBAAA;EACA,YAAA;EDjMD;AC0MD;;EAEE,cAAA;EDxMD;ACiND;EACE,+BAAA;EACA,8BAAA;EACA,iCAAA;EACA,yBAAA;ED/MD;ACwND;;EAEE,0BAAA;EDtND;AC6ND;EACE,2BAAA;EACA,eAAA;EACA,gCAAA;ED3ND;ACmOD;EACE,WAAA;EACA,YAAA;EDjOD;ACwOD;EACE,gBAAA;EDtOD;AC8OD;EACE,mBAAA;ED5OD;ACsPD;EACE,2BAAA;EACA,mBAAA;EDpPD;ACuPD;;EAEE,YAAA;EDrPD;AACD,sFAAqF;AE1ErF;EAnGI;;;IAGI,oCAAA;IACA,wBAAA;IACA,qCAAA;YAAA,6BAAA;IACA,8BAAA;IFgLL;EE7KC;;IAEI,4BAAA;IF+KL;EE5KC;IACI,8BAAA;IF8KL;EE3KC;IACI,+BAAA;IF6KL;EExKC;;IAEI,aAAA;IF0KL;EEvKC;;IAEI,wBAAA;IACA,0BAAA;IFyKL;EEtKC;IACI,6BAAA;IFwKL;EErKC;;IAEI,0BAAA;IFuKL;EEpKC;IACI,4BAAA;IFsKL;EEnKC;;;IAGI,YAAA;IACA,WAAA;IFqKL;EElKC;;IAEI,yBAAA;IFoKL;EE7JC;IACI,6BAAA;IF+JL;EE3JC;IACI,eAAA;IF6JL;EE3JC;;IAGQ,mCAAA;IF4JT;EEzJC;IACI,wBAAA;IF2JL;EExJC;IACI,sCAAA;IF0JL;EE3JC;;IAKQ,mCAAA;IF0JT;EEvJC;;IAGQ,mCAAA;IFwJT;EACF;AGpPD;EACE,qCAAA;EACA,uDAAA;EACA,6TAAA;EHsPD;AG/OD;EACE,oBAAA;EACA,UAAA;EACA,uBAAA;EACA,qCAAA;EACA,oBAAA;EACA,qBAAA;EACA,gBAAA;EACA,qCAAA;EACA,oCAAA;EHiPD;AG7OmC;EAAW,gBAAA;EHgP9C;AG/OmC;EAAW,gBAAA;EHkP9C;AGhPmC;;EAAW,kBAAA;EHoP9C;AGnPmC;EAAW,kBAAA;EHsP9C;AGrPmC;EAAW,kBAAA;EHwP9C;AGvPmC;EAAW,kBAAA;EH0P9C;AGzPmC;EAAW,kBAAA;EH4P9C;AG3PmC;EAAW,kBAAA;EH8P9C;AG7PmC;EAAW,kBAAA;EHgQ9C;AG/PmC;EAAW,kBAAA;EHkQ9C;AGjQmC;EAAW,kBAAA;EHoQ9C;AGnQmC;EAAW,kBAAA;EHsQ9C;AGrQmC;EAAW,kBAAA;EHwQ9C;AGvQmC;EAAW,kBAAA;EH0Q9C;AGzQmC;EAAW,kBAAA;EH4Q9C;AG3QmC;EAAW,kBAAA;EH8Q9C;AG7QmC;EAAW,kBAAA;EHgR9C;AG/QmC;EAAW,kBAAA;EHkR9C;AGjRmC;EAAW,kBAAA;EHoR9C;AGnRmC;EAAW,kBAAA;EHsR9C;AGrRmC;EAAW,kBAAA;EHwR9C;AGvRmC;EAAW,kBAAA;EH0R9C;AGzRmC;EAAW,kBAAA;EH4R9C;AG3RmC;EAAW,kBAAA;EH8R9C;AG7RmC;EAAW,kBAAA;EHgS9C;AG/RmC;EAAW,kBAAA;EHkS9C;AGjSmC;EAAW,kBAAA;EHoS9C;AGnSmC;EAAW,kBAAA;EHsS9C;AGrSmC;EAAW,kBAAA;EHwS9C;AGvSmC;EAAW,kBAAA;EH0S9C;AGzSmC;EAAW,kBAAA;EH4S9C;AG3SmC;EAAW,kBAAA;EH8S9C;AG7SmC;EAAW,kBAAA;EHgT9C;AG/SmC;EAAW,kBAAA;EHkT9C;AGjTmC;EAAW,kBAAA;EHoT9C;AGnTmC;EAAW,kBAAA;EHsT9C;AGrTmC;EAAW,kBAAA;EHwT9C;AGvTmC;EAAW,kBAAA;EH0T9C;AGzTmC;EAAW,kBAAA;EH4T9C;AG3TmC;EAAW,kBAAA;EH8T9C;AG7TmC;EAAW,kBAAA;EHgU9C;AG/TmC;EAAW,kBAAA;EHkU9C;AGjUmC;EAAW,kBAAA;EHoU9C;AGnUmC;EAAW,kBAAA;EHsU9C;AGrUmC;EAAW,kBAAA;EHwU9C;AGvUmC;EAAW,kBAAA;EH0U9C;AGzUmC;EAAW,kBAAA;EH4U9C;AG3UmC;EAAW,kBAAA;EH8U9C;AG7UmC;EAAW,kBAAA;EHgV9C;AG/UmC;EAAW,kBAAA;EHkV9C;AGjVmC;EAAW,kBAAA;EHoV9C;AGnVmC;EAAW,kBAAA;EHsV9C;AGrVmC;EAAW,kBAAA;EHwV9C;AGvVmC;EAAW,kBAAA;EH0V9C;AGzVmC;EAAW,kBAAA;EH4V9C;AG3VmC;EAAW,kBAAA;EH8V9C;AG7VmC;EAAW,kBAAA;EHgW9C;AG/VmC;EAAW,kBAAA;EHkW9C;AGjWmC;EAAW,kBAAA;EHoW9C;AGnWmC;EAAW,kBAAA;EHsW9C;AGrWmC;EAAW,kBAAA;EHwW9C;AGvWmC;EAAW,kBAAA;EH0W9C;AGzWmC;EAAW,kBAAA;EH4W9C;AG3WmC;EAAW,kBAAA;EH8W9C;AG7WmC;EAAW,kBAAA;EHgX9C;AG/WmC;EAAW,kBAAA;EHkX9C;AGjXmC;EAAW,kBAAA;EHoX9C;AGnXmC;EAAW,kBAAA;EHsX9C;AGrXmC;EAAW,kBAAA;EHwX9C;AGvXmC;EAAW,kBAAA;EH0X9C;AGzXmC;EAAW,kBAAA;EH4X9C;AG3XmC;EAAW,kBAAA;EH8X9C;AG7XmC;EAAW,kBAAA;EHgY9C;AG/XmC;EAAW,kBAAA;EHkY9C;AGjYmC;EAAW,kBAAA;EHoY9C;AGnYmC;EAAW,kBAAA;EHsY9C;AGrYmC;EAAW,kBAAA;EHwY9C;AGvYmC;EAAW,kBAAA;EH0Y9C;AGzYmC;EAAW,kBAAA;EH4Y9C;AG3YmC;EAAW,kBAAA;EH8Y9C;AG7YmC;EAAW,kBAAA;EHgZ9C;AG/YmC;EAAW,kBAAA;EHkZ9C;AGjZmC;EAAW,kBAAA;EHoZ9C;AGnZmC;EAAW,kBAAA;EHsZ9C;AGrZmC;EAAW,kBAAA;EHwZ9C;AGvZmC;EAAW,kBAAA;EH0Z9C;AGzZmC;EAAW,kBAAA;EH4Z9C;AG3ZmC;EAAW,kBAAA;EH8Z9C;AG7ZmC;EAAW,kBAAA;EHga9C;AG/ZmC;EAAW,kBAAA;EHka9C;AGjamC;EAAW,kBAAA;EHoa9C;AGnamC;EAAW,kBAAA;EHsa9C;AGramC;EAAW,kBAAA;EHwa9C;AGvamC;EAAW,kBAAA;EH0a9C;AGzamC;EAAW,kBAAA;EH4a9C;AG3amC;EAAW,kBAAA;EH8a9C;AG7amC;EAAW,kBAAA;EHgb9C;AG/amC;EAAW,kBAAA;EHkb9C;AGjbmC;EAAW,kBAAA;EHob9C;AGnbmC;EAAW,kBAAA;EHsb9C;AGrbmC;EAAW,kBAAA;EHwb9C;AGvbmC;EAAW,kBAAA;EH0b9C;AGzbmC;EAAW,kBAAA;EH4b9C;AG3bmC;EAAW,kBAAA;EH8b9C;AG7bmC;EAAW,kBAAA;EHgc9C;AG/bmC;EAAW,kBAAA;EHkc9C;AGjcmC;EAAW,kBAAA;EHoc9C;AGncmC;EAAW,kBAAA;EHsc9C;AGrcmC;EAAW,kBAAA;EHwc9C;AGvcmC;EAAW,kBAAA;EH0c9C;AGzcmC;EAAW,kBAAA;EH4c9C;AG3cmC;EAAW,kBAAA;EH8c9C;AG7cmC;EAAW,kBAAA;EHgd9C;AG/cmC;EAAW,kBAAA;EHkd9C;AGjdmC;EAAW,kBAAA;EHod9C;AGndmC;EAAW,kBAAA;EHsd9C;AGrdmC;EAAW,kBAAA;EHwd9C;AGvdmC;EAAW,kBAAA;EH0d9C;AGzdmC;EAAW,kBAAA;EH4d9C;AG3dmC;EAAW,kBAAA;EH8d9C;AG7dmC;EAAW,kBAAA;EHge9C;AG/dmC;EAAW,kBAAA;EHke9C;AGjemC;EAAW,kBAAA;EHoe9C;AGnemC;EAAW,kBAAA;EHse9C;AGremC;EAAW,kBAAA;EHwe9C;AGvemC;EAAW,kBAAA;EH0e9C;AGzemC;EAAW,kBAAA;EH4e9C;AG3emC;EAAW,kBAAA;EH8e9C;AG7emC;EAAW,kBAAA;EHgf9C;AG/emC;EAAW,kBAAA;EHkf9C;AGjfmC;EAAW,kBAAA;EHof9C;AGnfmC;EAAW,kBAAA;EHsf9C;AGrfmC;EAAW,kBAAA;EHwf9C;AGvfmC;EAAW,kBAAA;EH0f9C;AGzfmC;EAAW,kBAAA;EH4f9C;AG3fmC;EAAW,kBAAA;EH8f9C;AG7fmC;EAAW,kBAAA;EHggB9C;AG/fmC;EAAW,kBAAA;EHkgB9C;AGjgBmC;EAAW,kBAAA;EHogB9C;AGngBmC;EAAW,kBAAA;EHsgB9C;AGrgBmC;EAAW,kBAAA;EHwgB9C;AGvgBmC;EAAW,kBAAA;EH0gB9C;AGzgBmC;EAAW,kBAAA;EH4gB9C;AG3gBmC;EAAW,kBAAA;EH8gB9C;AG7gBmC;EAAW,kBAAA;EHghB9C;AG/gBmC;EAAW,kBAAA;EHkhB9C;AGjhBmC;EAAW,kBAAA;EHohB9C;AGnhBmC;EAAW,kBAAA;EHshB9C;AGrhBmC;EAAW,kBAAA;EHwhB9C;AGvhBmC;EAAW,kBAAA;EH0hB9C;AGzhBmC;EAAW,kBAAA;EH4hB9C;AG3hBmC;EAAW,kBAAA;EH8hB9C;AG7hBmC;EAAW,kBAAA;EHgiB9C;AG/hBmC;EAAW,kBAAA;EHkiB9C;AGjiBmC;EAAW,kBAAA;EHoiB9C;AGniBmC;EAAW,kBAAA;EHsiB9C;AGriBmC;EAAW,kBAAA;EHwiB9C;AGviBmC;EAAW,kBAAA;EH0iB9C;AGziBmC;EAAW,kBAAA;EH4iB9C;AG3iBmC;EAAW,kBAAA;EH8iB9C;AG7iBmC;EAAW,kBAAA;EHgjB9C;AG/iBmC;EAAW,kBAAA;EHkjB9C;AGjjBmC;EAAW,kBAAA;EHojB9C;AGnjBmC;EAAW,kBAAA;EHsjB9C;AGrjBmC;EAAW,kBAAA;EHwjB9C;AGvjBmC;EAAW,kBAAA;EH0jB9C;AGzjBmC;EAAW,kBAAA;EH4jB9C;AG3jBmC;EAAW,kBAAA;EH8jB9C;AG7jBmC;EAAW,kBAAA;EHgkB9C;AG/jBmC;EAAW,kBAAA;EHkkB9C;AGjkBmC;EAAW,kBAAA;EHokB9C;AGnkBmC;EAAW,kBAAA;EHskB9C;AGrkBmC;EAAW,kBAAA;EHwkB9C;AGvkBmC;EAAW,kBAAA;EH0kB9C;AGzkBmC;EAAW,kBAAA;EH4kB9C;AG3kBmC;EAAW,kBAAA;EH8kB9C;AG7kBmC;EAAW,kBAAA;EHglB9C;AG/kBmC;EAAW,kBAAA;EHklB9C;AGjlBmC;EAAW,kBAAA;EHolB9C;AGnlBmC;EAAW,kBAAA;EHslB9C;AGrlBmC;EAAW,kBAAA;EHwlB9C;AGvlBmC;EAAW,kBAAA;EH0lB9C;AGzlBmC;EAAW,kBAAA;EH4lB9C;AG3lBmC;EAAW,kBAAA;EH8lB9C;AG7lBmC;EAAW,kBAAA;EHgmB9C;AG/lBmC;EAAW,kBAAA;EHkmB9C;AGjmBmC;EAAW,kBAAA;EHomB9C;AGnmBmC;EAAW,kBAAA;EHsmB9C;AGrmBmC;EAAW,kBAAA;EHwmB9C;AGvmBmC;EAAW,kBAAA;EH0mB9C;AGzmBmC;EAAW,kBAAA;EH4mB9C;AG3mBmC;EAAW,kBAAA;EH8mB9C;AG7mBmC;EAAW,kBAAA;EHgnB9C;AG/mBmC;EAAW,kBAAA;EHknB9C;AGjnBmC;EAAW,kBAAA;EHonB9C;AGnnBmC;EAAW,kBAAA;EHsnB9C;AGrnBmC;EAAW,kBAAA;EHwnB9C;AGvnBmC;EAAW,kBAAA;EH0nB9C;AGznBmC;EAAW,kBAAA;EH4nB9C;AG3nBmC;EAAW,kBAAA;EH8nB9C;AI71BD;ECgEE,gCAAA;EACG,6BAAA;EACK,wBAAA;ELgyBT;AI/1BD;;EC6DE,gCAAA;EACG,6BAAA;EACK,wBAAA;ELsyBT;AI71BD;EACE,iBAAA;EACA,+CAAA;EJ+1BD;AI51BD;EACE,6DAAA;EACA,iBAAA;EACA,yBAAA;EACA,gBAAA;EACA,2BAAA;EJ81BD;AI11BD;;;;EAIE,sBAAA;EACA,oBAAA;EACA,sBAAA;EJ41BD;AIt1BD;EACE,gBAAA;EACA,uBAAA;EJw1BD;AIt1BC;;EAEE,gBAAA;EACA,4BAAA;EJw1BH;AIr1BC;EErDA,sBAAA;EAEA,4CAAA;EACA,sBAAA;EN44BD;AI/0BD;EACE,WAAA;EJi1BD;AI30BD;EACE,wBAAA;EJ60BD;AIz0BD;;;;;EGvEE,gBAAA;EACA,iBAAA;EACA,cAAA;EPu5BD;AI70BD;EACE,oBAAA;EJ+0BD;AIz0BD;EACE,cAAA;EACA,yBAAA;EACA,2BAAA;EACA,2BAAA;EACA,oBAAA;EC6FA,0CAAA;EACK,qCAAA;EACG,kCAAA;EEvLR,uBAAA;EACA,iBAAA;EACA,cAAA;EPu6BD;AIz0BD;EACE,oBAAA;EJ20BD;AIr0BD;EACE,kBAAA;EACA,qBAAA;EACA,WAAA;EACA,+BAAA;EJu0BD;AI/zBD;EACE,oBAAA;EACA,YAAA;EACA,aAAA;EACA,cAAA;EACA,YAAA;EACA,kBAAA;EACA,wBAAA;EACA,WAAA;EJi0BD;AIzzBC;;EAEE,kBAAA;EACA,aAAA;EACA,cAAA;EACA,WAAA;EACA,mBAAA;EACA,YAAA;EJ2zBH;AQt8BD;;;;;;;;;;;;EAEE,sBAAA;EACA,kBAAA;EACA,kBAAA;EACA,gBAAA;ERk9BD;AQv9BD;;;;;;;;;;;;;;;;;;;;;;;;EASI,qBAAA;EACA,gBAAA;EACA,gBAAA;ERw+BH;AQp+BD;;;;;;EAGE,kBAAA;EACA,qBAAA;ERy+BD;AQ7+BD;;;;;;;;;;;;EAQI,gBAAA;ERm/BH;AQh/BD;;;;;;EAGE,kBAAA;EACA,qBAAA;ERq/BD;AQz/BD;;;;;;;;;;;;EAQI,gBAAA;ER+/BH;AQ3/BD;;EAAU,iBAAA;ER+/BT;AQ9/BD;;EAAU,iBAAA;ERkgCT;AQjgCD;;EAAU,iBAAA;ERqgCT;AQpgCD;;EAAU,iBAAA;ERwgCT;AQvgCD;;EAAU,iBAAA;ER2gCT;AQ1gCD;;EAAU,iBAAA;ER8gCT;AQxgCD;EACE,kBAAA;ER0gCD;AQvgCD;EACE,qBAAA;EACA,iBAAA;EACA,kBAAA;EACA,kBAAA;ERygCD;AQpgCD;EAAA;IAFI,iBAAA;IR0gCD;EACF;AQlgCD;;EAEE,gBAAA;ERogCD;AQjgCD;;EAEE,2BAAA;EACA,eAAA;ERmgCD;AQ//BD;EAAuB,kBAAA;ERkgCtB;AQjgCD;EAAuB,mBAAA;ERogCtB;AQngCD;EAAuB,oBAAA;ERsgCtB;AQrgCD;EAAuB,qBAAA;ERwgCtB;AQvgCD;EAAuB,qBAAA;ER0gCtB;AQvgCD;EAAuB,2BAAA;ER0gCtB;AQzgCD;EAAuB,2BAAA;ER4gCtB;AQ3gCD;EAAuB,4BAAA;ER8gCtB;AQ3gCD;EACE,gBAAA;ER6gCD;AQ3gCD;ECrGE,gBAAA;ETmnCD;ASlnCC;EACE,gBAAA;ETonCH;AQ9gCD;ECxGE,gBAAA;ETynCD;ASxnCC;EACE,gBAAA;ET0nCH;AQjhCD;EC3GE,gBAAA;ET+nCD;AS9nCC;EACE,gBAAA;ETgoCH;AQphCD;EC9GE,gBAAA;ETqoCD;ASpoCC;EACE,gBAAA;ETsoCH;AQvhCD;ECjHE,gBAAA;ET2oCD;AS1oCC;EACE,gBAAA;ET4oCH;AQthCD;EAGE,aAAA;EE3HA,2BAAA;EVkpCD;AUjpCC;EACE,2BAAA;EVmpCH;AQvhCD;EE9HE,2BAAA;EVwpCD;AUvpCC;EACE,2BAAA;EVypCH;AQ1hCD;EEjIE,2BAAA;EV8pCD;AU7pCC;EACE,2BAAA;EV+pCH;AQ7hCD;EEpIE,2BAAA;EVoqCD;AUnqCC;EACE,2BAAA;EVqqCH;AQhiCD;EEvIE,2BAAA;EV0qCD;AUzqCC;EACE,2BAAA;EV2qCH;AQ9hCD;EACE,qBAAA;EACA,qBAAA;EACA,kCAAA;ERgiCD;AQxhCD;;EAEE,eAAA;EACA,qBAAA;ER0hCD;AQ7hCD;;;;EAMI,kBAAA;ER6hCH;AQthCD;EACE,iBAAA;EACA,kBAAA;ERwhCD;AQphCD;EALE,iBAAA;EACA,kBAAA;EAMA,mBAAA;ERuhCD;AQzhCD;EAKI,uBAAA;EACA,mBAAA;EACA,oBAAA;ERuhCH;AQlhCD;EACE,eAAA;EACA,qBAAA;ERohCD;AQlhCD;;EAEE,yBAAA;ERohCD;AQlhCD;EACE,mBAAA;ERohCD;AQlhCD;EACE,gBAAA;ERohCD;AQ3/BD;EAAA;IAVM,aAAA;IACA,cAAA;IACA,aAAA;IACA,mBAAA;IGtNJ,kBAAA;IACA,yBAAA;IACA,qBAAA;IXguCC;EQrgCH;IAHM,oBAAA;IR2gCH;EACF;AQlgCD;;EAGE,cAAA;EACA,mCAAA;ERmgCD;AQjgCD;EACE,gBAAA;EACA,2BAAA;ERmgCD;AQ//BD;EACE,oBAAA;EACA,kBAAA;EACA,mBAAA;EACA,gCAAA;ERigCD;AQ5/BG;;;EACE,kBAAA;ERggCL;AQ1gCD;;;EAmBI,gBAAA;EACA,gBAAA;EACA,yBAAA;EACA,gBAAA;ER4/BH;AQ1/BG;;;EACE,wBAAA;ER8/BL;AQt/BD;;EAEE,qBAAA;EACA,iBAAA;EACA,iCAAA;EACA,gBAAA;EACA,mBAAA;ERw/BD;AQl/BG;;;;;;EAAW,aAAA;ER0/Bd;AQz/BG;;;;;;EACE,wBAAA;ERggCL;AQ1/BD;EACE,qBAAA;EACA,oBAAA;EACA,yBAAA;ER4/BD;AYlyCD;;;;EAIE,gEAAA;EZoyCD;AYhyCD;EACE,kBAAA;EACA,gBAAA;EACA,gBAAA;EACA,2BAAA;EACA,oBAAA;EZkyCD;AY9xCD;EACE,kBAAA;EACA,gBAAA;EACA,gBAAA;EACA,2BAAA;EACA,oBAAA;EACA,wDAAA;UAAA,gDAAA;EZgyCD;AYtyCD;EASI,YAAA;EACA,iBAAA;EACA,mBAAA;EACA,0BAAA;UAAA,kBAAA;EZgyCH;AY3xCD;EACE,gBAAA;EACA,gBAAA;EACA,kBAAA;EACA,iBAAA;EACA,yBAAA;EACA,uBAAA;EACA,uBAAA;EACA,gBAAA;EACA,2BAAA;EACA,2BAAA;EACA,oBAAA;EZ6xCD;AYxyCD;EAeI,YAAA;EACA,oBAAA;EACA,gBAAA;EACA,uBAAA;EACA,+BAAA;EACA,kBAAA;EZ4xCH;AYvxCD;EACE,mBAAA;EACA,oBAAA;EZyxCD;Aan1CD;ECHE,oBAAA;EACA,mBAAA;EACA,oBAAA;EACA,qBAAA;Edy1CD;Aan1CC;EAAA;IAFE,cAAA;Iby1CD;EACF;Aar1CC;EAAA;IAFE,cAAA;Ib21CD;EACF;Aav1CD;EAAA;IAFI,eAAA;Ib61CD;EACF;Aap1CD;ECvBE,oBAAA;EACA,mBAAA;EACA,oBAAA;EACA,qBAAA;Ed82CD;Aaj1CD;ECvBE,oBAAA;EACA,qBAAA;Ed22CD;Ae32CG;EACE,oBAAA;EAEA,iBAAA;EAEA,oBAAA;EACA,qBAAA;Ef22CL;Ae31CG;EACE,aAAA;Ef61CL;Aet1CC;EACE,aAAA;Efw1CH;Aez1CC;EACE,qBAAA;Ef21CH;Ae51CC;EACE,qBAAA;Ef81CH;Ae/1CC;EACE,YAAA;Efi2CH;Ael2CC;EACE,qBAAA;Efo2CH;Aer2CC;EACE,qBAAA;Efu2CH;Aex2CC;EACE,YAAA;Ef02CH;Ae32CC;EACE,qBAAA;Ef62CH;Ae92CC;EACE,qBAAA;Efg3CH;Aej3CC;EACE,YAAA;Efm3CH;Aep3CC;EACE,qBAAA;Efs3CH;Aev3CC;EACE,oBAAA;Efy3CH;Ae32CC;EACE,aAAA;Ef62CH;Ae92CC;EACE,qBAAA;Efg3CH;Aej3CC;EACE,qBAAA;Efm3CH;Aep3CC;EACE,YAAA;Efs3CH;Aev3CC;EACE,qBAAA;Efy3CH;Ae13CC;EACE,qBAAA;Ef43CH;Ae73CC;EACE,YAAA;Ef+3CH;Aeh4CC;EACE,qBAAA;Efk4CH;Aen4CC;EACE,qBAAA;Efq4CH;Aet4CC;EACE,YAAA;Efw4CH;Aez4CC;EACE,qBAAA;Ef24CH;Ae54CC;EACE,oBAAA;Ef84CH;Ae14CC;EACE,aAAA;Ef44CH;Ae55CC;EACE,YAAA;Ef85CH;Ae/5CC;EACE,oBAAA;Efi6CH;Ael6CC;EACE,oBAAA;Efo6CH;Aer6CC;EACE,WAAA;Efu6CH;Aex6CC;EACE,oBAAA;Ef06CH;Ae36CC;EACE,oBAAA;Ef66CH;Ae96CC;EACE,WAAA;Efg7CH;Aej7CC;EACE,oBAAA;Efm7CH;Aep7CC;EACE,oBAAA;Efs7CH;Aev7CC;EACE,WAAA;Efy7CH;Ae17CC;EACE,oBAAA;Ef47CH;Ae77CC;EACE,mBAAA;Ef+7CH;Ae37CC;EACE,YAAA;Ef67CH;Ae/6CC;EACE,mBAAA;Efi7CH;Ael7CC;EACE,2BAAA;Efo7CH;Aer7CC;EACE,2BAAA;Efu7CH;Aex7CC;EACE,kBAAA;Ef07CH;Ae37CC;EACE,2BAAA;Ef67CH;Ae97CC;EACE,2BAAA;Efg8CH;Aej8CC;EACE,kBAAA;Efm8CH;Aep8CC;EACE,2BAAA;Efs8CH;Aev8CC;EACE,2BAAA;Efy8CH;Ae18CC;EACE,kBAAA;Ef48CH;Ae78CC;EACE,2BAAA;Ef+8CH;Aeh9CC;EACE,0BAAA;Efk9CH;Aen9CC;EACE,iBAAA;Efq9CH;Aaz9CD;EE9BI;IACE,aAAA;If0/CH;Een/CD;IACE,aAAA;Ifq/CD;Eet/CD;IACE,qBAAA;Ifw/CD;Eez/CD;IACE,qBAAA;If2/CD;Ee5/CD;IACE,YAAA;If8/CD;Ee//CD;IACE,qBAAA;IfigDD;EelgDD;IACE,qBAAA;IfogDD;EergDD;IACE,YAAA;IfugDD;EexgDD;IACE,qBAAA;If0gDD;Ee3gDD;IACE,qBAAA;If6gDD;Ee9gDD;IACE,YAAA;IfghDD;EejhDD;IACE,qBAAA;IfmhDD;EephDD;IACE,oBAAA;IfshDD;EexgDD;IACE,aAAA;If0gDD;Ee3gDD;IACE,qBAAA;If6gDD;Ee9gDD;IACE,qBAAA;IfghDD;EejhDD;IACE,YAAA;IfmhDD;EephDD;IACE,qBAAA;IfshDD;EevhDD;IACE,qBAAA;IfyhDD;Ee1hDD;IACE,YAAA;If4hDD;Ee7hDD;IACE,qBAAA;If+hDD;EehiDD;IACE,qBAAA;IfkiDD;EeniDD;IACE,YAAA;IfqiDD;EetiDD;IACE,qBAAA;IfwiDD;EeziDD;IACE,oBAAA;If2iDD;EeviDD;IACE,aAAA;IfyiDD;EezjDD;IACE,YAAA;If2jDD;Ee5jDD;IACE,oBAAA;If8jDD;Ee/jDD;IACE,oBAAA;IfikDD;EelkDD;IACE,WAAA;IfokDD;EerkDD;IACE,oBAAA;IfukDD;EexkDD;IACE,oBAAA;If0kDD;Ee3kDD;IACE,WAAA;If6kDD;Ee9kDD;IACE,oBAAA;IfglDD;EejlDD;IACE,oBAAA;IfmlDD;EeplDD;IACE,WAAA;IfslDD;EevlDD;IACE,oBAAA;IfylDD;Ee1lDD;IACE,mBAAA;If4lDD;EexlDD;IACE,YAAA;If0lDD;Ee5kDD;IACE,mBAAA;If8kDD;Ee/kDD;IACE,2BAAA;IfilDD;EellDD;IACE,2BAAA;IfolDD;EerlDD;IACE,kBAAA;IfulDD;EexlDD;IACE,2BAAA;If0lDD;Ee3lDD;IACE,2BAAA;If6lDD;Ee9lDD;IACE,kBAAA;IfgmDD;EejmDD;IACE,2BAAA;IfmmDD;EepmDD;IACE,2BAAA;IfsmDD;EevmDD;IACE,kBAAA;IfymDD;Ee1mDD;IACE,2BAAA;If4mDD;Ee7mDD;IACE,0BAAA;If+mDD;EehnDD;IACE,iBAAA;IfknDD;EACF;Aa9mDD;EEvCI;IACE,aAAA;IfwpDH;EejpDD;IACE,aAAA;IfmpDD;EeppDD;IACE,qBAAA;IfspDD;EevpDD;IACE,qBAAA;IfypDD;Ee1pDD;IACE,YAAA;If4pDD;Ee7pDD;IACE,qBAAA;If+pDD;EehqDD;IACE,qBAAA;IfkqDD;EenqDD;IACE,YAAA;IfqqDD;EetqDD;IACE,qBAAA;IfwqDD;EezqDD;IACE,qBAAA;If2qDD;Ee5qDD;IACE,YAAA;If8qDD;Ee/qDD;IACE,qBAAA;IfirDD;EelrDD;IACE,oBAAA;IforDD;EetqDD;IACE,aAAA;IfwqDD;EezqDD;IACE,qBAAA;If2qDD;Ee5qDD;IACE,qBAAA;If8qDD;Ee/qDD;IACE,YAAA;IfirDD;EelrDD;IACE,qBAAA;IforDD;EerrDD;IACE,qBAAA;IfurDD;EexrDD;IACE,YAAA;If0rDD;Ee3rDD;IACE,qBAAA;If6rDD;Ee9rDD;IACE,qBAAA;IfgsDD;EejsDD;IACE,YAAA;IfmsDD;EepsDD;IACE,qBAAA;IfssDD;EevsDD;IACE,oBAAA;IfysDD;EersDD;IACE,aAAA;IfusDD;EevtDD;IACE,YAAA;IfytDD;Ee1tDD;IACE,oBAAA;If4tDD;Ee7tDD;IACE,oBAAA;If+tDD;EehuDD;IACE,WAAA;IfkuDD;EenuDD;IACE,oBAAA;IfquDD;EetuDD;IACE,oBAAA;IfwuDD;EezuDD;IACE,WAAA;If2uDD;Ee5uDD;IACE,oBAAA;If8uDD;Ee/uDD;IACE,oBAAA;IfivDD;EelvDD;IACE,WAAA;IfovDD;EervDD;IACE,oBAAA;IfuvDD;EexvDD;IACE,mBAAA;If0vDD;EetvDD;IACE,YAAA;IfwvDD;Ee1uDD;IACE,mBAAA;If4uDD;Ee7uDD;IACE,2BAAA;If+uDD;EehvDD;IACE,2BAAA;IfkvDD;EenvDD;IACE,kBAAA;IfqvDD;EetvDD;IACE,2BAAA;IfwvDD;EezvDD;IACE,2BAAA;If2vDD;Ee5vDD;IACE,kBAAA;If8vDD;Ee/vDD;IACE,2BAAA;IfiwDD;EelwDD;IACE,2BAAA;IfowDD;EerwDD;IACE,kBAAA;IfuwDD;EexwDD;IACE,2BAAA;If0wDD;Ee3wDD;IACE,0BAAA;If6wDD;Ee9wDD;IACE,iBAAA;IfgxDD;EACF;AarwDD;EE9CI;IACE,aAAA;IfszDH;Ee/yDD;IACE,aAAA;IfizDD;EelzDD;IACE,qBAAA;IfozDD;EerzDD;IACE,qBAAA;IfuzDD;EexzDD;IACE,YAAA;If0zDD;Ee3zDD;IACE,qBAAA;If6zDD;Ee9zDD;IACE,qBAAA;Ifg0DD;Eej0DD;IACE,YAAA;Ifm0DD;Eep0DD;IACE,qBAAA;Ifs0DD;Eev0DD;IACE,qBAAA;Ify0DD;Ee10DD;IACE,YAAA;If40DD;Ee70DD;IACE,qBAAA;If+0DD;Eeh1DD;IACE,oBAAA;Ifk1DD;Eep0DD;IACE,aAAA;Ifs0DD;Eev0DD;IACE,qBAAA;Ify0DD;Ee10DD;IACE,qBAAA;If40DD;Ee70DD;IACE,YAAA;If+0DD;Eeh1DD;IACE,qBAAA;Ifk1DD;Een1DD;IACE,qBAAA;Ifq1DD;Eet1DD;IACE,YAAA;Ifw1DD;Eez1DD;IACE,qBAAA;If21DD;Ee51DD;IACE,qBAAA;If81DD;Ee/1DD;IACE,YAAA;Ifi2DD;Eel2DD;IACE,qBAAA;Ifo2DD;Eer2DD;IACE,oBAAA;Ifu2DD;Een2DD;IACE,aAAA;Ifq2DD;Eer3DD;IACE,YAAA;Ifu3DD;Eex3DD;IACE,oBAAA;If03DD;Ee33DD;IACE,oBAAA;If63DD;Ee93DD;IACE,WAAA;Ifg4DD;Eej4DD;IACE,oBAAA;Ifm4DD;Eep4DD;IACE,oBAAA;Ifs4DD;Eev4DD;IACE,WAAA;Ify4DD;Ee14DD;IACE,oBAAA;If44DD;Ee74DD;IACE,oBAAA;If+4DD;Eeh5DD;IACE,WAAA;Ifk5DD;Een5DD;IACE,oBAAA;Ifq5DD;Eet5DD;IACE,mBAAA;Ifw5DD;Eep5DD;IACE,YAAA;Ifs5DD;Eex4DD;IACE,mBAAA;If04DD;Ee34DD;IACE,2BAAA;If64DD;Ee94DD;IACE,2BAAA;Ifg5DD;Eej5DD;IACE,kBAAA;Ifm5DD;Eep5DD;IACE,2BAAA;Ifs5DD;Eev5DD;IACE,2BAAA;Ify5DD;Ee15DD;IACE,kBAAA;If45DD;Ee75DD;IACE,2BAAA;If+5DD;Eeh6DD;IACE,2BAAA;Ifk6DD;Een6DD;IACE,kBAAA;Ifq6DD;Eet6DD;IACE,2BAAA;Ifw6DD;Eez6DD;IACE,0BAAA;If26DD;Ee56DD;IACE,iBAAA;If86DD;EACF;AgBl/DD;EACE,+BAAA;EhBo/DD;AgBl/DD;EACE,kBAAA;EACA,qBAAA;EACA,gBAAA;EACA,kBAAA;EhBo/DD;AgBl/DD;EACE,kBAAA;EhBo/DD;AgB9+DD;EACE,aAAA;EACA,iBAAA;EACA,qBAAA;EhBg/DD;AgBn/DD;;;;;;EAWQ,cAAA;EACA,yBAAA;EACA,qBAAA;EACA,+BAAA;EhBg/DP;AgB9/DD;EAoBI,wBAAA;EACA,kCAAA;EhB6+DH;AgBlgED;;;;;;EA8BQ,eAAA;EhB4+DP;AgB1gED;EAoCI,+BAAA;EhBy+DH;AgB7gED;EAyCI,2BAAA;EhBu+DH;AgBh+DD;;;;;;EAOQ,cAAA;EhBi+DP;AgBt9DD;EACE,2BAAA;EhBw9DD;AgBz9DD;;;;;;EAQQ,2BAAA;EhBy9DP;AgBj+DD;;EAeM,0BAAA;EhBs9DL;AgB58DD;EAEI,2BAAA;EhB68DH;AgBp8DD;EAEI,2BAAA;EhBq8DH;AgB57DD;EACE,kBAAA;EACA,aAAA;EACA,uBAAA;EhB87DD;AgBz7DG;;EACE,kBAAA;EACA,aAAA;EACA,qBAAA;EhB47DL;AiBxkEC;;;;;;;;;;;;EAOI,2BAAA;EjB+kEL;AiBzkEC;;;;;EAMI,2BAAA;EjB0kEL;AiB7lEC;;;;;;;;;;;;EAOI,2BAAA;EjBomEL;AiB9lEC;;;;;EAMI,2BAAA;EjB+lEL;AiBlnEC;;;;;;;;;;;;EAOI,2BAAA;EjBynEL;AiBnnEC;;;;;EAMI,2BAAA;EjBonEL;AiBvoEC;;;;;;;;;;;;EAOI,2BAAA;EjB8oEL;AiBxoEC;;;;;EAMI,2BAAA;EjByoEL;AiB5pEC;;;;;;;;;;;;EAOI,2BAAA;EjBmqEL;AiB7pEC;;;;;EAMI,2BAAA;EjB8pEL;AgB5gED;EACE,kBAAA;EACA,mBAAA;EhB8gED;AgBj9DD;EAAA;IA1DI,aAAA;IACA,qBAAA;IACA,oBAAA;IACA,8CAAA;IACA,2BAAA;IhB+gED;EgBz9DH;IAlDM,kBAAA;IhB8gEH;EgB59DH;;;;;;IAzCY,qBAAA;IhB6gET;EgBp+DH;IAjCM,WAAA;IhBwgEH;EgBv+DH;;;;;;IAxBY,gBAAA;IhBugET;EgB/+DH;;;;;;IApBY,iBAAA;IhB2gET;EgBv/DH;;;;IAPY,kBAAA;IhBogET;EACF;AkB9tED;EACE,YAAA;EACA,WAAA;EACA,WAAA;EAIA,cAAA;ElB6tED;AkB1tED;EACE,gBAAA;EACA,aAAA;EACA,YAAA;EACA,qBAAA;EACA,iBAAA;EACA,sBAAA;EACA,gBAAA;EACA,WAAA;EACA,kCAAA;ElB4tED;AkBztED;EACE,uBAAA;EACA,iBAAA;EACA,oBAAA;EACA,mBAAA;ElB2tED;AkBhtED;Eb4BE,gCAAA;EACG,6BAAA;EACK,wBAAA;ELurET;AkBhtED;;EAEE,iBAAA;EACA,oBAAA;EACA,qBAAA;ElBktED;AkB9sED;EACE,gBAAA;ElBgtED;AkB5sED;EACE,gBAAA;EACA,aAAA;ElB8sED;AkB1sED;;EAEE,cAAA;ElB4sED;AkBxsED;;;EZxEE,sBAAA;EAEA,4CAAA;EACA,sBAAA;ENoxED;AkBxsED;EACE,gBAAA;EACA,kBAAA;EACA,iBAAA;EACA,yBAAA;EACA,gBAAA;ElB0sED;AkBhrED;EACE,gBAAA;EACA,aAAA;EACA,cAAA;EACA,mBAAA;EACA,iBAAA;EACA,yBAAA;EACA,gBAAA;EACA,2BAAA;EACA,wBAAA;EACA,2BAAA;EACA,oBAAA;EbzDA,0DAAA;EACQ,kDAAA;EAyHR,wFAAA;EACK,2EAAA;EACG,wEAAA;ELonET;AmB5vEC;EACE,uBAAA;EACA,YAAA;EdUF,wFAAA;EACQ,gFAAA;ELqvET;AKptEC;EACE,gBAAA;EACA,YAAA;ELstEH;AKptEC;EAA0B,gBAAA;ELutE3B;AKttEC;EAAgC,gBAAA;ELytEjC;AkBxrEC;;;EAGE,qBAAA;EACA,2BAAA;EACA,YAAA;ElB0rEH;AkBtrEC;EACE,cAAA;ElBwrEH;AkB5qED;EACE,0BAAA;ElB8qED;AkB7oED;EArBE;;;;IAIE,mBAAA;IlBqqED;EkBnqED;;;;IAIE,mBAAA;IlBqqED;EkBnqED;;;;IAIE,mBAAA;IlBqqED;EACF;AkB5pED;EACE,qBAAA;ElB8pED;AkBtpED;;EAEE,oBAAA;EACA,gBAAA;EACA,kBAAA;EACA,qBAAA;ElBwpED;AkB7pED;;EAQI,kBAAA;EACA,oBAAA;EACA,kBAAA;EACA,qBAAA;EACA,iBAAA;ElBypEH;AkBtpED;;;;EAIE,oBAAA;EACA,oBAAA;EACA,oBAAA;ElBwpED;AkBrpED;;EAEE,kBAAA;ElBupED;AkBnpED;;EAEE,uBAAA;EACA,oBAAA;EACA,kBAAA;EACA,wBAAA;EACA,qBAAA;EACA,iBAAA;ElBqpED;AkBnpED;;EAEE,eAAA;EACA,mBAAA;ElBqpED;AkB5oEC;;;;;;EAGE,qBAAA;ElBipEH;AkB3oEC;;;;EAEE,qBAAA;ElB+oEH;AkBzoEC;;;;EAGI,qBAAA;ElB4oEL;AkBjoED;EAEE,kBAAA;EACA,qBAAA;EAEA,kBAAA;ElBioED;AkB/nEC;;EAEE,iBAAA;EACA,kBAAA;ElBioEH;AkBvnED;;ECnPE,cAAA;EACA,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,oBAAA;EnB82ED;AmB52EC;;EACE,cAAA;EACA,mBAAA;EnB+2EH;AmB52EC;;;;EAEE,cAAA;EnBg3EH;AkBroED;;ECxPE,cAAA;EACA,oBAAA;EACA,iBAAA;EACA,mBAAA;EACA,oBAAA;EnBi4ED;AmB/3EC;;EACE,cAAA;EACA,mBAAA;EnBk4EH;AmB/3EC;;;;EAEE,cAAA;EnBm4EH;AkB9oED;EAEE,oBAAA;ElB+oED;AkBjpED;EAMI,uBAAA;ElB8oEH;AkB1oED;EACE,oBAAA;EACA,QAAA;EACA,UAAA;EACA,YAAA;EACA,gBAAA;EACA,aAAA;EACA,cAAA;EACA,mBAAA;EACA,oBAAA;EACA,sBAAA;ElB4oED;AkB1oED;EACE,aAAA;EACA,cAAA;EACA,mBAAA;ElB4oED;AkB1oED;EACE,aAAA;EACA,cAAA;EACA,mBAAA;ElB4oED;AkBxoED;;;;;;;;;;ECxVI,gBAAA;EnB4+EH;AkBppED;ECpVI,uBAAA;Ed+CF,0DAAA;EACQ,kDAAA;EL67ET;AmB3+EG;EACE,uBAAA;Ed4CJ,2EAAA;EACQ,mEAAA;ELk8ET;AkB9pED;EC1UI,gBAAA;EACA,uBAAA;EACA,2BAAA;EnB2+EH;AkBnqED;ECpUI,gBAAA;EnB0+EH;AkBnqED;;;;;;;;;;EC3VI,gBAAA;EnB0gFH;AkB/qED;ECvVI,uBAAA;Ed+CF,0DAAA;EACQ,kDAAA;EL29ET;AmBzgFG;EACE,uBAAA;Ed4CJ,2EAAA;EACQ,mEAAA;ELg+ET;AkBzrED;EC7UI,gBAAA;EACA,uBAAA;EACA,2BAAA;EnBygFH;AkB9rED;ECvUI,gBAAA;EnBwgFH;AkB9rED;;;;;;;;;;EC9VI,gBAAA;EnBwiFH;AkB1sED;EC1VI,uBAAA;Ed+CF,0DAAA;EACQ,kDAAA;ELy/ET;AmBviFG;EACE,uBAAA;Ed4CJ,2EAAA;EACQ,mEAAA;EL8/ET;AkBptED;EChVI,gBAAA;EACA,uBAAA;EACA,2BAAA;EnBuiFH;AkBztED;EC1UI,gBAAA;EnBsiFH;AkBrtEC;EACG,WAAA;ElButEJ;AkBrtEC;EACG,QAAA;ElButEJ;AkB7sED;EACE,gBAAA;EACA,iBAAA;EACA,qBAAA;EACA,gBAAA;ElB+sED;AkB3nED;EAAA;IA/DM,uBAAA;IACA,kBAAA;IACA,wBAAA;IlB8rEH;EkBjoEH;IAxDM,uBAAA;IACA,aAAA;IACA,wBAAA;IlB4rEH;EkBtoEH;IAjDM,uBAAA;IlB0rEH;EkBzoEH;IA7CM,uBAAA;IACA,wBAAA;IlByrEH;EkB7oEH;;;IAvCQ,aAAA;IlByrEL;EkBlpEH;IAjCM,aAAA;IlBsrEH;EkBrpEH;IA7BM,kBAAA;IACA,wBAAA;IlBqrEH;EkBzpEH;;IApBM,uBAAA;IACA,eAAA;IACA,kBAAA;IACA,wBAAA;IlBirEH;EkBhqEH;;IAdQ,iBAAA;IlBkrEL;EkBpqEH;;IATM,oBAAA;IACA,gBAAA;IlBirEH;EkBzqEH;IAHM,QAAA;IlB+qEH;EACF;AkBrqED;;;;EASI,eAAA;EACA,kBAAA;EACA,kBAAA;ElBkqEH;AkB7qED;;EAiBI,kBAAA;ElBgqEH;AkBjrED;EJrdE,oBAAA;EACA,qBAAA;EdyoFD;AkBlpEC;EAAA;IANI,mBAAA;IACA,kBAAA;IACA,kBAAA;IlB4pEH;EACF;AkB5rED;EAwCI,aAAA;ElBupEH;AkB1oEC;EAAA;IAHM,qBAAA;IlBipEL;EACF;AkBxoEC;EAAA;IAHM,kBAAA;IlB+oEL;EACF;AoBrqFD;EACE,uBAAA;EACA,kBAAA;EACA,qBAAA;EACA,oBAAA;EACA,wBAAA;EACA,gCAAA;MAAA,4BAAA;EACA,iBAAA;EACA,wBAAA;EACA,+BAAA;EACA,qBAAA;EC6BA,mBAAA;EACA,iBAAA;EACA,yBAAA;EACA,oBAAA;EhB4KA,2BAAA;EACG,wBAAA;EACC,uBAAA;EACI,mBAAA;ELg+ET;AoBxqFG;;;;;;EdrBF,sBAAA;EAEA,4CAAA;EACA,sBAAA;ENosFD;AoB5qFC;;;EAGE,gBAAA;EACA,uBAAA;EpB8qFH;AoB3qFC;;EAEE,YAAA;EACA,wBAAA;Ef2BF,0DAAA;EACQ,kDAAA;ELmpFT;AoB3qFC;;;EAGE,qBAAA;EACA,sBAAA;EE9CF,eAAA;EAGA,2BAAA;EjB8DA,0BAAA;EACQ,kBAAA;EL6pFT;AoBvqFD;ECrDE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErB+tFD;AqB7tFC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;ErB+tFP;AqB7tFC;;;EAGE,wBAAA;ErB+tFH;AqB1tFG;;;;;;;;;;;;;;;;;;EAME,2BAAA;EACI,uBAAA;ErBwuFT;AoBhtFD;ECnBI,gBAAA;EACA,2BAAA;ErBsuFH;AoBjtFD;ECxDE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErB4wFD;AqB1wFC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;ErB4wFP;AqB1wFC;;;EAGE,wBAAA;ErB4wFH;AqBvwFG;;;;;;;;;;;;;;;;;;EAME,2BAAA;EACI,uBAAA;ErBqxFT;AoB1vFD;ECtBI,gBAAA;EACA,2BAAA;ErBmxFH;AoB1vFD;EC5DE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErByzFD;AqBvzFC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;ErByzFP;AqBvzFC;;;EAGE,wBAAA;ErByzFH;AqBpzFG;;;;;;;;;;;;;;;;;;EAME,2BAAA;EACI,uBAAA;ErBk0FT;AoBnyFD;EC1BI,gBAAA;EACA,2BAAA;ErBg0FH;AoBnyFD;EChEE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErBs2FD;AqBp2FC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;ErBs2FP;AqBp2FC;;;EAGE,wBAAA;ErBs2FH;AqBj2FG;;;;;;;;;;;;;;;;;;EAME,2BAAA;EACI,uBAAA;ErB+2FT;AoB50FD;EC9BI,gBAAA;EACA,2BAAA;ErB62FH;AoB50FD;ECpEE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErBm5FD;AqBj5FC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;ErBm5FP;AqBj5FC;;;EAGE,wBAAA;ErBm5FH;AqB94FG;;;;;;;;;;;;;;;;;;EAME,2BAAA;EACI,uBAAA;ErB45FT;AoBr3FD;EClCI,gBAAA;EACA,2BAAA;ErB05FH;AoBr3FD;ECxEE,gBAAA;EACA,2BAAA;EACA,uBAAA;ErBg8FD;AqB97FC;;;;;;EAME,gBAAA;EACA,2BAAA;EACI,uBAAA;ErBg8FP;AqB97FC;;;EAGE,wBAAA;ErBg8FH;AqB37FG;;;;;;;;;;;;;;;;;;EAME,2BAAA;EACI,uBAAA;ErBy8FT;AoB95FD;ECtCI,gBAAA;EACA,2BAAA;ErBu8FH;AoBz5FD;EACE,gBAAA;EACA,qBAAA;EACA,kBAAA;EpB25FD;AoBz5FC;;;;;EAKE,+BAAA;Ef7BF,0BAAA;EACQ,kBAAA;ELy7FT;AoB15FC;;;;EAIE,2BAAA;EpB45FH;AoB15FC;;EAEE,gBAAA;EACA,4BAAA;EACA,+BAAA;EpB45FH;AoBx5FG;;;;EAEE,gBAAA;EACA,uBAAA;EpB45FL;AoBn5FD;;EC/EE,oBAAA;EACA,iBAAA;EACA,mBAAA;EACA,oBAAA;ErBs+FD;AoBt5FD;;ECnFE,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,oBAAA;ErB6+FD;AoBz5FD;;ECvFE,kBAAA;EACA,iBAAA;EACA,kBAAA;EACA,oBAAA;ErBo/FD;AoBx5FD;EACE,gBAAA;EACA,aAAA;EpB05FD;AoBt5FD;EACE,iBAAA;EpBw5FD;AoBj5FC;;;EACE,aAAA;EpBq5FH;AuBziGD;EACE,YAAA;ElBoLA,0CAAA;EACK,qCAAA;EACG,kCAAA;ELw3FT;AuB5iGC;EACE,YAAA;EvB8iGH;AuB1iGD;EACE,eAAA;EACA,oBAAA;EvB4iGD;AuB1iGC;EAAY,gBAAA;EAAgB,qBAAA;EvB8iG7B;AuB7iGC;EAAY,oBAAA;EvBgjGb;AuB/iGC;EAAY,0BAAA;EvBkjGb;AuB/iGD;EACE,oBAAA;EACA,WAAA;EACA,kBAAA;ElBsKA,iDAAA;EACQ,4CAAA;KAAA,yCAAA;EAOR,oCAAA;EACQ,+BAAA;KAAA,4BAAA;EAGR,0CAAA;EACQ,qCAAA;KAAA,kCAAA;ELo4FT;AwB9kGD;EACE,uBAAA;EACA,UAAA;EACA,WAAA;EACA,kBAAA;EACA,wBAAA;EACA,uBAAA;EACA,qCAAA;EACA,oCAAA;ExBglGD;AwB5kGD;EACE,oBAAA;ExB8kGD;AwB1kGD;EACE,YAAA;ExB4kGD;AwBxkGD;EACE,oBAAA;EACA,WAAA;EACA,SAAA;EACA,eAAA;EACA,eAAA;EACA,aAAA;EACA,kBAAA;EACA,gBAAA;EACA,iBAAA;EACA,kBAAA;EACA,iBAAA;EACA,kBAAA;EACA,2BAAA;EACA,2BAAA;EACA,uCAAA;EACA,oBAAA;EnBwBA,qDAAA;EACQ,6CAAA;EmBvBR,sCAAA;UAAA,8BAAA;ExB2kGD;AwBtkGC;EACE,UAAA;EACA,YAAA;ExBwkGH;AwBjmGD;ECvBE,aAAA;EACA,eAAA;EACA,kBAAA;EACA,2BAAA;EzB2nGD;AwBvmGD;EAmCI,gBAAA;EACA,mBAAA;EACA,aAAA;EACA,qBAAA;EACA,yBAAA;EACA,gBAAA;EACA,qBAAA;ExBukGH;AwBjkGC;;EAEE,uBAAA;EACA,gBAAA;EACA,2BAAA;ExBmkGH;AwB7jGC;;;EAGE,gBAAA;EACA,uBAAA;EACA,YAAA;EACA,2BAAA;ExB+jGH;AwBtjGC;;;EAGE,gBAAA;ExBwjGH;AwBpjGC;;EAEE,uBAAA;EACA,+BAAA;EACA,wBAAA;EEzGF,qEAAA;EF2GE,qBAAA;ExBsjGH;AwBjjGD;EAGI,gBAAA;ExBijGH;AwBpjGD;EAQI,YAAA;ExB+iGH;AwBviGD;EACE,YAAA;EACA,UAAA;ExByiGD;AwBjiGD;EACE,SAAA;EACA,aAAA;ExBmiGD;AwB/hGD;EACE,gBAAA;EACA,mBAAA;EACA,iBAAA;EACA,yBAAA;EACA,gBAAA;EACA,qBAAA;ExBiiGD;AwB7hGD;EACE,iBAAA;EACA,SAAA;EACA,UAAA;EACA,WAAA;EACA,QAAA;EACA,cAAA;ExB+hGD;AwB3hGD;EACE,UAAA;EACA,YAAA;ExB6hGD;AwBrhGD;;EAII,eAAA;EACA,0BAAA;EACA,aAAA;ExBqhGH;AwB3hGD;;EAUI,WAAA;EACA,cAAA;EACA,oBAAA;ExBqhGH;AwBhgGD;EAXE;IAnEA,YAAA;IACA,UAAA;IxBklGC;EwBhhGD;IAzDA,SAAA;IACA,aAAA;IxB4kGC;EACF;A2B1tGD;;EAEE,oBAAA;EACA,uBAAA;EACA,wBAAA;E3B4tGD;A2BhuGD;;EAMI,oBAAA;EACA,aAAA;E3B8tGH;A2B5tGG;;;;;;;;EAIE,YAAA;E3BkuGL;A2B5tGD;;;;EAKI,mBAAA;E3B6tGH;A2BxtGD;EACE,mBAAA;E3B0tGD;A2B3tGD;;EAMI,aAAA;E3BytGH;A2B/tGD;;;EAWI,kBAAA;E3BytGH;A2BrtGD;EACE,kBAAA;E3ButGD;A2BntGD;EACE,gBAAA;E3BqtGD;A2BptGC;ECjDA,+BAAA;EACG,4BAAA;E5BwwGJ;A2BntGD;;EC9CE,8BAAA;EACG,2BAAA;E5BqwGJ;A2BltGD;EACE,aAAA;E3BotGD;A2BltGD;EACE,kBAAA;E3BotGD;A2BltGD;;EClEE,+BAAA;EACG,4BAAA;E5BwxGJ;A2BjtGD;EChEE,8BAAA;EACG,2BAAA;E5BoxGJ;A2BhtGD;;EAEE,YAAA;E3BktGD;A2BjsGD;EACE,mBAAA;EACA,oBAAA;E3BmsGD;A2BjsGD;EACE,oBAAA;EACA,qBAAA;E3BmsGD;A2B9rGD;EtB9CE,0DAAA;EACQ,kDAAA;EL+uGT;A2B9rGC;EtBlDA,0BAAA;EACQ,kBAAA;ELmvGT;A2B3rGD;EACE,gBAAA;E3B6rGD;A2B1rGD;EACE,yBAAA;EACA,wBAAA;E3B4rGD;A2BzrGD;EACE,yBAAA;E3B2rGD;A2BprGD;;;EAII,gBAAA;EACA,aAAA;EACA,aAAA;EACA,iBAAA;E3BqrGH;A2B5rGD;EAcM,aAAA;E3BirGL;A2B/rGD;;;;EAsBI,kBAAA;EACA,gBAAA;E3B+qGH;A2B1qGC;EACE,kBAAA;E3B4qGH;A2B1qGC;EACE,8BAAA;ECnKF,+BAAA;EACC,8BAAA;E5Bg1GF;A2B3qGC;EACE,gCAAA;EC/KF,4BAAA;EACC,2BAAA;E5B61GF;A2B3qGD;EACE,kBAAA;E3B6qGD;A2B3qGD;;EC9KE,+BAAA;EACC,8BAAA;E5B61GF;A2B1qGD;EC5LE,4BAAA;EACC,2BAAA;E5By2GF;A2BtqGD;EACE,gBAAA;EACA,aAAA;EACA,qBAAA;EACA,2BAAA;E3BwqGD;A2B5qGD;;EAOI,aAAA;EACA,qBAAA;EACA,WAAA;E3ByqGH;A2BlrGD;EAYI,aAAA;E3ByqGH;A2BrrGD;EAgBI,YAAA;E3BwqGH;A2BvpGD;;;;EAKM,oBAAA;EACA,wBAAA;EACA,sBAAA;E3BwpGL;A6Bj4GD;EACE,oBAAA;EACA,gBAAA;EACA,2BAAA;E7Bm4GD;A6Bh4GC;EACE,aAAA;EACA,iBAAA;EACA,kBAAA;E7Bk4GH;A6B34GD;EAeI,oBAAA;EACA,YAAA;EAKA,aAAA;EAEA,aAAA;EACA,kBAAA;E7B03GH;A6Bj3GD;;;EV8BE,cAAA;EACA,oBAAA;EACA,iBAAA;EACA,mBAAA;EACA,oBAAA;EnBw1GD;AmBt1GC;;;EACE,cAAA;EACA,mBAAA;EnB01GH;AmBv1GC;;;;;;EAEE,cAAA;EnB61GH;A6Bn4GD;;;EVyBE,cAAA;EACA,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,oBAAA;EnB+2GD;AmB72GC;;;EACE,cAAA;EACA,mBAAA;EnBi3GH;AmB92GC;;;;;;EAEE,cAAA;EnBo3GH;A6Bj5GD;;;EAGE,qBAAA;E7Bm5GD;A6Bj5GC;;;EACE,kBAAA;E7Bq5GH;A6Bj5GD;;EAEE,WAAA;EACA,qBAAA;EACA,wBAAA;E7Bm5GD;A6B94GD;EACE,mBAAA;EACA,iBAAA;EACA,qBAAA;EACA,gBAAA;EACA,gBAAA;EACA,oBAAA;EACA,2BAAA;EACA,2BAAA;EACA,oBAAA;E7Bg5GD;A6B74GC;EACE,mBAAA;EACA,iBAAA;EACA,oBAAA;E7B+4GH;A6B74GC;EACE,oBAAA;EACA,iBAAA;EACA,oBAAA;E7B+4GH;A6Bn6GD;;EA0BI,eAAA;E7B64GH;A6Bx4GD;;;;;;;EDhGE,+BAAA;EACG,4BAAA;E5Bi/GJ;A6Bz4GD;EACE,iBAAA;E7B24GD;A6Bz4GD;;;;;;;EDpGE,8BAAA;EACG,2BAAA;E5Bs/GJ;A6B14GD;EACE,gBAAA;E7B44GD;A6Bv4GD;EACE,oBAAA;EAGA,cAAA;EACA,qBAAA;E7Bu4GD;A6B54GD;EAUI,oBAAA;E7Bq4GH;A6B/4GD;EAYM,mBAAA;E7Bs4GL;A6Bn4GG;;;EAGE,YAAA;E7Bq4GL;A6Bh4GC;;EAGI,oBAAA;E7Bi4GL;A6B93GC;;EAGI,mBAAA;E7B+3GL;A8BzhHD;EACE,kBAAA;EACA,iBAAA;EACA,kBAAA;E9B2hHD;A8B9hHD;EAOI,oBAAA;EACA,gBAAA;E9B0hHH;A8BliHD;EAWM,oBAAA;EACA,gBAAA;EACA,oBAAA;E9B0hHL;A8BzhHK;;EAEE,uBAAA;EACA,2BAAA;E9B2hHP;A8BthHG;EACE,gBAAA;E9BwhHL;A8BthHK;;EAEE,gBAAA;EACA,uBAAA;EACA,+BAAA;EACA,qBAAA;E9BwhHP;A8BjhHG;;;EAGE,2BAAA;EACA,uBAAA;E9BmhHL;A8B5jHD;ELHE,aAAA;EACA,eAAA;EACA,kBAAA;EACA,2BAAA;EzBkkHD;A8BlkHD;EA0DI,iBAAA;E9B2gHH;A8BlgHD;EACE,kCAAA;E9BogHD;A8BrgHD;EAGI,aAAA;EAEA,qBAAA;E9BogHH;A8BzgHD;EASM,mBAAA;EACA,yBAAA;EACA,+BAAA;EACA,4BAAA;E9BmgHL;A8BlgHK;EACE,uCAAA;E9BogHP;A8B9/GK;;;EAGE,gBAAA;EACA,2BAAA;EACA,2BAAA;EACA,kCAAA;EACA,iBAAA;E9BggHP;A8B3/GC;EAqDA,aAAA;EA8BA,kBAAA;E9B46GD;A8B//GC;EAwDE,aAAA;E9B08GH;A8BlgHC;EA0DI,oBAAA;EACA,oBAAA;E9B28GL;A8BtgHC;EAgEE,WAAA;EACA,YAAA;E9By8GH;A8B77GD;EAAA;IAPM,qBAAA;IACA,WAAA;I9Bw8GH;E8Bl8GH;IAJQ,kBAAA;I9By8GL;EACF;A8BnhHC;EAuFE,iBAAA;EACA,oBAAA;E9B+7GH;A8BvhHC;;;EA8FE,2BAAA;E9B87GH;A8Bh7GD;EAAA;IATM,kCAAA;IACA,4BAAA;I9B67GH;E8Br7GH;;;IAHM,8BAAA;I9B67GH;EACF;A8B9hHD;EAEI,aAAA;E9B+hHH;A8BjiHD;EAMM,oBAAA;E9B8hHL;A8BpiHD;EASM,kBAAA;E9B8hHL;A8BzhHK;;;EAGE,gBAAA;EACA,2BAAA;E9B2hHP;A8BnhHD;EAEI,aAAA;E9BohHH;A8BthHD;EAIM,iBAAA;EACA,gBAAA;E9BqhHL;A8BzgHD;EACE,aAAA;E9B2gHD;A8B5gHD;EAII,aAAA;E9B2gHH;A8B/gHD;EAMM,oBAAA;EACA,oBAAA;E9B4gHL;A8BnhHD;EAYI,WAAA;EACA,YAAA;E9B0gHH;A8B9/GD;EAAA;IAPM,qBAAA;IACA,WAAA;I9BygHH;E8BngHH;IAJQ,kBAAA;I9B0gHL;EACF;A8BlgHD;EACE,kBAAA;E9BogHD;A8BrgHD;EAKI,iBAAA;EACA,oBAAA;E9BmgHH;A8BzgHD;;;EAYI,2BAAA;E9BkgHH;A8Bp/GD;EAAA;IATM,kCAAA;IACA,4BAAA;I9BigHH;E8Bz/GH;;;IAHM,8BAAA;I9BigHH;EACF;A8Bx/GD;EAEI,eAAA;EACA,oBAAA;E9By/GH;A8B5/GD;EAMI,gBAAA;EACA,qBAAA;E9By/GH;A8Bh/GD;EAEE,kBAAA;EF7OA,4BAAA;EACC,2BAAA;E5B+tHF;A+BztHD;EACE,oBAAA;EACA,kBAAA;EACA,qBAAA;EACA,+BAAA;E/B2tHD;A+BntHD;EAAA;IAFI,oBAAA;I/BytHD;EACF;A+B1sHD;EAAA;IAFI,aAAA;I/BgtHD;EACF;A+BlsHD;EACE,qBAAA;EACA,qBAAA;EACA,oBAAA;EACA,mCAAA;EACA,4DAAA;UAAA,oDAAA;EAEA,mCAAA;E/BmsHD;A+BjsHC;EACE,kBAAA;E/BmsHH;A+BtqHD;EAAA;IAzBI,aAAA;IACA,eAAA;IACA,0BAAA;YAAA,kBAAA;I/BmsHD;E+BjsHC;IACE,2BAAA;IACA,gCAAA;IACA,yBAAA;IACA,mBAAA;IACA,8BAAA;I/BmsHH;E+BhsHC;IACE,qBAAA;I/BksHH;E+B7rHC;;;IAGE,iBAAA;IACA,kBAAA;I/B+rHH;EACF;A+B3rHD;;EAGI,mBAAA;E/B4rHH;A+BvrHC;EAAA;;IAFI,mBAAA;I/B8rHH;EACF;A+BrrHD;;;;EAII,qBAAA;EACA,oBAAA;E/BurHH;A+BjrHC;EAAA;;;;IAHI,iBAAA;IACA,gBAAA;I/B2rHH;EACF;A+B/qHD;EACE,eAAA;EACA,uBAAA;E/BirHD;A+B5qHD;EAAA;IAFI,kBAAA;I/BkrHD;EACF;A+B9qHD;;EAEE,iBAAA;EACA,UAAA;EACA,SAAA;EACA,eAAA;E/BgrHD;A+B1qHD;EAAA;;IAFI,kBAAA;I/BirHD;EACF;A+B/qHD;EACE,QAAA;EACA,uBAAA;E/BirHD;A+B/qHD;EACE,WAAA;EACA,kBAAA;EACA,uBAAA;E/BirHD;A+B3qHD;EACE,aAAA;EACA,oBAAA;EACA,iBAAA;EACA,mBAAA;EACA,cAAA;E/B6qHD;A+B3qHC;;EAEE,uBAAA;E/B6qHH;A+BtrHD;EAaI,gBAAA;E/B4qHH;A+BnqHD;EALI;;IAEE,oBAAA;I/B2qHH;EACF;A+BjqHD;EACE,oBAAA;EACA,cAAA;EACA,oBAAA;EACA,mBAAA;EC/LA,iBAAA;EACA,oBAAA;EDgMA,+BAAA;EACA,wBAAA;EACA,+BAAA;EACA,oBAAA;E/BoqHD;A+BhqHC;EACE,YAAA;E/BkqHH;A+BhrHD;EAmBI,gBAAA;EACA,aAAA;EACA,aAAA;EACA,oBAAA;E/BgqHH;A+BtrHD;EAyBI,iBAAA;E/BgqHH;A+B1pHD;EAAA;IAFI,eAAA;I/BgqHD;EACF;A+BvpHD;EACE,qBAAA;E/BypHD;A+B1pHD;EAII,mBAAA;EACA,sBAAA;EACA,mBAAA;E/BypHH;A+B9nHC;EAAA;IArBI,kBAAA;IACA,aAAA;IACA,aAAA;IACA,eAAA;IACA,+BAAA;IACA,WAAA;IACA,0BAAA;YAAA,kBAAA;I/BupHH;E+BxoHD;;IAZM,4BAAA;I/BwpHL;E+B5oHD;IATM,mBAAA;I/BwpHL;E+BvpHK;;IAEE,wBAAA;I/BypHP;EACF;A+BvoHD;EAAA;IAXI,aAAA;IACA,WAAA;I/BspHD;E+B5oHH;IAPM,aAAA;I/BspHH;E+B/oHH;IALQ,mBAAA;IACA,sBAAA;I/BupHL;EACF;A+B5oHD;EACE,oBAAA;EACA,qBAAA;EACA,oBAAA;EACA,mCAAA;EACA,sCAAA;E1B/NA,8FAAA;EACQ,sFAAA;E2B/DR,iBAAA;EACA,oBAAA;EhC86HD;AkBz9GD;EAAA;IA/DM,uBAAA;IACA,kBAAA;IACA,wBAAA;IlB4hHH;EkB/9GH;IAxDM,uBAAA;IACA,aAAA;IACA,wBAAA;IlB0hHH;EkBp+GH;IAjDM,uBAAA;IlBwhHH;EkBv+GH;IA7CM,uBAAA;IACA,wBAAA;IlBuhHH;EkB3+GH;;;IAvCQ,aAAA;IlBuhHL;EkBh/GH;IAjCM,aAAA;IlBohHH;EkBn/GH;IA7BM,kBAAA;IACA,wBAAA;IlBmhHH;EkBv/GH;;IApBM,uBAAA;IACA,eAAA;IACA,kBAAA;IACA,wBAAA;IlB+gHH;EkB9/GH;;IAdQ,iBAAA;IlBghHL;EkBlgHH;;IATM,oBAAA;IACA,gBAAA;IlB+gHH;EkBvgHH;IAHM,QAAA;IlB6gHH;EACF;A+BrrHC;EAAA;IANI,oBAAA;I/B+rHH;E+B7rHG;IACE,kBAAA;I/B+rHL;EACF;A+B9qHD;EAAA;IARI,aAAA;IACA,WAAA;IACA,gBAAA;IACA,iBAAA;IACA,gBAAA;IACA,mBAAA;I1B1PF,0BAAA;IACQ,kBAAA;ILq7HP;EACF;A+BprHD;EACE,eAAA;EHrUA,4BAAA;EACC,2BAAA;E5B4/HF;A+BprHD;EHzUE,8BAAA;EACC,6BAAA;EAOD,+BAAA;EACC,8BAAA;E5B0/HF;A+BhrHD;EChVE,iBAAA;EACA,oBAAA;EhCmgID;A+BjrHC;ECnVA,kBAAA;EACA,qBAAA;EhCugID;A+BlrHC;ECtVA,kBAAA;EACA,qBAAA;EhC2gID;A+B5qHD;EChWE,kBAAA;EACA,qBAAA;EhC+gID;A+BxqHD;EAAA;IAJI,aAAA;IACA,mBAAA;IACA,oBAAA;I/BgrHD;EACF;A+BvpHD;EAZE;IExWA,wBAAA;IjC+gIC;E+BtqHD;IE5WA,yBAAA;IF8WE,qBAAA;I/BwqHD;E+B1qHD;IAKI,iBAAA;I/BwqHH;EACF;A+B/pHD;EACE,2BAAA;EACA,uBAAA;E/BiqHD;A+BnqHD;EAKI,gBAAA;E/BiqHH;A+BhqHG;;EAEE,gBAAA;EACA,+BAAA;E/BkqHL;A+B3qHD;EAcI,gBAAA;E/BgqHH;A+B9qHD;EAmBM,gBAAA;E/B8pHL;A+B5pHK;;EAEE,gBAAA;EACA,+BAAA;E/B8pHP;A+B1pHK;;;EAGE,gBAAA;EACA,2BAAA;E/B4pHP;A+BxpHK;;;EAGE,gBAAA;EACA,+BAAA;E/B0pHP;A+BlsHD;EA8CI,uBAAA;E/BupHH;A+BtpHG;;EAEE,2BAAA;E/BwpHL;A+BzsHD;EAoDM,2BAAA;E/BwpHL;A+B5sHD;;EA0DI,uBAAA;E/BspHH;A+B/oHK;;;EAGE,2BAAA;EACA,gBAAA;E/BipHP;A+BhnHC;EAAA;IAzBQ,gBAAA;I/B6oHP;E+B5oHO;;IAEE,gBAAA;IACA,+BAAA;I/B8oHT;E+B1oHO;;;IAGE,gBAAA;IACA,2BAAA;I/B4oHT;E+BxoHO;;;IAGE,gBAAA;IACA,+BAAA;I/B0oHT;EACF;A+B5uHD;EA8GI,gBAAA;E/BioHH;A+BhoHG;EACE,gBAAA;E/BkoHL;A+BlvHD;EAqHI,gBAAA;E/BgoHH;A+B/nHG;;EAEE,gBAAA;E/BioHL;A+B7nHK;;;;EAEE,gBAAA;E/BioHP;A+BznHD;EACE,2BAAA;EACA,uBAAA;E/B2nHD;A+B7nHD;EAKI,gBAAA;E/B2nHH;A+B1nHG;;EAEE,gBAAA;EACA,+BAAA;E/B4nHL;A+BroHD;EAcI,gBAAA;E/B0nHH;A+BxoHD;EAmBM,gBAAA;E/BwnHL;A+BtnHK;;EAEE,gBAAA;EACA,+BAAA;E/BwnHP;A+BpnHK;;;EAGE,gBAAA;EACA,2BAAA;E/BsnHP;A+BlnHK;;;EAGE,gBAAA;EACA,+BAAA;E/BonHP;A+B5pHD;EA+CI,uBAAA;E/BgnHH;A+B/mHG;;EAEE,2BAAA;E/BinHL;A+BnqHD;EAqDM,2BAAA;E/BinHL;A+BtqHD;;EA2DI,uBAAA;E/B+mHH;A+BzmHK;;;EAGE,2BAAA;EACA,gBAAA;E/B2mHP;A+BpkHC;EAAA;IA/BQ,uBAAA;I/BumHP;E+BxkHD;IA5BQ,2BAAA;I/BumHP;E+B3kHD;IAzBQ,gBAAA;I/BumHP;E+BtmHO;;IAEE,gBAAA;IACA,+BAAA;I/BwmHT;E+BpmHO;;;IAGE,gBAAA;IACA,2BAAA;I/BsmHT;E+BlmHO;;;IAGE,gBAAA;IACA,+BAAA;I/BomHT;EACF;A+B5sHD;EA+GI,gBAAA;E/BgmHH;A+B/lHG;EACE,gBAAA;E/BimHL;A+BltHD;EAsHI,gBAAA;E/B+lHH;A+B9lHG;;EAEE,gBAAA;E/BgmHL;A+B5lHK;;;;EAEE,gBAAA;E/BgmHP;AkC1uID;EACE,mBAAA;EACA,qBAAA;EACA,kBAAA;EACA,2BAAA;EACA,oBAAA;ElC4uID;AkCjvID;EAQI,uBAAA;ElC4uIH;AkCpvID;EAWM,mBAAA;EACA,gBAAA;EACA,gBAAA;ElC4uIL;AkCzvID;EAkBI,gBAAA;ElC0uIH;AmC9vID;EACE,uBAAA;EACA,iBAAA;EACA,gBAAA;EACA,oBAAA;EnCgwID;AmCpwID;EAOI,iBAAA;EnCgwIH;AmCvwID;;EAUM,oBAAA;EACA,aAAA;EACA,mBAAA;EACA,yBAAA;EACA,uBAAA;EACA,gBAAA;EACA,2BAAA;EACA,2BAAA;EACA,mBAAA;EnCiwIL;AmC/vIG;;EAGI,gBAAA;EPXN,gCAAA;EACG,6BAAA;E5B4wIJ;AmC9vIG;;EPvBF,iCAAA;EACG,8BAAA;E5ByxIJ;AmCzvIG;;;;EAEE,gBAAA;EACA,2BAAA;EACA,uBAAA;EnC6vIL;AmCvvIG;;;;;;EAGE,YAAA;EACA,gBAAA;EACA,2BAAA;EACA,uBAAA;EACA,iBAAA;EnC4vIL;AmClzID;;;;;;EAiEM,gBAAA;EACA,2BAAA;EACA,uBAAA;EACA,qBAAA;EnCyvIL;AmChvID;;EC1EM,oBAAA;EACA,iBAAA;EpC8zIL;AoC5zIG;;ERMF,gCAAA;EACG,6BAAA;E5B0zIJ;AoC3zIG;;ERRF,iCAAA;EACG,8BAAA;E5Bu0IJ;AmC1vID;;EC/EM,mBAAA;EACA,iBAAA;EpC60IL;AoC30IG;;ERMF,gCAAA;EACG,6BAAA;E5By0IJ;AoC10IG;;ERRF,iCAAA;EACG,8BAAA;E5Bs1IJ;AqCz1ID;EACE,iBAAA;EACA,gBAAA;EACA,kBAAA;EACA,oBAAA;ErC21ID;AqC/1ID;EAOI,iBAAA;ErC21IH;AqCl2ID;;EAUM,uBAAA;EACA,mBAAA;EACA,2BAAA;EACA,2BAAA;EACA,qBAAA;ErC41IL;AqC12ID;;EAmBM,uBAAA;EACA,2BAAA;ErC21IL;AqC/2ID;;EA2BM,cAAA;ErCw1IL;AqCn3ID;;EAkCM,aAAA;ErCq1IL;AqCv3ID;;;;EA2CM,gBAAA;EACA,2BAAA;EACA,qBAAA;ErCk1IL;AsCh4ID;EACE,iBAAA;EACA,yBAAA;EACA,gBAAA;EACA,mBAAA;EACA,gBAAA;EACA,gBAAA;EACA,oBAAA;EACA,qBAAA;EACA,0BAAA;EACA,sBAAA;EtCk4ID;AsC93IG;;EAEE,gBAAA;EACA,uBAAA;EACA,iBAAA;EtCg4IL;AsC33IC;EACE,eAAA;EtC63IH;AsCz3IC;EACE,oBAAA;EACA,WAAA;EtC23IH;AsCp3ID;ECtCE,2BAAA;EvC65ID;AuC15IG;;EAEE,2BAAA;EvC45IL;AsCv3ID;EC1CE,2BAAA;EvCo6ID;AuCj6IG;;EAEE,2BAAA;EvCm6IL;AsC13ID;EC9CE,2BAAA;EvC26ID;AuCx6IG;;EAEE,2BAAA;EvC06IL;AsC73ID;EClDE,2BAAA;EvCk7ID;AuC/6IG;;EAEE,2BAAA;EvCi7IL;AsCh4ID;ECtDE,2BAAA;EvCy7ID;AuCt7IG;;EAEE,2BAAA;EvCw7IL;AsCn4ID;EC1DE,2BAAA;EvCg8ID;AuC77IG;;EAEE,2BAAA;EvC+7IL;AwCj8ID;EACE,uBAAA;EACA,iBAAA;EACA,kBAAA;EACA,iBAAA;EACA,mBAAA;EACA,gBAAA;EACA,gBAAA;EACA,0BAAA;EACA,qBAAA;EACA,oBAAA;EACA,2BAAA;EACA,qBAAA;ExCm8ID;AwCh8IC;EACE,eAAA;ExCk8IH;AwC97IC;EACE,oBAAA;EACA,WAAA;ExCg8IH;AwC97IC;EACE,QAAA;EACA,kBAAA;ExCg8IH;AwC37IG;;EAEE,gBAAA;EACA,uBAAA;EACA,iBAAA;ExC67IL;AwCx7IC;;EAEE,gBAAA;EACA,2BAAA;ExC07IH;AwCx7IC;EACE,cAAA;ExC07IH;AwCx7IC;EACE,mBAAA;ExC07IH;AwCx7IC;EACE,kBAAA;ExC07IH;AyC/+ID;EACE,oBAAA;EACA,qBAAA;EACA,gBAAA;EACA,2BAAA;EzCi/ID;AyCr/ID;;EAQI,gBAAA;EzCi/IH;AyCz/ID;EAWI,qBAAA;EACA,iBAAA;EACA,kBAAA;EzCi/IH;AyC9/ID;EAiBI,2BAAA;EzCg/IH;AyC7+IC;;EAEE,oBAAA;EzC++IH;AyCrgJD;EA0BI,iBAAA;EzC8+IH;AyC79ID;EAAA;IAbI,iBAAA;IzC8+ID;EyC5+IC;;IAEE,oBAAA;IACA,qBAAA;IzC8+IH;EyCt+IH;;IAHM,iBAAA;IzC6+IH;EACF;A0CrhJD;EACE,gBAAA;EACA,cAAA;EACA,qBAAA;EACA,yBAAA;EACA,2BAAA;EACA,2BAAA;EACA,oBAAA;ErCiLA,6CAAA;EACK,wCAAA;EACG,qCAAA;ELu2IT;A0CjiJD;;EAaI,mBAAA;EACA,oBAAA;E1CwhJH;A0CphJC;;;EAGE,uBAAA;E1CshJH;A0C3iJD;EA0BI,cAAA;EACA,gBAAA;E1CohJH;A2C7iJD;EACE,eAAA;EACA,qBAAA;EACA,+BAAA;EACA,oBAAA;E3C+iJD;A2CnjJD;EAQI,eAAA;EAEA,gBAAA;E3C6iJH;A2CvjJD;EAcI,mBAAA;E3C4iJH;A2C1jJD;;EAoBI,kBAAA;E3C0iJH;A2C9jJD;EAuBI,iBAAA;E3C0iJH;A2CliJD;;EAEE,qBAAA;E3CoiJD;A2CtiJD;;EAMI,oBAAA;EACA,WAAA;EACA,cAAA;EACA,gBAAA;E3CoiJH;A2C5hJD;ECrDE,2BAAA;EACA,uBAAA;EACA,gBAAA;E5ColJD;A2CjiJD;EChDI,2BAAA;E5ColJH;A2CpiJD;EC7CI,gBAAA;E5ColJH;A2CpiJD;ECxDE,2BAAA;EACA,uBAAA;EACA,gBAAA;E5C+lJD;A2CziJD;ECnDI,2BAAA;E5C+lJH;A2C5iJD;EChDI,gBAAA;E5C+lJH;A2C5iJD;EC3DE,2BAAA;EACA,uBAAA;EACA,gBAAA;E5C0mJD;A2CjjJD;ECtDI,2BAAA;E5C0mJH;A2CpjJD;ECnDI,gBAAA;E5C0mJH;A2CpjJD;EC9DE,2BAAA;EACA,uBAAA;EACA,gBAAA;E5CqnJD;A2CzjJD;ECzDI,2BAAA;E5CqnJH;A2C5jJD;ECtDI,gBAAA;E5CqnJH;A6CvnJD;EACE;IAAQ,6BAAA;I7C0nJP;E6CznJD;IAAQ,0BAAA;I7C4nJP;EACF;A6CznJD;EACE;IAAQ,6BAAA;I7C4nJP;E6C3nJD;IAAQ,0BAAA;I7C8nJP;EACF;A6CjoJD;EACE;IAAQ,6BAAA;I7C4nJP;E6C3nJD;IAAQ,0BAAA;I7C8nJP;EACF;A6CvnJD;EACE,kBAAA;EACA,cAAA;EACA,qBAAA;EACA,2BAAA;EACA,oBAAA;ExCsCA,wDAAA;EACQ,gDAAA;ELolJT;A6CtnJD;EACE,aAAA;EACA,WAAA;EACA,cAAA;EACA,iBAAA;EACA,mBAAA;EACA,gBAAA;EACA,oBAAA;EACA,2BAAA;ExCyBA,wDAAA;EACQ,gDAAA;EAyHR,qCAAA;EACK,gCAAA;EACG,6BAAA;ELw+IT;A6CnnJD;;ECCI,+MAAA;EACA,0MAAA;EACA,uMAAA;EDAF,oCAAA;UAAA,4BAAA;E7CunJD;A6ChnJD;;ExC5CE,4DAAA;EACK,uDAAA;EACG,oDAAA;ELgqJT;A6C7mJD;EErEE,2BAAA;E/CqrJD;A+ClrJC;EDgDE,+MAAA;EACA,0MAAA;EACA,uMAAA;E9CqoJH;A6CjnJD;EEzEE,2BAAA;E/C6rJD;A+C1rJC;EDgDE,+MAAA;EACA,0MAAA;EACA,uMAAA;E9C6oJH;A6CrnJD;EE7EE,2BAAA;E/CqsJD;A+ClsJC;EDgDE,+MAAA;EACA,0MAAA;EACA,uMAAA;E9CqpJH;A6CznJD;EEjFE,2BAAA;E/C6sJD;A+C1sJC;EDgDE,+MAAA;EACA,0MAAA;EACA,uMAAA;E9C6pJH;AgDrtJD;EAEE,kBAAA;EhDstJD;AgDptJC;EACE,eAAA;EhDstJH;AgDltJD;;EAEE,oBAAA;EhDotJD;AgDjtJD;;EAEE,qBAAA;EhDmtJD;AgDhtJD;;;EAGE,qBAAA;EACA,qBAAA;EhDktJD;AgD/sJD;EACE,wBAAA;EhDitJD;AgD9sJD;EACE,wBAAA;EhDgtJD;AgD5sJD;EACE,eAAA;EACA,oBAAA;EhD8sJD;AgDxsJD;EACE,iBAAA;EACA,kBAAA;EhD0sJD;AiD9uJD;EAEE,qBAAA;EACA,iBAAA;EjD+uJD;AiDvuJD;EACE,oBAAA;EACA,gBAAA;EACA,oBAAA;EAEA,qBAAA;EACA,2BAAA;EACA,2BAAA;EjDwuJD;AiDruJC;ErB3BA,8BAAA;EACC,6BAAA;E5BmwJF;AiDtuJC;EACE,kBAAA;ErBvBF,iCAAA;EACC,gCAAA;E5BgwJF;AiD/tJD;EACE,gBAAA;EjDiuJD;AiDluJD;EAII,gBAAA;EjDiuJH;AiD7tJC;;EAEE,uBAAA;EACA,gBAAA;EACA,2BAAA;EjD+tJH;AiDztJC;;;EAGE,2BAAA;EACA,gBAAA;EACA,qBAAA;EjD2tJH;AiDhuJC;;;EASI,gBAAA;EjD4tJL;AiDruJC;;;EAYI,gBAAA;EjD8tJL;AiDztJC;;;EAGE,YAAA;EACA,gBAAA;EACA,2BAAA;EACA,uBAAA;EjD2tJH;AiDjuJC;;;;;;;;;EAYI,gBAAA;EjDguJL;AiD5uJC;;;EAeI,gBAAA;EjDkuJL;AkD9zJC;EACE,gBAAA;EACA,2BAAA;ElDg0JH;AkD9zJG;EACE,gBAAA;ElDg0JL;AkDj0JG;EAII,gBAAA;ElDg0JP;AkD7zJK;;EAEE,gBAAA;EACA,2BAAA;ElD+zJP;AkD7zJK;;;EAGE,aAAA;EACA,2BAAA;EACA,uBAAA;ElD+zJP;AkDp1JC;EACE,gBAAA;EACA,2BAAA;ElDs1JH;AkDp1JG;EACE,gBAAA;ElDs1JL;AkDv1JG;EAII,gBAAA;ElDs1JP;AkDn1JK;;EAEE,gBAAA;EACA,2BAAA;ElDq1JP;AkDn1JK;;;EAGE,aAAA;EACA,2BAAA;EACA,uBAAA;ElDq1JP;AkD12JC;EACE,gBAAA;EACA,2BAAA;ElD42JH;AkD12JG;EACE,gBAAA;ElD42JL;AkD72JG;EAII,gBAAA;ElD42JP;AkDz2JK;;EAEE,gBAAA;EACA,2BAAA;ElD22JP;AkDz2JK;;;EAGE,aAAA;EACA,2BAAA;EACA,uBAAA;ElD22JP;AkDh4JC;EACE,gBAAA;EACA,2BAAA;ElDk4JH;AkDh4JG;EACE,gBAAA;ElDk4JL;AkDn4JG;EAII,gBAAA;ElDk4JP;AkD/3JK;;EAEE,gBAAA;EACA,2BAAA;ElDi4JP;AkD/3JK;;;EAGE,aAAA;EACA,2BAAA;EACA,uBAAA;ElDi4JP;AiDryJD;EACE,eAAA;EACA,oBAAA;EjDuyJD;AiDryJD;EACE,kBAAA;EACA,kBAAA;EjDuyJD;AmD35JD;EACE,qBAAA;EACA,2BAAA;EACA,+BAAA;EACA,oBAAA;E9C0DA,mDAAA;EACQ,2CAAA;ELo2JT;AmD15JD;EACE,eAAA;EnD45JD;AmDv5JD;EACE,oBAAA;EACA,sCAAA;EvBpBA,8BAAA;EACC,6BAAA;E5B86JF;AmD75JD;EAMI,gBAAA;EnD05JH;AmDr5JD;EACE,eAAA;EACA,kBAAA;EACA,iBAAA;EACA,gBAAA;EnDu5JD;AmD35JD;EAOI,gBAAA;EnDu5JH;AmDl5JD;EACE,oBAAA;EACA,2BAAA;EACA,+BAAA;EvBpCA,iCAAA;EACC,gCAAA;E5By7JF;AmD54JD;;EAGI,kBAAA;EnD64JH;AmDh5JD;;EAMM,qBAAA;EACA,kBAAA;EnD84JL;AmD14JG;;EAEI,eAAA;EvBnEN,8BAAA;EACC,6BAAA;E5Bg9JF;AmDz4JG;;EAEI,kBAAA;EvBlEN,iCAAA;EACC,gCAAA;E5B88JF;AmDt4JD;EAEI,qBAAA;EnDu4JH;AmDp4JD;EACE,qBAAA;EnDs4JD;AmD93JD;;;EAII,kBAAA;EnD+3JH;AmDn4JD;;;EAOM,oBAAA;EACA,qBAAA;EnDi4JL;AmDz4JD;;EvB/FE,8BAAA;EACC,6BAAA;E5B4+JF;AmD94JD;;;;EAmBQ,6BAAA;EACA,8BAAA;EnDi4JP;AmDr5JD;;;;;;;;EAwBU,6BAAA;EnDu4JT;AmD/5JD;;;;;;;;EA4BU,8BAAA;EnD64JT;AmDz6JD;;EvBvFE,iCAAA;EACC,gCAAA;E5BogKF;AmD96JD;;;;EAyCQ,gCAAA;EACA,iCAAA;EnD24JP;AmDr7JD;;;;;;;;EA8CU,gCAAA;EnDi5JT;AmD/7JD;;;;;;;;EAkDU,iCAAA;EnDu5JT;AmDz8JD;;;;EA2DI,+BAAA;EnDo5JH;AmD/8JD;;EA+DI,eAAA;EnDo5JH;AmDn9JD;;EAmEI,WAAA;EnDo5JH;AmDv9JD;;;;;;;;;;;;EA0EU,gBAAA;EnD25JT;AmDr+JD;;;;;;;;;;;;EA8EU,iBAAA;EnDq6JT;AmDn/JD;;;;;;;;EAuFU,kBAAA;EnDs6JT;AmD7/JD;;;;;;;;EAgGU,kBAAA;EnDu6JT;AmDvgKD;EAsGI,WAAA;EACA,kBAAA;EnDo6JH;AmD15JD;EACE,qBAAA;EnD45JD;AmD75JD;EAKI,kBAAA;EACA,oBAAA;EnD25JH;AmDj6JD;EASM,iBAAA;EnD25JL;AmDp6JD;EAcI,kBAAA;EnDy5JH;AmDv6JD;;EAkBM,+BAAA;EnDy5JL;AmD36JD;EAuBI,eAAA;EnDu5JH;AmD96JD;EAyBM,kCAAA;EnDw5JL;AmDj5JD;EChPE,uBAAA;EpDooKD;AoDloKC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpDooKH;AoDvoKC;EAMI,2BAAA;EpDooKL;AoD1oKC;EASI,gBAAA;EACA,2BAAA;EpDooKL;AoDjoKC;EAEI,8BAAA;EpDkoKL;AmDh6JD;ECnPE,uBAAA;EpDspKD;AoDppKC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpDspKH;AoDzpKC;EAMI,2BAAA;EpDspKL;AoD5pKC;EASI,gBAAA;EACA,2BAAA;EpDspKL;AoDnpKC;EAEI,8BAAA;EpDopKL;AmD/6JD;ECtPE,uBAAA;EpDwqKD;AoDtqKC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpDwqKH;AoD3qKC;EAMI,2BAAA;EpDwqKL;AoD9qKC;EASI,gBAAA;EACA,2BAAA;EpDwqKL;AoDrqKC;EAEI,8BAAA;EpDsqKL;AmD97JD;ECzPE,uBAAA;EpD0rKD;AoDxrKC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpD0rKH;AoD7rKC;EAMI,2BAAA;EpD0rKL;AoDhsKC;EASI,gBAAA;EACA,2BAAA;EpD0rKL;AoDvrKC;EAEI,8BAAA;EpDwrKL;AmD78JD;EC5PE,uBAAA;EpD4sKD;AoD1sKC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpD4sKH;AoD/sKC;EAMI,2BAAA;EpD4sKL;AoDltKC;EASI,gBAAA;EACA,2BAAA;EpD4sKL;AoDzsKC;EAEI,8BAAA;EpD0sKL;AmD59JD;EC/PE,uBAAA;EpD8tKD;AoD5tKC;EACE,gBAAA;EACA,2BAAA;EACA,uBAAA;EpD8tKH;AoDjuKC;EAMI,2BAAA;EpD8tKL;AoDpuKC;EASI,gBAAA;EACA,2BAAA;EpD8tKL;AoD3tKC;EAEI,8BAAA;EpD4tKL;AqD5uKD;EACE,oBAAA;EACA,gBAAA;EACA,WAAA;EACA,YAAA;EACA,kBAAA;ErD8uKD;AqDnvKD;;;;;EAYI,oBAAA;EACA,QAAA;EACA,SAAA;EACA,WAAA;EACA,cAAA;EACA,aAAA;EACA,WAAA;ErD8uKH;AqD1uKC;EACE,wBAAA;ErD4uKH;AqDxuKC;EACE,qBAAA;ErD0uKH;AsDpwKD;EACE,kBAAA;EACA,eAAA;EACA,qBAAA;EACA,2BAAA;EACA,2BAAA;EACA,oBAAA;EjDwDA,yDAAA;EACQ,iDAAA;EL+sKT;AsD9wKD;EASI,oBAAA;EACA,mCAAA;EtDwwKH;AsDnwKD;EACE,eAAA;EACA,oBAAA;EtDqwKD;AsDnwKD;EACE,cAAA;EACA,oBAAA;EtDqwKD;AuD3xKD;EACE,cAAA;EACA,iBAAA;EACA,mBAAA;EACA,gBAAA;EACA,gBAAA;EACA,8BAAA;EjCRA,cAAA;EAGA,2BAAA;EtBoyKD;AuD5xKC;;EAEE,gBAAA;EACA,uBAAA;EACA,iBAAA;EjCfF,cAAA;EAGA,2BAAA;EtB4yKD;AuDzxKC;EACE,YAAA;EACA,iBAAA;EACA,yBAAA;EACA,WAAA;EACA,0BAAA;EvD2xKH;AwD/yKD;EACE,kBAAA;ExDizKD;AwD7yKD;EACE,eAAA;EACA,kBAAA;EACA,iBAAA;EACA,QAAA;EACA,UAAA;EACA,WAAA;EACA,SAAA;EACA,eAAA;EACA,mCAAA;EAIA,YAAA;ExD4yKD;AwDzyKC;EnD+GA,uCAAA;EACI,mCAAA;EACC,kCAAA;EACG,+BAAA;EAkER,qDAAA;EAEK,2CAAA;EACG,qCAAA;EL4nKT;AwD/yKC;EnD2GA,oCAAA;EACI,gCAAA;EACC,+BAAA;EACG,4BAAA;ELusKT;AwDnzKD;EACE,oBAAA;EACA,kBAAA;ExDqzKD;AwDjzKD;EACE,oBAAA;EACA,aAAA;EACA,cAAA;ExDmzKD;AwD/yKD;EACE,oBAAA;EACA,2BAAA;EACA,2BAAA;EACA,sCAAA;EACA,oBAAA;EnDaA,kDAAA;EACQ,0CAAA;EmDZR,sCAAA;UAAA,8BAAA;EAEA,YAAA;ExDizKD;AwD7yKD;EACE,oBAAA;EACA,QAAA;EACA,UAAA;EACA,SAAA;EACA,2BAAA;ExD+yKD;AwD7yKC;ElCnEA,YAAA;EAGA,0BAAA;EtBi3KD;AwDhzKC;ElCpEA,cAAA;EAGA,2BAAA;EtBq3KD;AwD/yKD;EACE,eAAA;EACA,kCAAA;EACA,2BAAA;ExDizKD;AwD9yKD;EACE,kBAAA;ExDgzKD;AwD5yKD;EACE,WAAA;EACA,yBAAA;ExD8yKD;AwDzyKD;EACE,oBAAA;EACA,eAAA;ExD2yKD;AwDvyKD;EACE,eAAA;EACA,mBAAA;EACA,+BAAA;ExDyyKD;AwD5yKD;EAQI,kBAAA;EACA,kBAAA;ExDuyKH;AwDhzKD;EAaI,mBAAA;ExDsyKH;AwDnzKD;EAiBI,gBAAA;ExDqyKH;AwDhyKD;EACE,oBAAA;EACA,cAAA;EACA,aAAA;EACA,cAAA;EACA,kBAAA;ExDkyKD;AwDhxKD;EAZE;IACE,cAAA;IACA,mBAAA;IxD+xKD;EwD7xKD;InDrEA,mDAAA;IACQ,2CAAA;ILq2KP;EwD5xKD;IAAY,cAAA;IxD+xKX;EACF;AwD1xKD;EAFE;IAAY,cAAA;IxDgyKX;EACF;AyD76KD;EACE,oBAAA;EACA,eAAA;EACA,gBAAA;EACA,qBAAA;EAEA,6DAAA;EACA,iBAAA;EACA,qBAAA;EACA,kBAAA;EnCZA,YAAA;EAGA,0BAAA;EtBy7KD;AyD76KC;EnCfA,cAAA;EAGA,2BAAA;EtB67KD;AyDh7KC;EAAW,kBAAA;EAAmB,gBAAA;EzDo7K/B;AyDn7KC;EAAW,kBAAA;EAAmB,gBAAA;EzDu7K/B;AyDt7KC;EAAW,iBAAA;EAAmB,gBAAA;EzD07K/B;AyDz7KC;EAAW,mBAAA;EAAmB,gBAAA;EzD67K/B;AyDz7KD;EACE,kBAAA;EACA,kBAAA;EACA,gBAAA;EACA,oBAAA;EACA,uBAAA;EACA,2BAAA;EACA,oBAAA;EzD27KD;AyDv7KD;EACE,oBAAA;EACA,UAAA;EACA,WAAA;EACA,2BAAA;EACA,qBAAA;EzDy7KD;AyDr7KC;EACE,WAAA;EACA,WAAA;EACA,mBAAA;EACA,yBAAA;EACA,2BAAA;EzDu7KH;AyDr7KC;EACE,WAAA;EACA,YAAA;EACA,qBAAA;EACA,yBAAA;EACA,2BAAA;EzDu7KH;AyDr7KC;EACE,WAAA;EACA,WAAA;EACA,qBAAA;EACA,yBAAA;EACA,2BAAA;EzDu7KH;AyDr7KC;EACE,UAAA;EACA,SAAA;EACA,kBAAA;EACA,6BAAA;EACA,6BAAA;EzDu7KH;AyDr7KC;EACE,UAAA;EACA,UAAA;EACA,kBAAA;EACA,6BAAA;EACA,4BAAA;EzDu7KH;AyDr7KC;EACE,QAAA;EACA,WAAA;EACA,mBAAA;EACA,yBAAA;EACA,8BAAA;EzDu7KH;AyDr7KC;EACE,QAAA;EACA,YAAA;EACA,kBAAA;EACA,yBAAA;EACA,8BAAA;EzDu7KH;AyDr7KC;EACE,QAAA;EACA,WAAA;EACA,kBAAA;EACA,yBAAA;EACA,8BAAA;EzDu7KH;A0DthLD;EACE,oBAAA;EACA,QAAA;EACA,SAAA;EACA,eAAA;EACA,eAAA;EACA,kBAAA;EACA,cAAA;EAEA,6DAAA;EACA,iBAAA;EACA,qBAAA;EACA,yBAAA;EACA,kBAAA;EACA,2BAAA;EACA,sCAAA;UAAA,8BAAA;EACA,2BAAA;EACA,sCAAA;EACA,oBAAA;ErD6CA,mDAAA;EACQ,2CAAA;EqD1CR,qBAAA;E1DshLD;A0DnhLC;EAAY,mBAAA;E1DshLb;A0DrhLC;EAAY,mBAAA;E1DwhLb;A0DvhLC;EAAY,kBAAA;E1D0hLb;A0DzhLC;EAAY,oBAAA;E1D4hLb;A0DzhLD;EACE,WAAA;EACA,mBAAA;EACA,iBAAA;EACA,2BAAA;EACA,kCAAA;EACA,4BAAA;E1D2hLD;A0DxhLD;EACE,mBAAA;E1D0hLD;A0DlhLC;;EAEE,oBAAA;EACA,gBAAA;EACA,UAAA;EACA,WAAA;EACA,2BAAA;EACA,qBAAA;E1DohLH;A0DjhLD;EACE,oBAAA;E1DmhLD;A0DjhLD;EACE,oBAAA;EACA,aAAA;E1DmhLD;A0D/gLC;EACE,WAAA;EACA,oBAAA;EACA,wBAAA;EACA,2BAAA;EACA,uCAAA;EACA,eAAA;E1DihLH;A0DhhLG;EACE,cAAA;EACA,aAAA;EACA,oBAAA;EACA,wBAAA;EACA,2BAAA;E1DkhLL;A0D/gLC;EACE,UAAA;EACA,aAAA;EACA,mBAAA;EACA,sBAAA;EACA,6BAAA;EACA,yCAAA;E1DihLH;A0DhhLG;EACE,cAAA;EACA,WAAA;EACA,eAAA;EACA,sBAAA;EACA,6BAAA;E1DkhLL;A0D/gLC;EACE,WAAA;EACA,oBAAA;EACA,qBAAA;EACA,8BAAA;EACA,0CAAA;EACA,YAAA;E1DihLH;A0DhhLG;EACE,cAAA;EACA,UAAA;EACA,oBAAA;EACA,qBAAA;EACA,8BAAA;E1DkhLL;A0D9gLC;EACE,UAAA;EACA,cAAA;EACA,mBAAA;EACA,uBAAA;EACA,4BAAA;EACA,wCAAA;E1DghLH;A0D/gLG;EACE,cAAA;EACA,YAAA;EACA,uBAAA;EACA,4BAAA;EACA,eAAA;E1DihLL;A2D9oLD;EACE,oBAAA;E3DgpLD;A2D7oLD;EACE,oBAAA;EACA,kBAAA;EACA,aAAA;E3D+oLD;A2DlpLD;EAMI,eAAA;EACA,oBAAA;EtD6KF,2CAAA;EACK,sCAAA;EACG,mCAAA;ELm+KT;A2DzpLD;;EAcM,gBAAA;E3D+oLL;A2DrnLC;EAAA;IArBI,wDAAA;SAAA,8CAAA;YAAA,wCAAA;IACA,qCAAA;YAAA,6BAAA;IACA,2BAAA;YAAA,mBAAA;I3D8oLH;E2D5oLG;;IAEE,4CAAA;YAAA,oCAAA;IACA,SAAA;I3D8oLL;E2D5oLG;;IAEE,6CAAA;YAAA,qCAAA;IACA,SAAA;I3D8oLL;E2D5oLG;;;IAGE,yCAAA;YAAA,iCAAA;IACA,SAAA;I3D8oLL;EACF;A2DprLD;;;EA6CI,gBAAA;E3D4oLH;A2DzrLD;EAiDI,SAAA;E3D2oLH;A2D5rLD;;EAsDI,oBAAA;EACA,QAAA;EACA,aAAA;E3D0oLH;A2DlsLD;EA4DI,YAAA;E3DyoLH;A2DrsLD;EA+DI,aAAA;E3DyoLH;A2DxsLD;;EAmEI,SAAA;E3DyoLH;A2D5sLD;EAuEI,aAAA;E3DwoLH;A2D/sLD;EA0EI,YAAA;E3DwoLH;A2DhoLD;EACE,oBAAA;EACA,QAAA;EACA,SAAA;EACA,WAAA;EACA,YAAA;ErC9FA,cAAA;EAGA,2BAAA;EqC6FA,iBAAA;EACA,gBAAA;EACA,oBAAA;EACA,2CAAA;E3DmoLD;A2D9nLC;EblGE,oGAAA;EACA,+FAAA;EACA,sHAAA;EAAA,gGAAA;EACA,6BAAA;EACA,wHAAA;E9CmuLH;A2DloLC;EACE,YAAA;EACA,UAAA;EbvGA,oGAAA;EACA,+FAAA;EACA,sHAAA;EAAA,gGAAA;EACA,6BAAA;EACA,wHAAA;E9C4uLH;A2DpoLC;;EAEE,YAAA;EACA,gBAAA;EACA,uBAAA;ErCtHF,cAAA;EAGA,2BAAA;EtB2vLD;A2DrqLD;;;;EAsCI,oBAAA;EACA,UAAA;EACA,YAAA;EACA,uBAAA;E3DqoLH;A2D9qLD;;EA6CI,WAAA;EACA,oBAAA;E3DqoLH;A2DnrLD;;EAkDI,YAAA;EACA,qBAAA;E3DqoLH;A2DxrLD;;EAuDI,aAAA;EACA,cAAA;EACA,mBAAA;EACA,oBAAA;E3DqoLH;A2DhoLG;EACE,kBAAA;E3DkoLL;A2D9nLG;EACE,kBAAA;E3DgoLL;A2DtnLD;EACE,oBAAA;EACA,cAAA;EACA,WAAA;EACA,aAAA;EACA,YAAA;EACA,mBAAA;EACA,iBAAA;EACA,kBAAA;EACA,oBAAA;E3DwnLD;A2DjoLD;EAYI,uBAAA;EACA,aAAA;EACA,cAAA;EACA,aAAA;EACA,qBAAA;EACA,2BAAA;EACA,qBAAA;EACA,iBAAA;EAUA,2BAAA;EACA,oCAAA;E3D+mLH;A2D7oLD;EAiCI,WAAA;EACA,aAAA;EACA,cAAA;EACA,2BAAA;E3D+mLH;A2DxmLD;EACE,oBAAA;EACA,WAAA;EACA,YAAA;EACA,cAAA;EACA,aAAA;EACA,mBAAA;EACA,sBAAA;EACA,gBAAA;EACA,oBAAA;EACA,2CAAA;E3D0mLD;A2DzmLC;EACE,mBAAA;E3D2mLH;A2DlkLD;EAhCE;;;;IAKI,aAAA;IACA,cAAA;IACA,mBAAA;IACA,iBAAA;I3DomLH;E2D5mLD;;IAYI,oBAAA;I3DomLH;E2DhnLD;;IAgBI,qBAAA;I3DomLH;E2D/lLD;IACE,WAAA;IACA,YAAA;IACA,sBAAA;I3DimLD;E2D7lLD;IACE,cAAA;I3D+lLD;EACF;A4D31LC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEE,cAAA;EACA,gBAAA;E5Dy3LH;A4Dv3LC;;;;;;;;;;;;;;;EACE,aAAA;E5Du4LH;AiC/4LD;E4BRE,gBAAA;EACA,mBAAA;EACA,oBAAA;E7D05LD;AiCj5LD;EACE,yBAAA;EjCm5LD;AiCj5LD;EACE,wBAAA;EjCm5LD;AiC34LD;EACE,0BAAA;EjC64LD;AiC34LD;EACE,2BAAA;EjC64LD;AiC34LD;EACE,oBAAA;EjC64LD;AiC34LD;E6BzBE,aAAA;EACA,oBAAA;EACA,mBAAA;EACA,+BAAA;EACA,WAAA;E9Du6LD;AiCz4LD;EACE,0BAAA;EACA,+BAAA;EjC24LD;AiCp4LD;EACE,iBAAA;EjCs4LD;A+Dx6LD;EACE,qBAAA;E/D06LD;A+Dp6LD;;;;ECdE,0BAAA;EhEw7LD;A+Dn6LD;;;;;;;;;;;;EAYE,0BAAA;E/Dq6LD;A+D95LD;EAAA;IChDE,2BAAA;IhEk9LC;EgEj9LD;IAAU,gBAAA;IhEo9LT;EgEn9LD;IAAU,+BAAA;IhEs9LT;EgEr9LD;;IACU,gCAAA;IhEw9LT;EACF;A+Dx6LD;EAAA;IAFI,2BAAA;I/D86LD;EACF;A+Dx6LD;EAAA;IAFI,4BAAA;I/D86LD;EACF;A+Dx6LD;EAAA;IAFI,kCAAA;I/D86LD;EACF;A+Dv6LD;EAAA;ICrEE,2BAAA;IhEg/LC;EgE/+LD;IAAU,gBAAA;IhEk/LT;EgEj/LD;IAAU,+BAAA;IhEo/LT;EgEn/LD;;IACU,gCAAA;IhEs/LT;EACF;A+Dj7LD;EAAA;IAFI,2BAAA;I/Du7LD;EACF;A+Dj7LD;EAAA;IAFI,4BAAA;I/Du7LD;EACF;A+Dj7LD;EAAA;IAFI,kCAAA;I/Du7LD;EACF;A+Dh7LD;EAAA;IC1FE,2BAAA;IhE8gMC;EgE7gMD;IAAU,gBAAA;IhEghMT;EgE/gMD;IAAU,+BAAA;IhEkhMT;EgEjhMD;;IACU,gCAAA;IhEohMT;EACF;A+D17LD;EAAA;IAFI,2BAAA;I/Dg8LD;EACF;A+D17LD;EAAA;IAFI,4BAAA;I/Dg8LD;EACF;A+D17LD;EAAA;IAFI,kCAAA;I/Dg8LD;EACF;A+Dz7LD;EAAA;IC/GE,2BAAA;IhE4iMC;EgE3iMD;IAAU,gBAAA;IhE8iMT;EgE7iMD;IAAU,+BAAA;IhEgjMT;EgE/iMD;;IACU,gCAAA;IhEkjMT;EACF;A+Dn8LD;EAAA;IAFI,2BAAA;I/Dy8LD;EACF;A+Dn8LD;EAAA;IAFI,4BAAA;I/Dy8LD;EACF;A+Dn8LD;EAAA;IAFI,kCAAA;I/Dy8LD;EACF;A+Dl8LD;EAAA;IC5HE,0BAAA;IhEkkMC;EACF;A+Dl8LD;EAAA;ICjIE,0BAAA;IhEukMC;EACF;A+Dl8LD;EAAA;ICtIE,0BAAA;IhE4kMC;EACF;A+Dl8LD;EAAA;IC3IE,0BAAA;IhEilMC;EACF;A+D/7LD;ECnJE,0BAAA;EhEqlMD;A+D57LD;EAAA;ICjKE,2BAAA;IhEimMC;EgEhmMD;IAAU,gBAAA;IhEmmMT;EgElmMD;IAAU,+BAAA;IhEqmMT;EgEpmMD;;IACU,gCAAA;IhEumMT;EACF;A+D18LD;EACE,0BAAA;E/D48LD;A+Dv8LD;EAAA;IAFI,2BAAA;I/D68LD;EACF;A+D38LD;EACE,0BAAA;E/D68LD;A+Dx8LD;EAAA;IAFI,4BAAA;I/D88LD;EACF;A+D58LD;EACE,0BAAA;E/D88LD;A+Dz8LD;EAAA;IAFI,kCAAA;I/D+8LD;EACF;A+Dx8LD;EAAA;ICpLE,0BAAA;IhEgoMC;EACF","file":"bootstrap.css","sourcesContent":["/*! normalize.css v3.0.2 | MIT License | git.io/normalize */\nhtml {\n font-family: sans-serif;\n -ms-text-size-adjust: 100%;\n -webkit-text-size-adjust: 100%;\n}\nbody {\n margin: 0;\n}\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block;\n vertical-align: baseline;\n}\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n[hidden],\ntemplate {\n display: none;\n}\na {\n background-color: transparent;\n}\na:active,\na:hover {\n outline: 0;\n}\nabbr[title] {\n border-bottom: 1px dotted;\n}\nb,\nstrong {\n font-weight: bold;\n}\ndfn {\n font-style: italic;\n}\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\nmark {\n background: #ff0;\n color: #000;\n}\nsmall {\n font-size: 80%;\n}\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\nsup {\n top: -0.5em;\n}\nsub {\n bottom: -0.25em;\n}\nimg {\n border: 0;\n}\nsvg:not(:root) {\n overflow: hidden;\n}\nfigure {\n margin: 1em 40px;\n}\nhr {\n -moz-box-sizing: content-box;\n box-sizing: content-box;\n height: 0;\n}\npre {\n overflow: auto;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit;\n font: inherit;\n margin: 0;\n}\nbutton {\n overflow: visible;\n}\nbutton,\nselect {\n text-transform: none;\n}\nbutton,\nhtml input[type=\"button\"],\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button;\n cursor: pointer;\n}\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\ninput {\n line-height: normal;\n}\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box;\n padding: 0;\n}\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: textfield;\n -moz-box-sizing: content-box;\n -webkit-box-sizing: content-box;\n box-sizing: content-box;\n}\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\nlegend {\n border: 0;\n padding: 0;\n}\ntextarea {\n overflow: auto;\n}\noptgroup {\n font-weight: bold;\n}\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\ntd,\nth {\n padding: 0;\n}\n/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n@media print {\n *,\n *:before,\n *:after {\n background: transparent !important;\n color: #000 !important;\n box-shadow: none !important;\n text-shadow: none !important;\n }\n a,\n a:visited {\n text-decoration: underline;\n }\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n thead {\n display: table-header-group;\n }\n tr,\n img {\n page-break-inside: avoid;\n }\n img {\n max-width: 100% !important;\n }\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n h2,\n h3 {\n page-break-after: avoid;\n }\n select {\n background: #fff !important;\n }\n .navbar {\n display: none;\n }\n .btn > .caret,\n .dropup > .btn > .caret {\n border-top-color: #000 !important;\n }\n .label {\n border: 1px solid #000;\n }\n .table {\n border-collapse: collapse !important;\n }\n .table td,\n .table th {\n background-color: #fff !important;\n }\n .table-bordered th,\n .table-bordered td {\n border: 1px solid #ddd !important;\n }\n}\n@font-face {\n font-family: 'Glyphicons Halflings';\n src: url('../fonts/glyphicons-halflings-regular.eot');\n src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg');\n}\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: 'Glyphicons Halflings';\n font-style: normal;\n font-weight: normal;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n.glyphicon-asterisk:before {\n content: \"\\2a\";\n}\n.glyphicon-plus:before {\n content: \"\\2b\";\n}\n.glyphicon-euro:before,\n.glyphicon-eur:before {\n content: \"\\20ac\";\n}\n.glyphicon-minus:before {\n content: \"\\2212\";\n}\n.glyphicon-cloud:before {\n content: \"\\2601\";\n}\n.glyphicon-envelope:before {\n content: \"\\2709\";\n}\n.glyphicon-pencil:before {\n content: \"\\270f\";\n}\n.glyphicon-glass:before {\n content: \"\\e001\";\n}\n.glyphicon-music:before {\n content: \"\\e002\";\n}\n.glyphicon-search:before {\n content: \"\\e003\";\n}\n.glyphicon-heart:before {\n content: \"\\e005\";\n}\n.glyphicon-star:before {\n content: \"\\e006\";\n}\n.glyphicon-star-empty:before {\n content: \"\\e007\";\n}\n.glyphicon-user:before {\n content: \"\\e008\";\n}\n.glyphicon-film:before {\n content: \"\\e009\";\n}\n.glyphicon-th-large:before {\n content: \"\\e010\";\n}\n.glyphicon-th:before {\n content: \"\\e011\";\n}\n.glyphicon-th-list:before {\n content: \"\\e012\";\n}\n.glyphicon-ok:before {\n content: \"\\e013\";\n}\n.glyphicon-remove:before {\n content: \"\\e014\";\n}\n.glyphicon-zoom-in:before {\n content: \"\\e015\";\n}\n.glyphicon-zoom-out:before {\n content: \"\\e016\";\n}\n.glyphicon-off:before {\n content: \"\\e017\";\n}\n.glyphicon-signal:before {\n content: \"\\e018\";\n}\n.glyphicon-cog:before {\n content: \"\\e019\";\n}\n.glyphicon-trash:before {\n content: \"\\e020\";\n}\n.glyphicon-home:before {\n content: \"\\e021\";\n}\n.glyphicon-file:before {\n content: \"\\e022\";\n}\n.glyphicon-time:before {\n content: \"\\e023\";\n}\n.glyphicon-road:before {\n content: \"\\e024\";\n}\n.glyphicon-download-alt:before {\n content: \"\\e025\";\n}\n.glyphicon-download:before {\n content: \"\\e026\";\n}\n.glyphicon-upload:before {\n content: \"\\e027\";\n}\n.glyphicon-inbox:before {\n content: \"\\e028\";\n}\n.glyphicon-play-circle:before {\n content: \"\\e029\";\n}\n.glyphicon-repeat:before {\n content: \"\\e030\";\n}\n.glyphicon-refresh:before {\n content: \"\\e031\";\n}\n.glyphicon-list-alt:before {\n content: \"\\e032\";\n}\n.glyphicon-lock:before {\n content: \"\\e033\";\n}\n.glyphicon-flag:before {\n content: \"\\e034\";\n}\n.glyphicon-headphones:before {\n content: \"\\e035\";\n}\n.glyphicon-volume-off:before {\n content: \"\\e036\";\n}\n.glyphicon-volume-down:before {\n content: \"\\e037\";\n}\n.glyphicon-volume-up:before {\n content: \"\\e038\";\n}\n.glyphicon-qrcode:before {\n content: \"\\e039\";\n}\n.glyphicon-barcode:before {\n content: \"\\e040\";\n}\n.glyphicon-tag:before {\n content: \"\\e041\";\n}\n.glyphicon-tags:before {\n content: \"\\e042\";\n}\n.glyphicon-book:before {\n content: \"\\e043\";\n}\n.glyphicon-bookmark:before {\n content: \"\\e044\";\n}\n.glyphicon-print:before {\n content: \"\\e045\";\n}\n.glyphicon-camera:before {\n content: \"\\e046\";\n}\n.glyphicon-font:before {\n content: \"\\e047\";\n}\n.glyphicon-bold:before {\n content: \"\\e048\";\n}\n.glyphicon-italic:before {\n content: \"\\e049\";\n}\n.glyphicon-text-height:before {\n content: \"\\e050\";\n}\n.glyphicon-text-width:before {\n content: \"\\e051\";\n}\n.glyphicon-align-left:before {\n content: \"\\e052\";\n}\n.glyphicon-align-center:before {\n content: \"\\e053\";\n}\n.glyphicon-align-right:before {\n content: \"\\e054\";\n}\n.glyphicon-align-justify:before {\n content: \"\\e055\";\n}\n.glyphicon-list:before {\n content: \"\\e056\";\n}\n.glyphicon-indent-left:before {\n content: \"\\e057\";\n}\n.glyphicon-indent-right:before {\n content: \"\\e058\";\n}\n.glyphicon-facetime-video:before {\n content: \"\\e059\";\n}\n.glyphicon-picture:before {\n content: \"\\e060\";\n}\n.glyphicon-map-marker:before {\n content: \"\\e062\";\n}\n.glyphicon-adjust:before {\n content: \"\\e063\";\n}\n.glyphicon-tint:before {\n content: \"\\e064\";\n}\n.glyphicon-edit:before {\n content: \"\\e065\";\n}\n.glyphicon-share:before {\n content: \"\\e066\";\n}\n.glyphicon-check:before {\n content: \"\\e067\";\n}\n.glyphicon-move:before {\n content: \"\\e068\";\n}\n.glyphicon-step-backward:before {\n content: \"\\e069\";\n}\n.glyphicon-fast-backward:before {\n content: \"\\e070\";\n}\n.glyphicon-backward:before {\n content: \"\\e071\";\n}\n.glyphicon-play:before {\n content: \"\\e072\";\n}\n.glyphicon-pause:before {\n content: \"\\e073\";\n}\n.glyphicon-stop:before {\n content: \"\\e074\";\n}\n.glyphicon-forward:before {\n content: \"\\e075\";\n}\n.glyphicon-fast-forward:before {\n content: \"\\e076\";\n}\n.glyphicon-step-forward:before {\n content: \"\\e077\";\n}\n.glyphicon-eject:before {\n content: \"\\e078\";\n}\n.glyphicon-chevron-left:before {\n content: \"\\e079\";\n}\n.glyphicon-chevron-right:before {\n content: \"\\e080\";\n}\n.glyphicon-plus-sign:before {\n content: \"\\e081\";\n}\n.glyphicon-minus-sign:before {\n content: \"\\e082\";\n}\n.glyphicon-remove-sign:before {\n content: \"\\e083\";\n}\n.glyphicon-ok-sign:before {\n content: \"\\e084\";\n}\n.glyphicon-question-sign:before {\n content: \"\\e085\";\n}\n.glyphicon-info-sign:before {\n content: \"\\e086\";\n}\n.glyphicon-screenshot:before {\n content: \"\\e087\";\n}\n.glyphicon-remove-circle:before {\n content: \"\\e088\";\n}\n.glyphicon-ok-circle:before {\n content: \"\\e089\";\n}\n.glyphicon-ban-circle:before {\n content: \"\\e090\";\n}\n.glyphicon-arrow-left:before {\n content: \"\\e091\";\n}\n.glyphicon-arrow-right:before {\n content: \"\\e092\";\n}\n.glyphicon-arrow-up:before {\n content: \"\\e093\";\n}\n.glyphicon-arrow-down:before {\n content: \"\\e094\";\n}\n.glyphicon-share-alt:before {\n content: \"\\e095\";\n}\n.glyphicon-resize-full:before {\n content: \"\\e096\";\n}\n.glyphicon-resize-small:before {\n content: \"\\e097\";\n}\n.glyphicon-exclamation-sign:before {\n content: \"\\e101\";\n}\n.glyphicon-gift:before {\n content: \"\\e102\";\n}\n.glyphicon-leaf:before {\n content: \"\\e103\";\n}\n.glyphicon-fire:before {\n content: \"\\e104\";\n}\n.glyphicon-eye-open:before {\n content: \"\\e105\";\n}\n.glyphicon-eye-close:before {\n content: \"\\e106\";\n}\n.glyphicon-warning-sign:before {\n content: \"\\e107\";\n}\n.glyphicon-plane:before {\n content: \"\\e108\";\n}\n.glyphicon-calendar:before {\n content: \"\\e109\";\n}\n.glyphicon-random:before {\n content: \"\\e110\";\n}\n.glyphicon-comment:before {\n content: \"\\e111\";\n}\n.glyphicon-magnet:before {\n content: \"\\e112\";\n}\n.glyphicon-chevron-up:before {\n content: \"\\e113\";\n}\n.glyphicon-chevron-down:before {\n content: \"\\e114\";\n}\n.glyphicon-retweet:before {\n content: \"\\e115\";\n}\n.glyphicon-shopping-cart:before {\n content: \"\\e116\";\n}\n.glyphicon-folder-close:before {\n content: \"\\e117\";\n}\n.glyphicon-folder-open:before {\n content: \"\\e118\";\n}\n.glyphicon-resize-vertical:before {\n content: \"\\e119\";\n}\n.glyphicon-resize-horizontal:before {\n content: \"\\e120\";\n}\n.glyphicon-hdd:before {\n content: \"\\e121\";\n}\n.glyphicon-bullhorn:before {\n content: \"\\e122\";\n}\n.glyphicon-bell:before {\n content: \"\\e123\";\n}\n.glyphicon-certificate:before {\n content: \"\\e124\";\n}\n.glyphicon-thumbs-up:before {\n content: \"\\e125\";\n}\n.glyphicon-thumbs-down:before {\n content: \"\\e126\";\n}\n.glyphicon-hand-right:before {\n content: \"\\e127\";\n}\n.glyphicon-hand-left:before {\n content: \"\\e128\";\n}\n.glyphicon-hand-up:before {\n content: \"\\e129\";\n}\n.glyphicon-hand-down:before {\n content: \"\\e130\";\n}\n.glyphicon-circle-arrow-right:before {\n content: \"\\e131\";\n}\n.glyphicon-circle-arrow-left:before {\n content: \"\\e132\";\n}\n.glyphicon-circle-arrow-up:before {\n content: \"\\e133\";\n}\n.glyphicon-circle-arrow-down:before {\n content: \"\\e134\";\n}\n.glyphicon-globe:before {\n content: \"\\e135\";\n}\n.glyphicon-wrench:before {\n content: \"\\e136\";\n}\n.glyphicon-tasks:before {\n content: \"\\e137\";\n}\n.glyphicon-filter:before {\n content: \"\\e138\";\n}\n.glyphicon-briefcase:before {\n content: \"\\e139\";\n}\n.glyphicon-fullscreen:before {\n content: \"\\e140\";\n}\n.glyphicon-dashboard:before {\n content: \"\\e141\";\n}\n.glyphicon-paperclip:before {\n content: \"\\e142\";\n}\n.glyphicon-heart-empty:before {\n content: \"\\e143\";\n}\n.glyphicon-link:before {\n content: \"\\e144\";\n}\n.glyphicon-phone:before {\n content: \"\\e145\";\n}\n.glyphicon-pushpin:before {\n content: \"\\e146\";\n}\n.glyphicon-usd:before {\n content: \"\\e148\";\n}\n.glyphicon-gbp:before {\n content: \"\\e149\";\n}\n.glyphicon-sort:before {\n content: \"\\e150\";\n}\n.glyphicon-sort-by-alphabet:before {\n content: \"\\e151\";\n}\n.glyphicon-sort-by-alphabet-alt:before {\n content: \"\\e152\";\n}\n.glyphicon-sort-by-order:before {\n content: \"\\e153\";\n}\n.glyphicon-sort-by-order-alt:before {\n content: \"\\e154\";\n}\n.glyphicon-sort-by-attributes:before {\n content: \"\\e155\";\n}\n.glyphicon-sort-by-attributes-alt:before {\n content: \"\\e156\";\n}\n.glyphicon-unchecked:before {\n content: \"\\e157\";\n}\n.glyphicon-expand:before {\n content: \"\\e158\";\n}\n.glyphicon-collapse-down:before {\n content: \"\\e159\";\n}\n.glyphicon-collapse-up:before {\n content: \"\\e160\";\n}\n.glyphicon-log-in:before {\n content: \"\\e161\";\n}\n.glyphicon-flash:before {\n content: \"\\e162\";\n}\n.glyphicon-log-out:before {\n content: \"\\e163\";\n}\n.glyphicon-new-window:before {\n content: \"\\e164\";\n}\n.glyphicon-record:before {\n content: \"\\e165\";\n}\n.glyphicon-save:before {\n content: \"\\e166\";\n}\n.glyphicon-open:before {\n content: \"\\e167\";\n}\n.glyphicon-saved:before {\n content: \"\\e168\";\n}\n.glyphicon-import:before {\n content: \"\\e169\";\n}\n.glyphicon-export:before {\n content: \"\\e170\";\n}\n.glyphicon-send:before {\n content: \"\\e171\";\n}\n.glyphicon-floppy-disk:before {\n content: \"\\e172\";\n}\n.glyphicon-floppy-saved:before {\n content: \"\\e173\";\n}\n.glyphicon-floppy-remove:before {\n content: \"\\e174\";\n}\n.glyphicon-floppy-save:before {\n content: \"\\e175\";\n}\n.glyphicon-floppy-open:before {\n content: \"\\e176\";\n}\n.glyphicon-credit-card:before {\n content: \"\\e177\";\n}\n.glyphicon-transfer:before {\n content: \"\\e178\";\n}\n.glyphicon-cutlery:before {\n content: \"\\e179\";\n}\n.glyphicon-header:before {\n content: \"\\e180\";\n}\n.glyphicon-compressed:before {\n content: \"\\e181\";\n}\n.glyphicon-earphone:before {\n content: \"\\e182\";\n}\n.glyphicon-phone-alt:before {\n content: \"\\e183\";\n}\n.glyphicon-tower:before {\n content: \"\\e184\";\n}\n.glyphicon-stats:before {\n content: \"\\e185\";\n}\n.glyphicon-sd-video:before {\n content: \"\\e186\";\n}\n.glyphicon-hd-video:before {\n content: \"\\e187\";\n}\n.glyphicon-subtitles:before {\n content: \"\\e188\";\n}\n.glyphicon-sound-stereo:before {\n content: \"\\e189\";\n}\n.glyphicon-sound-dolby:before {\n content: \"\\e190\";\n}\n.glyphicon-sound-5-1:before {\n content: \"\\e191\";\n}\n.glyphicon-sound-6-1:before {\n content: \"\\e192\";\n}\n.glyphicon-sound-7-1:before {\n content: \"\\e193\";\n}\n.glyphicon-copyright-mark:before {\n content: \"\\e194\";\n}\n.glyphicon-registration-mark:before {\n content: \"\\e195\";\n}\n.glyphicon-cloud-download:before {\n content: \"\\e197\";\n}\n.glyphicon-cloud-upload:before {\n content: \"\\e198\";\n}\n.glyphicon-tree-conifer:before {\n content: \"\\e199\";\n}\n.glyphicon-tree-deciduous:before {\n content: \"\\e200\";\n}\n* {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\n*:before,\n*:after {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0, 0, 0, 0);\n}\nbody {\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n line-height: 1.42857143;\n color: #333333;\n background-color: #ffffff;\n}\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\na {\n color: #337ab7;\n text-decoration: none;\n}\na:hover,\na:focus {\n color: #23527c;\n text-decoration: underline;\n}\na:focus {\n outline: thin dotted;\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\nfigure {\n margin: 0;\n}\nimg {\n vertical-align: middle;\n}\n.img-responsive,\n.thumbnail > img,\n.thumbnail a > img,\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n display: block;\n max-width: 100%;\n height: auto;\n}\n.img-rounded {\n border-radius: 6px;\n}\n.img-thumbnail {\n padding: 4px;\n line-height: 1.42857143;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n border-radius: 4px;\n -webkit-transition: all 0.2s ease-in-out;\n -o-transition: all 0.2s ease-in-out;\n transition: all 0.2s ease-in-out;\n display: inline-block;\n max-width: 100%;\n height: auto;\n}\n.img-circle {\n border-radius: 50%;\n}\nhr {\n margin-top: 20px;\n margin-bottom: 20px;\n border: 0;\n border-top: 1px solid #eeeeee;\n}\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0, 0, 0, 0);\n border: 0;\n}\n.sr-only-focusable:active,\n.sr-only-focusable:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n}\nh1,\nh2,\nh3,\nh4,\nh5,\nh6,\n.h1,\n.h2,\n.h3,\n.h4,\n.h5,\n.h6 {\n font-family: inherit;\n font-weight: 500;\n line-height: 1.1;\n color: inherit;\n}\nh1 small,\nh2 small,\nh3 small,\nh4 small,\nh5 small,\nh6 small,\n.h1 small,\n.h2 small,\n.h3 small,\n.h4 small,\n.h5 small,\n.h6 small,\nh1 .small,\nh2 .small,\nh3 .small,\nh4 .small,\nh5 .small,\nh6 .small,\n.h1 .small,\n.h2 .small,\n.h3 .small,\n.h4 .small,\n.h5 .small,\n.h6 .small {\n font-weight: normal;\n line-height: 1;\n color: #777777;\n}\nh1,\n.h1,\nh2,\n.h2,\nh3,\n.h3 {\n margin-top: 20px;\n margin-bottom: 10px;\n}\nh1 small,\n.h1 small,\nh2 small,\n.h2 small,\nh3 small,\n.h3 small,\nh1 .small,\n.h1 .small,\nh2 .small,\n.h2 .small,\nh3 .small,\n.h3 .small {\n font-size: 65%;\n}\nh4,\n.h4,\nh5,\n.h5,\nh6,\n.h6 {\n margin-top: 10px;\n margin-bottom: 10px;\n}\nh4 small,\n.h4 small,\nh5 small,\n.h5 small,\nh6 small,\n.h6 small,\nh4 .small,\n.h4 .small,\nh5 .small,\n.h5 .small,\nh6 .small,\n.h6 .small {\n font-size: 75%;\n}\nh1,\n.h1 {\n font-size: 36px;\n}\nh2,\n.h2 {\n font-size: 30px;\n}\nh3,\n.h3 {\n font-size: 24px;\n}\nh4,\n.h4 {\n font-size: 18px;\n}\nh5,\n.h5 {\n font-size: 14px;\n}\nh6,\n.h6 {\n font-size: 12px;\n}\np {\n margin: 0 0 10px;\n}\n.lead {\n margin-bottom: 20px;\n font-size: 16px;\n font-weight: 300;\n line-height: 1.4;\n}\n@media (min-width: 768px) {\n .lead {\n font-size: 21px;\n }\n}\nsmall,\n.small {\n font-size: 85%;\n}\nmark,\n.mark {\n background-color: #fcf8e3;\n padding: .2em;\n}\n.text-left {\n text-align: left;\n}\n.text-right {\n text-align: right;\n}\n.text-center {\n text-align: center;\n}\n.text-justify {\n text-align: justify;\n}\n.text-nowrap {\n white-space: nowrap;\n}\n.text-lowercase {\n text-transform: lowercase;\n}\n.text-uppercase {\n text-transform: uppercase;\n}\n.text-capitalize {\n text-transform: capitalize;\n}\n.text-muted {\n color: #777777;\n}\n.text-primary {\n color: #337ab7;\n}\na.text-primary:hover {\n color: #286090;\n}\n.text-success {\n color: #3c763d;\n}\na.text-success:hover {\n color: #2b542c;\n}\n.text-info {\n color: #31708f;\n}\na.text-info:hover {\n color: #245269;\n}\n.text-warning {\n color: #8a6d3b;\n}\na.text-warning:hover {\n color: #66512c;\n}\n.text-danger {\n color: #a94442;\n}\na.text-danger:hover {\n color: #843534;\n}\n.bg-primary {\n color: #fff;\n background-color: #337ab7;\n}\na.bg-primary:hover {\n background-color: #286090;\n}\n.bg-success {\n background-color: #dff0d8;\n}\na.bg-success:hover {\n background-color: #c1e2b3;\n}\n.bg-info {\n background-color: #d9edf7;\n}\na.bg-info:hover {\n background-color: #afd9ee;\n}\n.bg-warning {\n background-color: #fcf8e3;\n}\na.bg-warning:hover {\n background-color: #f7ecb5;\n}\n.bg-danger {\n background-color: #f2dede;\n}\na.bg-danger:hover {\n background-color: #e4b9b9;\n}\n.page-header {\n padding-bottom: 9px;\n margin: 40px 0 20px;\n border-bottom: 1px solid #eeeeee;\n}\nul,\nol {\n margin-top: 0;\n margin-bottom: 10px;\n}\nul ul,\nol ul,\nul ol,\nol ol {\n margin-bottom: 0;\n}\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n.list-inline {\n padding-left: 0;\n list-style: none;\n margin-left: -5px;\n}\n.list-inline > li {\n display: inline-block;\n padding-left: 5px;\n padding-right: 5px;\n}\ndl {\n margin-top: 0;\n margin-bottom: 20px;\n}\ndt,\ndd {\n line-height: 1.42857143;\n}\ndt {\n font-weight: bold;\n}\ndd {\n margin-left: 0;\n}\n@media (min-width: 768px) {\n .dl-horizontal dt {\n float: left;\n width: 160px;\n clear: left;\n text-align: right;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n .dl-horizontal dd {\n margin-left: 180px;\n }\n}\nabbr[title],\nabbr[data-original-title] {\n cursor: help;\n border-bottom: 1px dotted #777777;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\nblockquote {\n padding: 10px 20px;\n margin: 0 0 20px;\n font-size: 17.5px;\n border-left: 5px solid #eeeeee;\n}\nblockquote p:last-child,\nblockquote ul:last-child,\nblockquote ol:last-child {\n margin-bottom: 0;\n}\nblockquote footer,\nblockquote small,\nblockquote .small {\n display: block;\n font-size: 80%;\n line-height: 1.42857143;\n color: #777777;\n}\nblockquote footer:before,\nblockquote small:before,\nblockquote .small:before {\n content: '\\2014 \\00A0';\n}\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n border-right: 5px solid #eeeeee;\n border-left: 0;\n text-align: right;\n}\n.blockquote-reverse footer:before,\nblockquote.pull-right footer:before,\n.blockquote-reverse small:before,\nblockquote.pull-right small:before,\n.blockquote-reverse .small:before,\nblockquote.pull-right .small:before {\n content: '';\n}\n.blockquote-reverse footer:after,\nblockquote.pull-right footer:after,\n.blockquote-reverse small:after,\nblockquote.pull-right small:after,\n.blockquote-reverse .small:after,\nblockquote.pull-right .small:after {\n content: '\\00A0 \\2014';\n}\naddress {\n margin-bottom: 20px;\n font-style: normal;\n line-height: 1.42857143;\n}\ncode,\nkbd,\npre,\nsamp {\n font-family: Menlo, Monaco, Consolas, \"Courier New\", monospace;\n}\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: #c7254e;\n background-color: #f9f2f4;\n border-radius: 4px;\n}\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: #ffffff;\n background-color: #333333;\n border-radius: 3px;\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.25);\n}\nkbd kbd {\n padding: 0;\n font-size: 100%;\n font-weight: bold;\n box-shadow: none;\n}\npre {\n display: block;\n padding: 9.5px;\n margin: 0 0 10px;\n font-size: 13px;\n line-height: 1.42857143;\n word-break: break-all;\n word-wrap: break-word;\n color: #333333;\n background-color: #f5f5f5;\n border: 1px solid #cccccc;\n border-radius: 4px;\n}\npre code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n}\n.pre-scrollable {\n max-height: 340px;\n overflow-y: scroll;\n}\n.container {\n margin-right: auto;\n margin-left: auto;\n padding-left: 15px;\n padding-right: 15px;\n}\n@media (min-width: 768px) {\n .container {\n width: 750px;\n }\n}\n@media (min-width: 992px) {\n .container {\n width: 970px;\n }\n}\n@media (min-width: 1200px) {\n .container {\n width: 1170px;\n }\n}\n.container-fluid {\n margin-right: auto;\n margin-left: auto;\n padding-left: 15px;\n padding-right: 15px;\n}\n.row {\n margin-left: -15px;\n margin-right: -15px;\n}\n.col-xs-1, .col-sm-1, .col-md-1, .col-lg-1, .col-xs-2, .col-sm-2, .col-md-2, .col-lg-2, .col-xs-3, .col-sm-3, .col-md-3, .col-lg-3, .col-xs-4, .col-sm-4, .col-md-4, .col-lg-4, .col-xs-5, .col-sm-5, .col-md-5, .col-lg-5, .col-xs-6, .col-sm-6, .col-md-6, .col-lg-6, .col-xs-7, .col-sm-7, .col-md-7, .col-lg-7, .col-xs-8, .col-sm-8, .col-md-8, .col-lg-8, .col-xs-9, .col-sm-9, .col-md-9, .col-lg-9, .col-xs-10, .col-sm-10, .col-md-10, .col-lg-10, .col-xs-11, .col-sm-11, .col-md-11, .col-lg-11, .col-xs-12, .col-sm-12, .col-md-12, .col-lg-12 {\n position: relative;\n min-height: 1px;\n padding-left: 15px;\n padding-right: 15px;\n}\n.col-xs-1, .col-xs-2, .col-xs-3, .col-xs-4, .col-xs-5, .col-xs-6, .col-xs-7, .col-xs-8, .col-xs-9, .col-xs-10, .col-xs-11, .col-xs-12 {\n float: left;\n}\n.col-xs-12 {\n width: 100%;\n}\n.col-xs-11 {\n width: 91.66666667%;\n}\n.col-xs-10 {\n width: 83.33333333%;\n}\n.col-xs-9 {\n width: 75%;\n}\n.col-xs-8 {\n width: 66.66666667%;\n}\n.col-xs-7 {\n width: 58.33333333%;\n}\n.col-xs-6 {\n width: 50%;\n}\n.col-xs-5 {\n width: 41.66666667%;\n}\n.col-xs-4 {\n width: 33.33333333%;\n}\n.col-xs-3 {\n width: 25%;\n}\n.col-xs-2 {\n width: 16.66666667%;\n}\n.col-xs-1 {\n width: 8.33333333%;\n}\n.col-xs-pull-12 {\n right: 100%;\n}\n.col-xs-pull-11 {\n right: 91.66666667%;\n}\n.col-xs-pull-10 {\n right: 83.33333333%;\n}\n.col-xs-pull-9 {\n right: 75%;\n}\n.col-xs-pull-8 {\n right: 66.66666667%;\n}\n.col-xs-pull-7 {\n right: 58.33333333%;\n}\n.col-xs-pull-6 {\n right: 50%;\n}\n.col-xs-pull-5 {\n right: 41.66666667%;\n}\n.col-xs-pull-4 {\n right: 33.33333333%;\n}\n.col-xs-pull-3 {\n right: 25%;\n}\n.col-xs-pull-2 {\n right: 16.66666667%;\n}\n.col-xs-pull-1 {\n right: 8.33333333%;\n}\n.col-xs-pull-0 {\n right: auto;\n}\n.col-xs-push-12 {\n left: 100%;\n}\n.col-xs-push-11 {\n left: 91.66666667%;\n}\n.col-xs-push-10 {\n left: 83.33333333%;\n}\n.col-xs-push-9 {\n left: 75%;\n}\n.col-xs-push-8 {\n left: 66.66666667%;\n}\n.col-xs-push-7 {\n left: 58.33333333%;\n}\n.col-xs-push-6 {\n left: 50%;\n}\n.col-xs-push-5 {\n left: 41.66666667%;\n}\n.col-xs-push-4 {\n left: 33.33333333%;\n}\n.col-xs-push-3 {\n left: 25%;\n}\n.col-xs-push-2 {\n left: 16.66666667%;\n}\n.col-xs-push-1 {\n left: 8.33333333%;\n}\n.col-xs-push-0 {\n left: auto;\n}\n.col-xs-offset-12 {\n margin-left: 100%;\n}\n.col-xs-offset-11 {\n margin-left: 91.66666667%;\n}\n.col-xs-offset-10 {\n margin-left: 83.33333333%;\n}\n.col-xs-offset-9 {\n margin-left: 75%;\n}\n.col-xs-offset-8 {\n margin-left: 66.66666667%;\n}\n.col-xs-offset-7 {\n margin-left: 58.33333333%;\n}\n.col-xs-offset-6 {\n margin-left: 50%;\n}\n.col-xs-offset-5 {\n margin-left: 41.66666667%;\n}\n.col-xs-offset-4 {\n margin-left: 33.33333333%;\n}\n.col-xs-offset-3 {\n margin-left: 25%;\n}\n.col-xs-offset-2 {\n margin-left: 16.66666667%;\n}\n.col-xs-offset-1 {\n margin-left: 8.33333333%;\n}\n.col-xs-offset-0 {\n margin-left: 0%;\n}\n@media (min-width: 768px) {\n .col-sm-1, .col-sm-2, .col-sm-3, .col-sm-4, .col-sm-5, .col-sm-6, .col-sm-7, .col-sm-8, .col-sm-9, .col-sm-10, .col-sm-11, .col-sm-12 {\n float: left;\n }\n .col-sm-12 {\n width: 100%;\n }\n .col-sm-11 {\n width: 91.66666667%;\n }\n .col-sm-10 {\n width: 83.33333333%;\n }\n .col-sm-9 {\n width: 75%;\n }\n .col-sm-8 {\n width: 66.66666667%;\n }\n .col-sm-7 {\n width: 58.33333333%;\n }\n .col-sm-6 {\n width: 50%;\n }\n .col-sm-5 {\n width: 41.66666667%;\n }\n .col-sm-4 {\n width: 33.33333333%;\n }\n .col-sm-3 {\n width: 25%;\n }\n .col-sm-2 {\n width: 16.66666667%;\n }\n .col-sm-1 {\n width: 8.33333333%;\n }\n .col-sm-pull-12 {\n right: 100%;\n }\n .col-sm-pull-11 {\n right: 91.66666667%;\n }\n .col-sm-pull-10 {\n right: 83.33333333%;\n }\n .col-sm-pull-9 {\n right: 75%;\n }\n .col-sm-pull-8 {\n right: 66.66666667%;\n }\n .col-sm-pull-7 {\n right: 58.33333333%;\n }\n .col-sm-pull-6 {\n right: 50%;\n }\n .col-sm-pull-5 {\n right: 41.66666667%;\n }\n .col-sm-pull-4 {\n right: 33.33333333%;\n }\n .col-sm-pull-3 {\n right: 25%;\n }\n .col-sm-pull-2 {\n right: 16.66666667%;\n }\n .col-sm-pull-1 {\n right: 8.33333333%;\n }\n .col-sm-pull-0 {\n right: auto;\n }\n .col-sm-push-12 {\n left: 100%;\n }\n .col-sm-push-11 {\n left: 91.66666667%;\n }\n .col-sm-push-10 {\n left: 83.33333333%;\n }\n .col-sm-push-9 {\n left: 75%;\n }\n .col-sm-push-8 {\n left: 66.66666667%;\n }\n .col-sm-push-7 {\n left: 58.33333333%;\n }\n .col-sm-push-6 {\n left: 50%;\n }\n .col-sm-push-5 {\n left: 41.66666667%;\n }\n .col-sm-push-4 {\n left: 33.33333333%;\n }\n .col-sm-push-3 {\n left: 25%;\n }\n .col-sm-push-2 {\n left: 16.66666667%;\n }\n .col-sm-push-1 {\n left: 8.33333333%;\n }\n .col-sm-push-0 {\n left: auto;\n }\n .col-sm-offset-12 {\n margin-left: 100%;\n }\n .col-sm-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-sm-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-sm-offset-9 {\n margin-left: 75%;\n }\n .col-sm-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-sm-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-sm-offset-6 {\n margin-left: 50%;\n }\n .col-sm-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-sm-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-sm-offset-3 {\n margin-left: 25%;\n }\n .col-sm-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-sm-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-sm-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 992px) {\n .col-md-1, .col-md-2, .col-md-3, .col-md-4, .col-md-5, .col-md-6, .col-md-7, .col-md-8, .col-md-9, .col-md-10, .col-md-11, .col-md-12 {\n float: left;\n }\n .col-md-12 {\n width: 100%;\n }\n .col-md-11 {\n width: 91.66666667%;\n }\n .col-md-10 {\n width: 83.33333333%;\n }\n .col-md-9 {\n width: 75%;\n }\n .col-md-8 {\n width: 66.66666667%;\n }\n .col-md-7 {\n width: 58.33333333%;\n }\n .col-md-6 {\n width: 50%;\n }\n .col-md-5 {\n width: 41.66666667%;\n }\n .col-md-4 {\n width: 33.33333333%;\n }\n .col-md-3 {\n width: 25%;\n }\n .col-md-2 {\n width: 16.66666667%;\n }\n .col-md-1 {\n width: 8.33333333%;\n }\n .col-md-pull-12 {\n right: 100%;\n }\n .col-md-pull-11 {\n right: 91.66666667%;\n }\n .col-md-pull-10 {\n right: 83.33333333%;\n }\n .col-md-pull-9 {\n right: 75%;\n }\n .col-md-pull-8 {\n right: 66.66666667%;\n }\n .col-md-pull-7 {\n right: 58.33333333%;\n }\n .col-md-pull-6 {\n right: 50%;\n }\n .col-md-pull-5 {\n right: 41.66666667%;\n }\n .col-md-pull-4 {\n right: 33.33333333%;\n }\n .col-md-pull-3 {\n right: 25%;\n }\n .col-md-pull-2 {\n right: 16.66666667%;\n }\n .col-md-pull-1 {\n right: 8.33333333%;\n }\n .col-md-pull-0 {\n right: auto;\n }\n .col-md-push-12 {\n left: 100%;\n }\n .col-md-push-11 {\n left: 91.66666667%;\n }\n .col-md-push-10 {\n left: 83.33333333%;\n }\n .col-md-push-9 {\n left: 75%;\n }\n .col-md-push-8 {\n left: 66.66666667%;\n }\n .col-md-push-7 {\n left: 58.33333333%;\n }\n .col-md-push-6 {\n left: 50%;\n }\n .col-md-push-5 {\n left: 41.66666667%;\n }\n .col-md-push-4 {\n left: 33.33333333%;\n }\n .col-md-push-3 {\n left: 25%;\n }\n .col-md-push-2 {\n left: 16.66666667%;\n }\n .col-md-push-1 {\n left: 8.33333333%;\n }\n .col-md-push-0 {\n left: auto;\n }\n .col-md-offset-12 {\n margin-left: 100%;\n }\n .col-md-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-md-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-md-offset-9 {\n margin-left: 75%;\n }\n .col-md-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-md-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-md-offset-6 {\n margin-left: 50%;\n }\n .col-md-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-md-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-md-offset-3 {\n margin-left: 25%;\n }\n .col-md-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-md-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-md-offset-0 {\n margin-left: 0%;\n }\n}\n@media (min-width: 1200px) {\n .col-lg-1, .col-lg-2, .col-lg-3, .col-lg-4, .col-lg-5, .col-lg-6, .col-lg-7, .col-lg-8, .col-lg-9, .col-lg-10, .col-lg-11, .col-lg-12 {\n float: left;\n }\n .col-lg-12 {\n width: 100%;\n }\n .col-lg-11 {\n width: 91.66666667%;\n }\n .col-lg-10 {\n width: 83.33333333%;\n }\n .col-lg-9 {\n width: 75%;\n }\n .col-lg-8 {\n width: 66.66666667%;\n }\n .col-lg-7 {\n width: 58.33333333%;\n }\n .col-lg-6 {\n width: 50%;\n }\n .col-lg-5 {\n width: 41.66666667%;\n }\n .col-lg-4 {\n width: 33.33333333%;\n }\n .col-lg-3 {\n width: 25%;\n }\n .col-lg-2 {\n width: 16.66666667%;\n }\n .col-lg-1 {\n width: 8.33333333%;\n }\n .col-lg-pull-12 {\n right: 100%;\n }\n .col-lg-pull-11 {\n right: 91.66666667%;\n }\n .col-lg-pull-10 {\n right: 83.33333333%;\n }\n .col-lg-pull-9 {\n right: 75%;\n }\n .col-lg-pull-8 {\n right: 66.66666667%;\n }\n .col-lg-pull-7 {\n right: 58.33333333%;\n }\n .col-lg-pull-6 {\n right: 50%;\n }\n .col-lg-pull-5 {\n right: 41.66666667%;\n }\n .col-lg-pull-4 {\n right: 33.33333333%;\n }\n .col-lg-pull-3 {\n right: 25%;\n }\n .col-lg-pull-2 {\n right: 16.66666667%;\n }\n .col-lg-pull-1 {\n right: 8.33333333%;\n }\n .col-lg-pull-0 {\n right: auto;\n }\n .col-lg-push-12 {\n left: 100%;\n }\n .col-lg-push-11 {\n left: 91.66666667%;\n }\n .col-lg-push-10 {\n left: 83.33333333%;\n }\n .col-lg-push-9 {\n left: 75%;\n }\n .col-lg-push-8 {\n left: 66.66666667%;\n }\n .col-lg-push-7 {\n left: 58.33333333%;\n }\n .col-lg-push-6 {\n left: 50%;\n }\n .col-lg-push-5 {\n left: 41.66666667%;\n }\n .col-lg-push-4 {\n left: 33.33333333%;\n }\n .col-lg-push-3 {\n left: 25%;\n }\n .col-lg-push-2 {\n left: 16.66666667%;\n }\n .col-lg-push-1 {\n left: 8.33333333%;\n }\n .col-lg-push-0 {\n left: auto;\n }\n .col-lg-offset-12 {\n margin-left: 100%;\n }\n .col-lg-offset-11 {\n margin-left: 91.66666667%;\n }\n .col-lg-offset-10 {\n margin-left: 83.33333333%;\n }\n .col-lg-offset-9 {\n margin-left: 75%;\n }\n .col-lg-offset-8 {\n margin-left: 66.66666667%;\n }\n .col-lg-offset-7 {\n margin-left: 58.33333333%;\n }\n .col-lg-offset-6 {\n margin-left: 50%;\n }\n .col-lg-offset-5 {\n margin-left: 41.66666667%;\n }\n .col-lg-offset-4 {\n margin-left: 33.33333333%;\n }\n .col-lg-offset-3 {\n margin-left: 25%;\n }\n .col-lg-offset-2 {\n margin-left: 16.66666667%;\n }\n .col-lg-offset-1 {\n margin-left: 8.33333333%;\n }\n .col-lg-offset-0 {\n margin-left: 0%;\n }\n}\ntable {\n background-color: transparent;\n}\ncaption {\n padding-top: 8px;\n padding-bottom: 8px;\n color: #777777;\n text-align: left;\n}\nth {\n text-align: left;\n}\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: 20px;\n}\n.table > thead > tr > th,\n.table > tbody > tr > th,\n.table > tfoot > tr > th,\n.table > thead > tr > td,\n.table > tbody > tr > td,\n.table > tfoot > tr > td {\n padding: 8px;\n line-height: 1.42857143;\n vertical-align: top;\n border-top: 1px solid #dddddd;\n}\n.table > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid #dddddd;\n}\n.table > caption + thead > tr:first-child > th,\n.table > colgroup + thead > tr:first-child > th,\n.table > thead:first-child > tr:first-child > th,\n.table > caption + thead > tr:first-child > td,\n.table > colgroup + thead > tr:first-child > td,\n.table > thead:first-child > tr:first-child > td {\n border-top: 0;\n}\n.table > tbody + tbody {\n border-top: 2px solid #dddddd;\n}\n.table .table {\n background-color: #ffffff;\n}\n.table-condensed > thead > tr > th,\n.table-condensed > tbody > tr > th,\n.table-condensed > tfoot > tr > th,\n.table-condensed > thead > tr > td,\n.table-condensed > tbody > tr > td,\n.table-condensed > tfoot > tr > td {\n padding: 5px;\n}\n.table-bordered {\n border: 1px solid #dddddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > tbody > tr > th,\n.table-bordered > tfoot > tr > th,\n.table-bordered > thead > tr > td,\n.table-bordered > tbody > tr > td,\n.table-bordered > tfoot > tr > td {\n border: 1px solid #dddddd;\n}\n.table-bordered > thead > tr > th,\n.table-bordered > thead > tr > td {\n border-bottom-width: 2px;\n}\n.table-striped > tbody > tr:nth-child(odd) {\n background-color: #f9f9f9;\n}\n.table-hover > tbody > tr:hover {\n background-color: #f5f5f5;\n}\ntable col[class*=\"col-\"] {\n position: static;\n float: none;\n display: table-column;\n}\ntable td[class*=\"col-\"],\ntable th[class*=\"col-\"] {\n position: static;\n float: none;\n display: table-cell;\n}\n.table > thead > tr > td.active,\n.table > tbody > tr > td.active,\n.table > tfoot > tr > td.active,\n.table > thead > tr > th.active,\n.table > tbody > tr > th.active,\n.table > tfoot > tr > th.active,\n.table > thead > tr.active > td,\n.table > tbody > tr.active > td,\n.table > tfoot > tr.active > td,\n.table > thead > tr.active > th,\n.table > tbody > tr.active > th,\n.table > tfoot > tr.active > th {\n background-color: #f5f5f5;\n}\n.table-hover > tbody > tr > td.active:hover,\n.table-hover > tbody > tr > th.active:hover,\n.table-hover > tbody > tr.active:hover > td,\n.table-hover > tbody > tr:hover > .active,\n.table-hover > tbody > tr.active:hover > th {\n background-color: #e8e8e8;\n}\n.table > thead > tr > td.success,\n.table > tbody > tr > td.success,\n.table > tfoot > tr > td.success,\n.table > thead > tr > th.success,\n.table > tbody > tr > th.success,\n.table > tfoot > tr > th.success,\n.table > thead > tr.success > td,\n.table > tbody > tr.success > td,\n.table > tfoot > tr.success > td,\n.table > thead > tr.success > th,\n.table > tbody > tr.success > th,\n.table > tfoot > tr.success > th {\n background-color: #dff0d8;\n}\n.table-hover > tbody > tr > td.success:hover,\n.table-hover > tbody > tr > th.success:hover,\n.table-hover > tbody > tr.success:hover > td,\n.table-hover > tbody > tr:hover > .success,\n.table-hover > tbody > tr.success:hover > th {\n background-color: #d0e9c6;\n}\n.table > thead > tr > td.info,\n.table > tbody > tr > td.info,\n.table > tfoot > tr > td.info,\n.table > thead > tr > th.info,\n.table > tbody > tr > th.info,\n.table > tfoot > tr > th.info,\n.table > thead > tr.info > td,\n.table > tbody > tr.info > td,\n.table > tfoot > tr.info > td,\n.table > thead > tr.info > th,\n.table > tbody > tr.info > th,\n.table > tfoot > tr.info > th {\n background-color: #d9edf7;\n}\n.table-hover > tbody > tr > td.info:hover,\n.table-hover > tbody > tr > th.info:hover,\n.table-hover > tbody > tr.info:hover > td,\n.table-hover > tbody > tr:hover > .info,\n.table-hover > tbody > tr.info:hover > th {\n background-color: #c4e3f3;\n}\n.table > thead > tr > td.warning,\n.table > tbody > tr > td.warning,\n.table > tfoot > tr > td.warning,\n.table > thead > tr > th.warning,\n.table > tbody > tr > th.warning,\n.table > tfoot > tr > th.warning,\n.table > thead > tr.warning > td,\n.table > tbody > tr.warning > td,\n.table > tfoot > tr.warning > td,\n.table > thead > tr.warning > th,\n.table > tbody > tr.warning > th,\n.table > tfoot > tr.warning > th {\n background-color: #fcf8e3;\n}\n.table-hover > tbody > tr > td.warning:hover,\n.table-hover > tbody > tr > th.warning:hover,\n.table-hover > tbody > tr.warning:hover > td,\n.table-hover > tbody > tr:hover > .warning,\n.table-hover > tbody > tr.warning:hover > th {\n background-color: #faf2cc;\n}\n.table > thead > tr > td.danger,\n.table > tbody > tr > td.danger,\n.table > tfoot > tr > td.danger,\n.table > thead > tr > th.danger,\n.table > tbody > tr > th.danger,\n.table > tfoot > tr > th.danger,\n.table > thead > tr.danger > td,\n.table > tbody > tr.danger > td,\n.table > tfoot > tr.danger > td,\n.table > thead > tr.danger > th,\n.table > tbody > tr.danger > th,\n.table > tfoot > tr.danger > th {\n background-color: #f2dede;\n}\n.table-hover > tbody > tr > td.danger:hover,\n.table-hover > tbody > tr > th.danger:hover,\n.table-hover > tbody > tr.danger:hover > td,\n.table-hover > tbody > tr:hover > .danger,\n.table-hover > tbody > tr.danger:hover > th {\n background-color: #ebcccc;\n}\n.table-responsive {\n overflow-x: auto;\n min-height: 0.01%;\n}\n@media screen and (max-width: 767px) {\n .table-responsive {\n width: 100%;\n margin-bottom: 15px;\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid #dddddd;\n }\n .table-responsive > .table {\n margin-bottom: 0;\n }\n .table-responsive > .table > thead > tr > th,\n .table-responsive > .table > tbody > tr > th,\n .table-responsive > .table > tfoot > tr > th,\n .table-responsive > .table > thead > tr > td,\n .table-responsive > .table > tbody > tr > td,\n .table-responsive > .table > tfoot > tr > td {\n white-space: nowrap;\n }\n .table-responsive > .table-bordered {\n border: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:first-child,\n .table-responsive > .table-bordered > tbody > tr > th:first-child,\n .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n .table-responsive > .table-bordered > thead > tr > td:first-child,\n .table-responsive > .table-bordered > tbody > tr > td:first-child,\n .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n }\n .table-responsive > .table-bordered > thead > tr > th:last-child,\n .table-responsive > .table-bordered > tbody > tr > th:last-child,\n .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n .table-responsive > .table-bordered > thead > tr > td:last-child,\n .table-responsive > .table-bordered > tbody > tr > td:last-child,\n .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n }\n .table-responsive > .table-bordered > tbody > tr:last-child > th,\n .table-responsive > .table-bordered > tfoot > tr:last-child > th,\n .table-responsive > .table-bordered > tbody > tr:last-child > td,\n .table-responsive > .table-bordered > tfoot > tr:last-child > td {\n border-bottom: 0;\n }\n}\nfieldset {\n padding: 0;\n margin: 0;\n border: 0;\n min-width: 0;\n}\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: 20px;\n font-size: 21px;\n line-height: inherit;\n color: #333333;\n border: 0;\n border-bottom: 1px solid #e5e5e5;\n}\nlabel {\n display: inline-block;\n max-width: 100%;\n margin-bottom: 5px;\n font-weight: bold;\n}\ninput[type=\"search\"] {\n -webkit-box-sizing: border-box;\n -moz-box-sizing: border-box;\n box-sizing: border-box;\n}\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9;\n line-height: normal;\n}\ninput[type=\"file\"] {\n display: block;\n}\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\nselect[multiple],\nselect[size] {\n height: auto;\n}\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n outline: thin dotted;\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\noutput {\n display: block;\n padding-top: 7px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n}\n.form-control {\n display: block;\n width: 100%;\n height: 34px;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n color: #555555;\n background-color: #ffffff;\n background-image: none;\n border: 1px solid #cccccc;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n -webkit-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n -o-transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n transition: border-color ease-in-out .15s, box-shadow ease-in-out .15s;\n}\n.form-control:focus {\n border-color: #66afe9;\n outline: 0;\n -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 8px rgba(102, 175, 233, 0.6);\n}\n.form-control::-moz-placeholder {\n color: #999999;\n opacity: 1;\n}\n.form-control:-ms-input-placeholder {\n color: #999999;\n}\n.form-control::-webkit-input-placeholder {\n color: #999999;\n}\n.form-control[disabled],\n.form-control[readonly],\nfieldset[disabled] .form-control {\n cursor: not-allowed;\n background-color: #eeeeee;\n opacity: 1;\n}\ntextarea.form-control {\n height: auto;\n}\ninput[type=\"search\"] {\n -webkit-appearance: none;\n}\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"],\n input[type=\"time\"],\n input[type=\"datetime-local\"],\n input[type=\"month\"] {\n line-height: 34px;\n }\n input[type=\"date\"].input-sm,\n input[type=\"time\"].input-sm,\n input[type=\"datetime-local\"].input-sm,\n input[type=\"month\"].input-sm {\n line-height: 30px;\n }\n input[type=\"date\"].input-lg,\n input[type=\"time\"].input-lg,\n input[type=\"datetime-local\"].input-lg,\n input[type=\"month\"].input-lg {\n line-height: 46px;\n }\n}\n.form-group {\n margin-bottom: 15px;\n}\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.radio label,\n.checkbox label {\n min-height: 20px;\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: normal;\n cursor: pointer;\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-left: -20px;\n margin-top: 4px \\9;\n}\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px;\n}\n.radio-inline,\n.checkbox-inline {\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n vertical-align: middle;\n font-weight: normal;\n cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px;\n}\ninput[type=\"radio\"][disabled],\ninput[type=\"checkbox\"][disabled],\ninput[type=\"radio\"].disabled,\ninput[type=\"checkbox\"].disabled,\nfieldset[disabled] input[type=\"radio\"],\nfieldset[disabled] input[type=\"checkbox\"] {\n cursor: not-allowed;\n}\n.radio-inline.disabled,\n.checkbox-inline.disabled,\nfieldset[disabled] .radio-inline,\nfieldset[disabled] .checkbox-inline {\n cursor: not-allowed;\n}\n.radio.disabled label,\n.checkbox.disabled label,\nfieldset[disabled] .radio label,\nfieldset[disabled] .checkbox label {\n cursor: not-allowed;\n}\n.form-control-static {\n padding-top: 7px;\n padding-bottom: 7px;\n margin-bottom: 0;\n}\n.form-control-static.input-lg,\n.form-control-static.input-sm {\n padding-left: 0;\n padding-right: 0;\n}\n.input-sm,\n.form-group-sm .form-control {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-sm,\nselect.form-group-sm .form-control {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-sm,\ntextarea.form-group-sm .form-control,\nselect[multiple].input-sm,\nselect[multiple].form-group-sm .form-control {\n height: auto;\n}\n.input-lg,\n.form-group-lg .form-control {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.33;\n border-radius: 6px;\n}\nselect.input-lg,\nselect.form-group-lg .form-control {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-lg,\ntextarea.form-group-lg .form-control,\nselect[multiple].input-lg,\nselect[multiple].form-group-lg .form-control {\n height: auto;\n}\n.has-feedback {\n position: relative;\n}\n.has-feedback .form-control {\n padding-right: 42.5px;\n}\n.form-control-feedback {\n position: absolute;\n top: 0;\n right: 0;\n z-index: 2;\n display: block;\n width: 34px;\n height: 34px;\n line-height: 34px;\n text-align: center;\n pointer-events: none;\n}\n.input-lg + .form-control-feedback {\n width: 46px;\n height: 46px;\n line-height: 46px;\n}\n.input-sm + .form-control-feedback {\n width: 30px;\n height: 30px;\n line-height: 30px;\n}\n.has-success .help-block,\n.has-success .control-label,\n.has-success .radio,\n.has-success .checkbox,\n.has-success .radio-inline,\n.has-success .checkbox-inline,\n.has-success.radio label,\n.has-success.checkbox label,\n.has-success.radio-inline label,\n.has-success.checkbox-inline label {\n color: #3c763d;\n}\n.has-success .form-control {\n border-color: #3c763d;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-success .form-control:focus {\n border-color: #2b542c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #67b168;\n}\n.has-success .input-group-addon {\n color: #3c763d;\n border-color: #3c763d;\n background-color: #dff0d8;\n}\n.has-success .form-control-feedback {\n color: #3c763d;\n}\n.has-warning .help-block,\n.has-warning .control-label,\n.has-warning .radio,\n.has-warning .checkbox,\n.has-warning .radio-inline,\n.has-warning .checkbox-inline,\n.has-warning.radio label,\n.has-warning.checkbox label,\n.has-warning.radio-inline label,\n.has-warning.checkbox-inline label {\n color: #8a6d3b;\n}\n.has-warning .form-control {\n border-color: #8a6d3b;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-warning .form-control:focus {\n border-color: #66512c;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #c0a16b;\n}\n.has-warning .input-group-addon {\n color: #8a6d3b;\n border-color: #8a6d3b;\n background-color: #fcf8e3;\n}\n.has-warning .form-control-feedback {\n color: #8a6d3b;\n}\n.has-error .help-block,\n.has-error .control-label,\n.has-error .radio,\n.has-error .checkbox,\n.has-error .radio-inline,\n.has-error .checkbox-inline,\n.has-error.radio label,\n.has-error.checkbox label,\n.has-error.radio-inline label,\n.has-error.checkbox-inline label {\n color: #a94442;\n}\n.has-error .form-control {\n border-color: #a94442;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075);\n}\n.has-error .form-control:focus {\n border-color: #843534;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075), 0 0 6px #ce8483;\n}\n.has-error .input-group-addon {\n color: #a94442;\n border-color: #a94442;\n background-color: #f2dede;\n}\n.has-error .form-control-feedback {\n color: #a94442;\n}\n.has-feedback label ~ .form-control-feedback {\n top: 25px;\n}\n.has-feedback label.sr-only ~ .form-control-feedback {\n top: 0;\n}\n.help-block {\n display: block;\n margin-top: 5px;\n margin-bottom: 10px;\n color: #737373;\n}\n@media (min-width: 768px) {\n .form-inline .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .form-inline .form-control-static {\n display: inline-block;\n }\n .form-inline .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .form-inline .input-group .input-group-addon,\n .form-inline .input-group .input-group-btn,\n .form-inline .input-group .form-control {\n width: auto;\n }\n .form-inline .input-group > .form-control {\n width: 100%;\n }\n .form-inline .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio,\n .form-inline .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .form-inline .radio label,\n .form-inline .checkbox label {\n padding-left: 0;\n }\n .form-inline .radio input[type=\"radio\"],\n .form-inline .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .form-inline .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox,\n.form-horizontal .radio-inline,\n.form-horizontal .checkbox-inline {\n margin-top: 0;\n margin-bottom: 0;\n padding-top: 7px;\n}\n.form-horizontal .radio,\n.form-horizontal .checkbox {\n min-height: 27px;\n}\n.form-horizontal .form-group {\n margin-left: -15px;\n margin-right: -15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .control-label {\n text-align: right;\n margin-bottom: 0;\n padding-top: 7px;\n }\n}\n.form-horizontal .has-feedback .form-control-feedback {\n right: 15px;\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-lg .control-label {\n padding-top: 14.3px;\n }\n}\n@media (min-width: 768px) {\n .form-horizontal .form-group-sm .control-label {\n padding-top: 6px;\n }\n}\n.btn {\n display: inline-block;\n margin-bottom: 0;\n font-weight: normal;\n text-align: center;\n vertical-align: middle;\n touch-action: manipulation;\n cursor: pointer;\n background-image: none;\n border: 1px solid transparent;\n white-space: nowrap;\n padding: 6px 12px;\n font-size: 14px;\n line-height: 1.42857143;\n border-radius: 4px;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n.btn:focus,\n.btn:active:focus,\n.btn.active:focus,\n.btn.focus,\n.btn:active.focus,\n.btn.active.focus {\n outline: thin dotted;\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n.btn:hover,\n.btn:focus,\n.btn.focus {\n color: #333333;\n text-decoration: none;\n}\n.btn:active,\n.btn.active {\n outline: 0;\n background-image: none;\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn.disabled,\n.btn[disabled],\nfieldset[disabled] .btn {\n cursor: not-allowed;\n pointer-events: none;\n opacity: 0.65;\n filter: alpha(opacity=65);\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-default {\n color: #333333;\n background-color: #ffffff;\n border-color: #cccccc;\n}\n.btn-default:hover,\n.btn-default:focus,\n.btn-default.focus,\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n color: #333333;\n background-color: #e6e6e6;\n border-color: #adadad;\n}\n.btn-default:active,\n.btn-default.active,\n.open > .dropdown-toggle.btn-default {\n background-image: none;\n}\n.btn-default.disabled,\n.btn-default[disabled],\nfieldset[disabled] .btn-default,\n.btn-default.disabled:hover,\n.btn-default[disabled]:hover,\nfieldset[disabled] .btn-default:hover,\n.btn-default.disabled:focus,\n.btn-default[disabled]:focus,\nfieldset[disabled] .btn-default:focus,\n.btn-default.disabled.focus,\n.btn-default[disabled].focus,\nfieldset[disabled] .btn-default.focus,\n.btn-default.disabled:active,\n.btn-default[disabled]:active,\nfieldset[disabled] .btn-default:active,\n.btn-default.disabled.active,\n.btn-default[disabled].active,\nfieldset[disabled] .btn-default.active {\n background-color: #ffffff;\n border-color: #cccccc;\n}\n.btn-default .badge {\n color: #ffffff;\n background-color: #333333;\n}\n.btn-primary {\n color: #ffffff;\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary:hover,\n.btn-primary:focus,\n.btn-primary.focus,\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n color: #ffffff;\n background-color: #286090;\n border-color: #204d74;\n}\n.btn-primary:active,\n.btn-primary.active,\n.open > .dropdown-toggle.btn-primary {\n background-image: none;\n}\n.btn-primary.disabled,\n.btn-primary[disabled],\nfieldset[disabled] .btn-primary,\n.btn-primary.disabled:hover,\n.btn-primary[disabled]:hover,\nfieldset[disabled] .btn-primary:hover,\n.btn-primary.disabled:focus,\n.btn-primary[disabled]:focus,\nfieldset[disabled] .btn-primary:focus,\n.btn-primary.disabled.focus,\n.btn-primary[disabled].focus,\nfieldset[disabled] .btn-primary.focus,\n.btn-primary.disabled:active,\n.btn-primary[disabled]:active,\nfieldset[disabled] .btn-primary:active,\n.btn-primary.disabled.active,\n.btn-primary[disabled].active,\nfieldset[disabled] .btn-primary.active {\n background-color: #337ab7;\n border-color: #2e6da4;\n}\n.btn-primary .badge {\n color: #337ab7;\n background-color: #ffffff;\n}\n.btn-success {\n color: #ffffff;\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success:hover,\n.btn-success:focus,\n.btn-success.focus,\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n color: #ffffff;\n background-color: #449d44;\n border-color: #398439;\n}\n.btn-success:active,\n.btn-success.active,\n.open > .dropdown-toggle.btn-success {\n background-image: none;\n}\n.btn-success.disabled,\n.btn-success[disabled],\nfieldset[disabled] .btn-success,\n.btn-success.disabled:hover,\n.btn-success[disabled]:hover,\nfieldset[disabled] .btn-success:hover,\n.btn-success.disabled:focus,\n.btn-success[disabled]:focus,\nfieldset[disabled] .btn-success:focus,\n.btn-success.disabled.focus,\n.btn-success[disabled].focus,\nfieldset[disabled] .btn-success.focus,\n.btn-success.disabled:active,\n.btn-success[disabled]:active,\nfieldset[disabled] .btn-success:active,\n.btn-success.disabled.active,\n.btn-success[disabled].active,\nfieldset[disabled] .btn-success.active {\n background-color: #5cb85c;\n border-color: #4cae4c;\n}\n.btn-success .badge {\n color: #5cb85c;\n background-color: #ffffff;\n}\n.btn-info {\n color: #ffffff;\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info:hover,\n.btn-info:focus,\n.btn-info.focus,\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n color: #ffffff;\n background-color: #31b0d5;\n border-color: #269abc;\n}\n.btn-info:active,\n.btn-info.active,\n.open > .dropdown-toggle.btn-info {\n background-image: none;\n}\n.btn-info.disabled,\n.btn-info[disabled],\nfieldset[disabled] .btn-info,\n.btn-info.disabled:hover,\n.btn-info[disabled]:hover,\nfieldset[disabled] .btn-info:hover,\n.btn-info.disabled:focus,\n.btn-info[disabled]:focus,\nfieldset[disabled] .btn-info:focus,\n.btn-info.disabled.focus,\n.btn-info[disabled].focus,\nfieldset[disabled] .btn-info.focus,\n.btn-info.disabled:active,\n.btn-info[disabled]:active,\nfieldset[disabled] .btn-info:active,\n.btn-info.disabled.active,\n.btn-info[disabled].active,\nfieldset[disabled] .btn-info.active {\n background-color: #5bc0de;\n border-color: #46b8da;\n}\n.btn-info .badge {\n color: #5bc0de;\n background-color: #ffffff;\n}\n.btn-warning {\n color: #ffffff;\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning:hover,\n.btn-warning:focus,\n.btn-warning.focus,\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n color: #ffffff;\n background-color: #ec971f;\n border-color: #d58512;\n}\n.btn-warning:active,\n.btn-warning.active,\n.open > .dropdown-toggle.btn-warning {\n background-image: none;\n}\n.btn-warning.disabled,\n.btn-warning[disabled],\nfieldset[disabled] .btn-warning,\n.btn-warning.disabled:hover,\n.btn-warning[disabled]:hover,\nfieldset[disabled] .btn-warning:hover,\n.btn-warning.disabled:focus,\n.btn-warning[disabled]:focus,\nfieldset[disabled] .btn-warning:focus,\n.btn-warning.disabled.focus,\n.btn-warning[disabled].focus,\nfieldset[disabled] .btn-warning.focus,\n.btn-warning.disabled:active,\n.btn-warning[disabled]:active,\nfieldset[disabled] .btn-warning:active,\n.btn-warning.disabled.active,\n.btn-warning[disabled].active,\nfieldset[disabled] .btn-warning.active {\n background-color: #f0ad4e;\n border-color: #eea236;\n}\n.btn-warning .badge {\n color: #f0ad4e;\n background-color: #ffffff;\n}\n.btn-danger {\n color: #ffffff;\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger:hover,\n.btn-danger:focus,\n.btn-danger.focus,\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n color: #ffffff;\n background-color: #c9302c;\n border-color: #ac2925;\n}\n.btn-danger:active,\n.btn-danger.active,\n.open > .dropdown-toggle.btn-danger {\n background-image: none;\n}\n.btn-danger.disabled,\n.btn-danger[disabled],\nfieldset[disabled] .btn-danger,\n.btn-danger.disabled:hover,\n.btn-danger[disabled]:hover,\nfieldset[disabled] .btn-danger:hover,\n.btn-danger.disabled:focus,\n.btn-danger[disabled]:focus,\nfieldset[disabled] .btn-danger:focus,\n.btn-danger.disabled.focus,\n.btn-danger[disabled].focus,\nfieldset[disabled] .btn-danger.focus,\n.btn-danger.disabled:active,\n.btn-danger[disabled]:active,\nfieldset[disabled] .btn-danger:active,\n.btn-danger.disabled.active,\n.btn-danger[disabled].active,\nfieldset[disabled] .btn-danger.active {\n background-color: #d9534f;\n border-color: #d43f3a;\n}\n.btn-danger .badge {\n color: #d9534f;\n background-color: #ffffff;\n}\n.btn-link {\n color: #337ab7;\n font-weight: normal;\n border-radius: 0;\n}\n.btn-link,\n.btn-link:active,\n.btn-link.active,\n.btn-link[disabled],\nfieldset[disabled] .btn-link {\n background-color: transparent;\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn-link,\n.btn-link:hover,\n.btn-link:focus,\n.btn-link:active {\n border-color: transparent;\n}\n.btn-link:hover,\n.btn-link:focus {\n color: #23527c;\n text-decoration: underline;\n background-color: transparent;\n}\n.btn-link[disabled]:hover,\nfieldset[disabled] .btn-link:hover,\n.btn-link[disabled]:focus,\nfieldset[disabled] .btn-link:focus {\n color: #777777;\n text-decoration: none;\n}\n.btn-lg,\n.btn-group-lg > .btn {\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.33;\n border-radius: 6px;\n}\n.btn-sm,\n.btn-group-sm > .btn {\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-xs,\n.btn-group-xs > .btn {\n padding: 1px 5px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\n.btn-block {\n display: block;\n width: 100%;\n}\n.btn-block + .btn-block {\n margin-top: 5px;\n}\ninput[type=\"submit\"].btn-block,\ninput[type=\"reset\"].btn-block,\ninput[type=\"button\"].btn-block {\n width: 100%;\n}\n.fade {\n opacity: 0;\n -webkit-transition: opacity 0.15s linear;\n -o-transition: opacity 0.15s linear;\n transition: opacity 0.15s linear;\n}\n.fade.in {\n opacity: 1;\n}\n.collapse {\n display: none;\n visibility: hidden;\n}\n.collapse.in {\n display: block;\n visibility: visible;\n}\ntr.collapse.in {\n display: table-row;\n}\ntbody.collapse.in {\n display: table-row-group;\n}\n.collapsing {\n position: relative;\n height: 0;\n overflow: hidden;\n -webkit-transition-property: height, visibility;\n transition-property: height, visibility;\n -webkit-transition-duration: 0.35s;\n transition-duration: 0.35s;\n -webkit-transition-timing-function: ease;\n transition-timing-function: ease;\n}\n.caret {\n display: inline-block;\n width: 0;\n height: 0;\n margin-left: 2px;\n vertical-align: middle;\n border-top: 4px solid;\n border-right: 4px solid transparent;\n border-left: 4px solid transparent;\n}\n.dropdown {\n position: relative;\n}\n.dropdown-toggle:focus {\n outline: 0;\n}\n.dropdown-menu {\n position: absolute;\n top: 100%;\n left: 0;\n z-index: 1000;\n display: none;\n float: left;\n min-width: 160px;\n padding: 5px 0;\n margin: 2px 0 0;\n list-style: none;\n font-size: 14px;\n text-align: left;\n background-color: #ffffff;\n border: 1px solid #cccccc;\n border: 1px solid rgba(0, 0, 0, 0.15);\n border-radius: 4px;\n -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);\n background-clip: padding-box;\n}\n.dropdown-menu.pull-right {\n right: 0;\n left: auto;\n}\n.dropdown-menu .divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.dropdown-menu > li > a {\n display: block;\n padding: 3px 20px;\n clear: both;\n font-weight: normal;\n line-height: 1.42857143;\n color: #333333;\n white-space: nowrap;\n}\n.dropdown-menu > li > a:hover,\n.dropdown-menu > li > a:focus {\n text-decoration: none;\n color: #262626;\n background-color: #f5f5f5;\n}\n.dropdown-menu > .active > a,\n.dropdown-menu > .active > a:hover,\n.dropdown-menu > .active > a:focus {\n color: #ffffff;\n text-decoration: none;\n outline: 0;\n background-color: #337ab7;\n}\n.dropdown-menu > .disabled > a,\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n color: #777777;\n}\n.dropdown-menu > .disabled > a:hover,\n.dropdown-menu > .disabled > a:focus {\n text-decoration: none;\n background-color: transparent;\n background-image: none;\n filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);\n cursor: not-allowed;\n}\n.open > .dropdown-menu {\n display: block;\n}\n.open > a {\n outline: 0;\n}\n.dropdown-menu-right {\n left: auto;\n right: 0;\n}\n.dropdown-menu-left {\n left: 0;\n right: auto;\n}\n.dropdown-header {\n display: block;\n padding: 3px 20px;\n font-size: 12px;\n line-height: 1.42857143;\n color: #777777;\n white-space: nowrap;\n}\n.dropdown-backdrop {\n position: fixed;\n left: 0;\n right: 0;\n bottom: 0;\n top: 0;\n z-index: 990;\n}\n.pull-right > .dropdown-menu {\n right: 0;\n left: auto;\n}\n.dropup .caret,\n.navbar-fixed-bottom .dropdown .caret {\n border-top: 0;\n border-bottom: 4px solid;\n content: \"\";\n}\n.dropup .dropdown-menu,\n.navbar-fixed-bottom .dropdown .dropdown-menu {\n top: auto;\n bottom: 100%;\n margin-bottom: 1px;\n}\n@media (min-width: 768px) {\n .navbar-right .dropdown-menu {\n left: auto;\n right: 0;\n }\n .navbar-right .dropdown-menu-left {\n left: 0;\n right: auto;\n }\n}\n.btn-group,\n.btn-group-vertical {\n position: relative;\n display: inline-block;\n vertical-align: middle;\n}\n.btn-group > .btn,\n.btn-group-vertical > .btn {\n position: relative;\n float: left;\n}\n.btn-group > .btn:hover,\n.btn-group-vertical > .btn:hover,\n.btn-group > .btn:focus,\n.btn-group-vertical > .btn:focus,\n.btn-group > .btn:active,\n.btn-group-vertical > .btn:active,\n.btn-group > .btn.active,\n.btn-group-vertical > .btn.active {\n z-index: 2;\n}\n.btn-group .btn + .btn,\n.btn-group .btn + .btn-group,\n.btn-group .btn-group + .btn,\n.btn-group .btn-group + .btn-group {\n margin-left: -1px;\n}\n.btn-toolbar {\n margin-left: -5px;\n}\n.btn-toolbar .btn-group,\n.btn-toolbar .input-group {\n float: left;\n}\n.btn-toolbar > .btn,\n.btn-toolbar > .btn-group,\n.btn-toolbar > .input-group {\n margin-left: 5px;\n}\n.btn-group > .btn:not(:first-child):not(:last-child):not(.dropdown-toggle) {\n border-radius: 0;\n}\n.btn-group > .btn:first-child {\n margin-left: 0;\n}\n.btn-group > .btn:first-child:not(:last-child):not(.dropdown-toggle) {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group > .btn:last-child:not(:first-child),\n.btn-group > .dropdown-toggle:not(:first-child) {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group > .btn-group {\n float: left;\n}\n.btn-group > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group > .btn-group:first-child > .btn:last-child,\n.btn-group > .btn-group:first-child > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.btn-group > .btn-group:last-child > .btn:first-child {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group .dropdown-toggle:active,\n.btn-group.open .dropdown-toggle {\n outline: 0;\n}\n.btn-group > .btn + .dropdown-toggle {\n padding-left: 8px;\n padding-right: 8px;\n}\n.btn-group > .btn-lg + .dropdown-toggle {\n padding-left: 12px;\n padding-right: 12px;\n}\n.btn-group.open .dropdown-toggle {\n -webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);\n}\n.btn-group.open .dropdown-toggle.btn-link {\n -webkit-box-shadow: none;\n box-shadow: none;\n}\n.btn .caret {\n margin-left: 0;\n}\n.btn-lg .caret {\n border-width: 5px 5px 0;\n border-bottom-width: 0;\n}\n.dropup .btn-lg .caret {\n border-width: 0 5px 5px;\n}\n.btn-group-vertical > .btn,\n.btn-group-vertical > .btn-group,\n.btn-group-vertical > .btn-group > .btn {\n display: block;\n float: none;\n width: 100%;\n max-width: 100%;\n}\n.btn-group-vertical > .btn-group > .btn {\n float: none;\n}\n.btn-group-vertical > .btn + .btn,\n.btn-group-vertical > .btn + .btn-group,\n.btn-group-vertical > .btn-group + .btn,\n.btn-group-vertical > .btn-group + .btn-group {\n margin-top: -1px;\n margin-left: 0;\n}\n.btn-group-vertical > .btn:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.btn-group-vertical > .btn:first-child:not(:last-child) {\n border-top-right-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn:last-child:not(:first-child) {\n border-bottom-left-radius: 4px;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:not(:first-child):not(:last-child) > .btn {\n border-radius: 0;\n}\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .btn:last-child,\n.btn-group-vertical > .btn-group:first-child:not(:last-child) > .dropdown-toggle {\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.btn-group-vertical > .btn-group:last-child:not(:first-child) > .btn:first-child {\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.btn-group-justified {\n display: table;\n width: 100%;\n table-layout: fixed;\n border-collapse: separate;\n}\n.btn-group-justified > .btn,\n.btn-group-justified > .btn-group {\n float: none;\n display: table-cell;\n width: 1%;\n}\n.btn-group-justified > .btn-group .btn {\n width: 100%;\n}\n.btn-group-justified > .btn-group .dropdown-menu {\n left: auto;\n}\n[data-toggle=\"buttons\"] > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"radio\"],\n[data-toggle=\"buttons\"] > .btn input[type=\"checkbox\"],\n[data-toggle=\"buttons\"] > .btn-group > .btn input[type=\"checkbox\"] {\n position: absolute;\n clip: rect(0, 0, 0, 0);\n pointer-events: none;\n}\n.input-group {\n position: relative;\n display: table;\n border-collapse: separate;\n}\n.input-group[class*=\"col-\"] {\n float: none;\n padding-left: 0;\n padding-right: 0;\n}\n.input-group .form-control {\n position: relative;\n z-index: 2;\n float: left;\n width: 100%;\n margin-bottom: 0;\n}\n.input-group-lg > .form-control,\n.input-group-lg > .input-group-addon,\n.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n padding: 10px 16px;\n font-size: 18px;\n line-height: 1.33;\n border-radius: 6px;\n}\nselect.input-group-lg > .form-control,\nselect.input-group-lg > .input-group-addon,\nselect.input-group-lg > .input-group-btn > .btn {\n height: 46px;\n line-height: 46px;\n}\ntextarea.input-group-lg > .form-control,\ntextarea.input-group-lg > .input-group-addon,\ntextarea.input-group-lg > .input-group-btn > .btn,\nselect[multiple].input-group-lg > .form-control,\nselect[multiple].input-group-lg > .input-group-addon,\nselect[multiple].input-group-lg > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-sm > .form-control,\n.input-group-sm > .input-group-addon,\n.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n padding: 5px 10px;\n font-size: 12px;\n line-height: 1.5;\n border-radius: 3px;\n}\nselect.input-group-sm > .form-control,\nselect.input-group-sm > .input-group-addon,\nselect.input-group-sm > .input-group-btn > .btn {\n height: 30px;\n line-height: 30px;\n}\ntextarea.input-group-sm > .form-control,\ntextarea.input-group-sm > .input-group-addon,\ntextarea.input-group-sm > .input-group-btn > .btn,\nselect[multiple].input-group-sm > .form-control,\nselect[multiple].input-group-sm > .input-group-addon,\nselect[multiple].input-group-sm > .input-group-btn > .btn {\n height: auto;\n}\n.input-group-addon,\n.input-group-btn,\n.input-group .form-control {\n display: table-cell;\n}\n.input-group-addon:not(:first-child):not(:last-child),\n.input-group-btn:not(:first-child):not(:last-child),\n.input-group .form-control:not(:first-child):not(:last-child) {\n border-radius: 0;\n}\n.input-group-addon,\n.input-group-btn {\n width: 1%;\n white-space: nowrap;\n vertical-align: middle;\n}\n.input-group-addon {\n padding: 6px 12px;\n font-size: 14px;\n font-weight: normal;\n line-height: 1;\n color: #555555;\n text-align: center;\n background-color: #eeeeee;\n border: 1px solid #cccccc;\n border-radius: 4px;\n}\n.input-group-addon.input-sm {\n padding: 5px 10px;\n font-size: 12px;\n border-radius: 3px;\n}\n.input-group-addon.input-lg {\n padding: 10px 16px;\n font-size: 18px;\n border-radius: 6px;\n}\n.input-group-addon input[type=\"radio\"],\n.input-group-addon input[type=\"checkbox\"] {\n margin-top: 0;\n}\n.input-group .form-control:first-child,\n.input-group-addon:first-child,\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group > .btn,\n.input-group-btn:first-child > .dropdown-toggle,\n.input-group-btn:last-child > .btn:not(:last-child):not(.dropdown-toggle),\n.input-group-btn:last-child > .btn-group:not(:last-child) > .btn {\n border-bottom-right-radius: 0;\n border-top-right-radius: 0;\n}\n.input-group-addon:first-child {\n border-right: 0;\n}\n.input-group .form-control:last-child,\n.input-group-addon:last-child,\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group > .btn,\n.input-group-btn:last-child > .dropdown-toggle,\n.input-group-btn:first-child > .btn:not(:first-child),\n.input-group-btn:first-child > .btn-group:not(:first-child) > .btn {\n border-bottom-left-radius: 0;\n border-top-left-radius: 0;\n}\n.input-group-addon:last-child {\n border-left: 0;\n}\n.input-group-btn {\n position: relative;\n font-size: 0;\n white-space: nowrap;\n}\n.input-group-btn > .btn {\n position: relative;\n}\n.input-group-btn > .btn + .btn {\n margin-left: -1px;\n}\n.input-group-btn > .btn:hover,\n.input-group-btn > .btn:focus,\n.input-group-btn > .btn:active {\n z-index: 2;\n}\n.input-group-btn:first-child > .btn,\n.input-group-btn:first-child > .btn-group {\n margin-right: -1px;\n}\n.input-group-btn:last-child > .btn,\n.input-group-btn:last-child > .btn-group {\n margin-left: -1px;\n}\n.nav {\n margin-bottom: 0;\n padding-left: 0;\n list-style: none;\n}\n.nav > li {\n position: relative;\n display: block;\n}\n.nav > li > a {\n position: relative;\n display: block;\n padding: 10px 15px;\n}\n.nav > li > a:hover,\n.nav > li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.nav > li.disabled > a {\n color: #777777;\n}\n.nav > li.disabled > a:hover,\n.nav > li.disabled > a:focus {\n color: #777777;\n text-decoration: none;\n background-color: transparent;\n cursor: not-allowed;\n}\n.nav .open > a,\n.nav .open > a:hover,\n.nav .open > a:focus {\n background-color: #eeeeee;\n border-color: #337ab7;\n}\n.nav .nav-divider {\n height: 1px;\n margin: 9px 0;\n overflow: hidden;\n background-color: #e5e5e5;\n}\n.nav > li > a > img {\n max-width: none;\n}\n.nav-tabs {\n border-bottom: 1px solid #dddddd;\n}\n.nav-tabs > li {\n float: left;\n margin-bottom: -1px;\n}\n.nav-tabs > li > a {\n margin-right: 2px;\n line-height: 1.42857143;\n border: 1px solid transparent;\n border-radius: 4px 4px 0 0;\n}\n.nav-tabs > li > a:hover {\n border-color: #eeeeee #eeeeee #dddddd;\n}\n.nav-tabs > li.active > a,\n.nav-tabs > li.active > a:hover,\n.nav-tabs > li.active > a:focus {\n color: #555555;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n border-bottom-color: transparent;\n cursor: default;\n}\n.nav-tabs.nav-justified {\n width: 100%;\n border-bottom: 0;\n}\n.nav-tabs.nav-justified > li {\n float: none;\n}\n.nav-tabs.nav-justified > li > a {\n text-align: center;\n margin-bottom: 5px;\n}\n.nav-tabs.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-tabs.nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs.nav-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs.nav-justified > .active > a,\n.nav-tabs.nav-justified > .active > a:hover,\n.nav-tabs.nav-justified > .active > a:focus {\n border: 1px solid #dddddd;\n}\n@media (min-width: 768px) {\n .nav-tabs.nav-justified > li > a {\n border-bottom: 1px solid #dddddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs.nav-justified > .active > a,\n .nav-tabs.nav-justified > .active > a:hover,\n .nav-tabs.nav-justified > .active > a:focus {\n border-bottom-color: #ffffff;\n }\n}\n.nav-pills > li {\n float: left;\n}\n.nav-pills > li > a {\n border-radius: 4px;\n}\n.nav-pills > li + li {\n margin-left: 2px;\n}\n.nav-pills > li.active > a,\n.nav-pills > li.active > a:hover,\n.nav-pills > li.active > a:focus {\n color: #ffffff;\n background-color: #337ab7;\n}\n.nav-stacked > li {\n float: none;\n}\n.nav-stacked > li + li {\n margin-top: 2px;\n margin-left: 0;\n}\n.nav-justified {\n width: 100%;\n}\n.nav-justified > li {\n float: none;\n}\n.nav-justified > li > a {\n text-align: center;\n margin-bottom: 5px;\n}\n.nav-justified > .dropdown .dropdown-menu {\n top: auto;\n left: auto;\n}\n@media (min-width: 768px) {\n .nav-justified > li {\n display: table-cell;\n width: 1%;\n }\n .nav-justified > li > a {\n margin-bottom: 0;\n }\n}\n.nav-tabs-justified {\n border-bottom: 0;\n}\n.nav-tabs-justified > li > a {\n margin-right: 0;\n border-radius: 4px;\n}\n.nav-tabs-justified > .active > a,\n.nav-tabs-justified > .active > a:hover,\n.nav-tabs-justified > .active > a:focus {\n border: 1px solid #dddddd;\n}\n@media (min-width: 768px) {\n .nav-tabs-justified > li > a {\n border-bottom: 1px solid #dddddd;\n border-radius: 4px 4px 0 0;\n }\n .nav-tabs-justified > .active > a,\n .nav-tabs-justified > .active > a:hover,\n .nav-tabs-justified > .active > a:focus {\n border-bottom-color: #ffffff;\n }\n}\n.tab-content > .tab-pane {\n display: none;\n visibility: hidden;\n}\n.tab-content > .active {\n display: block;\n visibility: visible;\n}\n.nav-tabs .dropdown-menu {\n margin-top: -1px;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.navbar {\n position: relative;\n min-height: 50px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n}\n@media (min-width: 768px) {\n .navbar {\n border-radius: 4px;\n }\n}\n@media (min-width: 768px) {\n .navbar-header {\n float: left;\n }\n}\n.navbar-collapse {\n overflow-x: visible;\n padding-right: 15px;\n padding-left: 15px;\n border-top: 1px solid transparent;\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1);\n -webkit-overflow-scrolling: touch;\n}\n.navbar-collapse.in {\n overflow-y: auto;\n}\n@media (min-width: 768px) {\n .navbar-collapse {\n width: auto;\n border-top: 0;\n box-shadow: none;\n }\n .navbar-collapse.collapse {\n display: block !important;\n visibility: visible !important;\n height: auto !important;\n padding-bottom: 0;\n overflow: visible !important;\n }\n .navbar-collapse.in {\n overflow-y: visible;\n }\n .navbar-fixed-top .navbar-collapse,\n .navbar-static-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n padding-left: 0;\n padding-right: 0;\n }\n}\n.navbar-fixed-top .navbar-collapse,\n.navbar-fixed-bottom .navbar-collapse {\n max-height: 340px;\n}\n@media (max-device-width: 480px) and (orientation: landscape) {\n .navbar-fixed-top .navbar-collapse,\n .navbar-fixed-bottom .navbar-collapse {\n max-height: 200px;\n }\n}\n.container > .navbar-header,\n.container-fluid > .navbar-header,\n.container > .navbar-collapse,\n.container-fluid > .navbar-collapse {\n margin-right: -15px;\n margin-left: -15px;\n}\n@media (min-width: 768px) {\n .container > .navbar-header,\n .container-fluid > .navbar-header,\n .container > .navbar-collapse,\n .container-fluid > .navbar-collapse {\n margin-right: 0;\n margin-left: 0;\n }\n}\n.navbar-static-top {\n z-index: 1000;\n border-width: 0 0 1px;\n}\n@media (min-width: 768px) {\n .navbar-static-top {\n border-radius: 0;\n }\n}\n.navbar-fixed-top,\n.navbar-fixed-bottom {\n position: fixed;\n right: 0;\n left: 0;\n z-index: 1030;\n}\n@media (min-width: 768px) {\n .navbar-fixed-top,\n .navbar-fixed-bottom {\n border-radius: 0;\n }\n}\n.navbar-fixed-top {\n top: 0;\n border-width: 0 0 1px;\n}\n.navbar-fixed-bottom {\n bottom: 0;\n margin-bottom: 0;\n border-width: 1px 0 0;\n}\n.navbar-brand {\n float: left;\n padding: 15px 15px;\n font-size: 18px;\n line-height: 20px;\n height: 50px;\n}\n.navbar-brand:hover,\n.navbar-brand:focus {\n text-decoration: none;\n}\n.navbar-brand > img {\n display: block;\n}\n@media (min-width: 768px) {\n .navbar > .container .navbar-brand,\n .navbar > .container-fluid .navbar-brand {\n margin-left: -15px;\n }\n}\n.navbar-toggle {\n position: relative;\n float: right;\n margin-right: 15px;\n padding: 9px 10px;\n margin-top: 8px;\n margin-bottom: 8px;\n background-color: transparent;\n background-image: none;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.navbar-toggle:focus {\n outline: 0;\n}\n.navbar-toggle .icon-bar {\n display: block;\n width: 22px;\n height: 2px;\n border-radius: 1px;\n}\n.navbar-toggle .icon-bar + .icon-bar {\n margin-top: 4px;\n}\n@media (min-width: 768px) {\n .navbar-toggle {\n display: none;\n }\n}\n.navbar-nav {\n margin: 7.5px -15px;\n}\n.navbar-nav > li > a {\n padding-top: 10px;\n padding-bottom: 10px;\n line-height: 20px;\n}\n@media (max-width: 767px) {\n .navbar-nav .open .dropdown-menu {\n position: static;\n float: none;\n width: auto;\n margin-top: 0;\n background-color: transparent;\n border: 0;\n box-shadow: none;\n }\n .navbar-nav .open .dropdown-menu > li > a,\n .navbar-nav .open .dropdown-menu .dropdown-header {\n padding: 5px 15px 5px 25px;\n }\n .navbar-nav .open .dropdown-menu > li > a {\n line-height: 20px;\n }\n .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-nav .open .dropdown-menu > li > a:focus {\n background-image: none;\n }\n}\n@media (min-width: 768px) {\n .navbar-nav {\n float: left;\n margin: 0;\n }\n .navbar-nav > li {\n float: left;\n }\n .navbar-nav > li > a {\n padding-top: 15px;\n padding-bottom: 15px;\n }\n}\n.navbar-form {\n margin-left: -15px;\n margin-right: -15px;\n padding: 10px 15px;\n border-top: 1px solid transparent;\n border-bottom: 1px solid transparent;\n -webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 1px 0 rgba(255, 255, 255, 0.1);\n margin-top: 8px;\n margin-bottom: 8px;\n}\n@media (min-width: 768px) {\n .navbar-form .form-group {\n display: inline-block;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .form-control {\n display: inline-block;\n width: auto;\n vertical-align: middle;\n }\n .navbar-form .form-control-static {\n display: inline-block;\n }\n .navbar-form .input-group {\n display: inline-table;\n vertical-align: middle;\n }\n .navbar-form .input-group .input-group-addon,\n .navbar-form .input-group .input-group-btn,\n .navbar-form .input-group .form-control {\n width: auto;\n }\n .navbar-form .input-group > .form-control {\n width: 100%;\n }\n .navbar-form .control-label {\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio,\n .navbar-form .checkbox {\n display: inline-block;\n margin-top: 0;\n margin-bottom: 0;\n vertical-align: middle;\n }\n .navbar-form .radio label,\n .navbar-form .checkbox label {\n padding-left: 0;\n }\n .navbar-form .radio input[type=\"radio\"],\n .navbar-form .checkbox input[type=\"checkbox\"] {\n position: relative;\n margin-left: 0;\n }\n .navbar-form .has-feedback .form-control-feedback {\n top: 0;\n }\n}\n@media (max-width: 767px) {\n .navbar-form .form-group {\n margin-bottom: 5px;\n }\n .navbar-form .form-group:last-child {\n margin-bottom: 0;\n }\n}\n@media (min-width: 768px) {\n .navbar-form {\n width: auto;\n border: 0;\n margin-left: 0;\n margin-right: 0;\n padding-top: 0;\n padding-bottom: 0;\n -webkit-box-shadow: none;\n box-shadow: none;\n }\n}\n.navbar-nav > li > .dropdown-menu {\n margin-top: 0;\n border-top-right-radius: 0;\n border-top-left-radius: 0;\n}\n.navbar-fixed-bottom .navbar-nav > li > .dropdown-menu {\n border-top-right-radius: 4px;\n border-top-left-radius: 4px;\n border-bottom-right-radius: 0;\n border-bottom-left-radius: 0;\n}\n.navbar-btn {\n margin-top: 8px;\n margin-bottom: 8px;\n}\n.navbar-btn.btn-sm {\n margin-top: 10px;\n margin-bottom: 10px;\n}\n.navbar-btn.btn-xs {\n margin-top: 14px;\n margin-bottom: 14px;\n}\n.navbar-text {\n margin-top: 15px;\n margin-bottom: 15px;\n}\n@media (min-width: 768px) {\n .navbar-text {\n float: left;\n margin-left: 15px;\n margin-right: 15px;\n }\n}\n@media (min-width: 768px) {\n .navbar-left {\n float: left !important;\n }\n .navbar-right {\n float: right !important;\n margin-right: -15px;\n }\n .navbar-right ~ .navbar-right {\n margin-right: 0;\n }\n}\n.navbar-default {\n background-color: #f8f8f8;\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-brand {\n color: #777777;\n}\n.navbar-default .navbar-brand:hover,\n.navbar-default .navbar-brand:focus {\n color: #5e5e5e;\n background-color: transparent;\n}\n.navbar-default .navbar-text {\n color: #777777;\n}\n.navbar-default .navbar-nav > li > a {\n color: #777777;\n}\n.navbar-default .navbar-nav > li > a:hover,\n.navbar-default .navbar-nav > li > a:focus {\n color: #333333;\n background-color: transparent;\n}\n.navbar-default .navbar-nav > .active > a,\n.navbar-default .navbar-nav > .active > a:hover,\n.navbar-default .navbar-nav > .active > a:focus {\n color: #555555;\n background-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .disabled > a,\n.navbar-default .navbar-nav > .disabled > a:hover,\n.navbar-default .navbar-nav > .disabled > a:focus {\n color: #cccccc;\n background-color: transparent;\n}\n.navbar-default .navbar-toggle {\n border-color: #dddddd;\n}\n.navbar-default .navbar-toggle:hover,\n.navbar-default .navbar-toggle:focus {\n background-color: #dddddd;\n}\n.navbar-default .navbar-toggle .icon-bar {\n background-color: #888888;\n}\n.navbar-default .navbar-collapse,\n.navbar-default .navbar-form {\n border-color: #e7e7e7;\n}\n.navbar-default .navbar-nav > .open > a,\n.navbar-default .navbar-nav > .open > a:hover,\n.navbar-default .navbar-nav > .open > a:focus {\n background-color: #e7e7e7;\n color: #555555;\n}\n@media (max-width: 767px) {\n .navbar-default .navbar-nav .open .dropdown-menu > li > a {\n color: #777777;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #333333;\n background-color: transparent;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #555555;\n background-color: #e7e7e7;\n }\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-default .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #cccccc;\n background-color: transparent;\n }\n}\n.navbar-default .navbar-link {\n color: #777777;\n}\n.navbar-default .navbar-link:hover {\n color: #333333;\n}\n.navbar-default .btn-link {\n color: #777777;\n}\n.navbar-default .btn-link:hover,\n.navbar-default .btn-link:focus {\n color: #333333;\n}\n.navbar-default .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-default .btn-link:hover,\n.navbar-default .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-default .btn-link:focus {\n color: #cccccc;\n}\n.navbar-inverse {\n background-color: #222222;\n border-color: #080808;\n}\n.navbar-inverse .navbar-brand {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-brand:hover,\n.navbar-inverse .navbar-brand:focus {\n color: #ffffff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-text {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-nav > li > a:hover,\n.navbar-inverse .navbar-nav > li > a:focus {\n color: #ffffff;\n background-color: transparent;\n}\n.navbar-inverse .navbar-nav > .active > a,\n.navbar-inverse .navbar-nav > .active > a:hover,\n.navbar-inverse .navbar-nav > .active > a:focus {\n color: #ffffff;\n background-color: #080808;\n}\n.navbar-inverse .navbar-nav > .disabled > a,\n.navbar-inverse .navbar-nav > .disabled > a:hover,\n.navbar-inverse .navbar-nav > .disabled > a:focus {\n color: #444444;\n background-color: transparent;\n}\n.navbar-inverse .navbar-toggle {\n border-color: #333333;\n}\n.navbar-inverse .navbar-toggle:hover,\n.navbar-inverse .navbar-toggle:focus {\n background-color: #333333;\n}\n.navbar-inverse .navbar-toggle .icon-bar {\n background-color: #ffffff;\n}\n.navbar-inverse .navbar-collapse,\n.navbar-inverse .navbar-form {\n border-color: #101010;\n}\n.navbar-inverse .navbar-nav > .open > a,\n.navbar-inverse .navbar-nav > .open > a:hover,\n.navbar-inverse .navbar-nav > .open > a:focus {\n background-color: #080808;\n color: #ffffff;\n}\n@media (max-width: 767px) {\n .navbar-inverse .navbar-nav .open .dropdown-menu > .dropdown-header {\n border-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu .divider {\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a {\n color: #9d9d9d;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > li > a:focus {\n color: #ffffff;\n background-color: transparent;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .active > a:focus {\n color: #ffffff;\n background-color: #080808;\n }\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:hover,\n .navbar-inverse .navbar-nav .open .dropdown-menu > .disabled > a:focus {\n color: #444444;\n background-color: transparent;\n }\n}\n.navbar-inverse .navbar-link {\n color: #9d9d9d;\n}\n.navbar-inverse .navbar-link:hover {\n color: #ffffff;\n}\n.navbar-inverse .btn-link {\n color: #9d9d9d;\n}\n.navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link:focus {\n color: #ffffff;\n}\n.navbar-inverse .btn-link[disabled]:hover,\nfieldset[disabled] .navbar-inverse .btn-link:hover,\n.navbar-inverse .btn-link[disabled]:focus,\nfieldset[disabled] .navbar-inverse .btn-link:focus {\n color: #444444;\n}\n.breadcrumb {\n padding: 8px 15px;\n margin-bottom: 20px;\n list-style: none;\n background-color: #f5f5f5;\n border-radius: 4px;\n}\n.breadcrumb > li {\n display: inline-block;\n}\n.breadcrumb > li + li:before {\n content: \"/\\00a0\";\n padding: 0 5px;\n color: #cccccc;\n}\n.breadcrumb > .active {\n color: #777777;\n}\n.pagination {\n display: inline-block;\n padding-left: 0;\n margin: 20px 0;\n border-radius: 4px;\n}\n.pagination > li {\n display: inline;\n}\n.pagination > li > a,\n.pagination > li > span {\n position: relative;\n float: left;\n padding: 6px 12px;\n line-height: 1.42857143;\n text-decoration: none;\n color: #337ab7;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n margin-left: -1px;\n}\n.pagination > li:first-child > a,\n.pagination > li:first-child > span {\n margin-left: 0;\n border-bottom-left-radius: 4px;\n border-top-left-radius: 4px;\n}\n.pagination > li:last-child > a,\n.pagination > li:last-child > span {\n border-bottom-right-radius: 4px;\n border-top-right-radius: 4px;\n}\n.pagination > li > a:hover,\n.pagination > li > span:hover,\n.pagination > li > a:focus,\n.pagination > li > span:focus {\n color: #23527c;\n background-color: #eeeeee;\n border-color: #dddddd;\n}\n.pagination > .active > a,\n.pagination > .active > span,\n.pagination > .active > a:hover,\n.pagination > .active > span:hover,\n.pagination > .active > a:focus,\n.pagination > .active > span:focus {\n z-index: 2;\n color: #ffffff;\n background-color: #337ab7;\n border-color: #337ab7;\n cursor: default;\n}\n.pagination > .disabled > span,\n.pagination > .disabled > span:hover,\n.pagination > .disabled > span:focus,\n.pagination > .disabled > a,\n.pagination > .disabled > a:hover,\n.pagination > .disabled > a:focus {\n color: #777777;\n background-color: #ffffff;\n border-color: #dddddd;\n cursor: not-allowed;\n}\n.pagination-lg > li > a,\n.pagination-lg > li > span {\n padding: 10px 16px;\n font-size: 18px;\n}\n.pagination-lg > li:first-child > a,\n.pagination-lg > li:first-child > span {\n border-bottom-left-radius: 6px;\n border-top-left-radius: 6px;\n}\n.pagination-lg > li:last-child > a,\n.pagination-lg > li:last-child > span {\n border-bottom-right-radius: 6px;\n border-top-right-radius: 6px;\n}\n.pagination-sm > li > a,\n.pagination-sm > li > span {\n padding: 5px 10px;\n font-size: 12px;\n}\n.pagination-sm > li:first-child > a,\n.pagination-sm > li:first-child > span {\n border-bottom-left-radius: 3px;\n border-top-left-radius: 3px;\n}\n.pagination-sm > li:last-child > a,\n.pagination-sm > li:last-child > span {\n border-bottom-right-radius: 3px;\n border-top-right-radius: 3px;\n}\n.pager {\n padding-left: 0;\n margin: 20px 0;\n list-style: none;\n text-align: center;\n}\n.pager li {\n display: inline;\n}\n.pager li > a,\n.pager li > span {\n display: inline-block;\n padding: 5px 14px;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n border-radius: 15px;\n}\n.pager li > a:hover,\n.pager li > a:focus {\n text-decoration: none;\n background-color: #eeeeee;\n}\n.pager .next > a,\n.pager .next > span {\n float: right;\n}\n.pager .previous > a,\n.pager .previous > span {\n float: left;\n}\n.pager .disabled > a,\n.pager .disabled > a:hover,\n.pager .disabled > a:focus,\n.pager .disabled > span {\n color: #777777;\n background-color: #ffffff;\n cursor: not-allowed;\n}\n.label {\n display: inline;\n padding: .2em .6em .3em;\n font-size: 75%;\n font-weight: bold;\n line-height: 1;\n color: #ffffff;\n text-align: center;\n white-space: nowrap;\n vertical-align: baseline;\n border-radius: .25em;\n}\na.label:hover,\na.label:focus {\n color: #ffffff;\n text-decoration: none;\n cursor: pointer;\n}\n.label:empty {\n display: none;\n}\n.btn .label {\n position: relative;\n top: -1px;\n}\n.label-default {\n background-color: #777777;\n}\n.label-default[href]:hover,\n.label-default[href]:focus {\n background-color: #5e5e5e;\n}\n.label-primary {\n background-color: #337ab7;\n}\n.label-primary[href]:hover,\n.label-primary[href]:focus {\n background-color: #286090;\n}\n.label-success {\n background-color: #5cb85c;\n}\n.label-success[href]:hover,\n.label-success[href]:focus {\n background-color: #449d44;\n}\n.label-info {\n background-color: #5bc0de;\n}\n.label-info[href]:hover,\n.label-info[href]:focus {\n background-color: #31b0d5;\n}\n.label-warning {\n background-color: #f0ad4e;\n}\n.label-warning[href]:hover,\n.label-warning[href]:focus {\n background-color: #ec971f;\n}\n.label-danger {\n background-color: #d9534f;\n}\n.label-danger[href]:hover,\n.label-danger[href]:focus {\n background-color: #c9302c;\n}\n.badge {\n display: inline-block;\n min-width: 10px;\n padding: 3px 7px;\n font-size: 12px;\n font-weight: bold;\n color: #ffffff;\n line-height: 1;\n vertical-align: baseline;\n white-space: nowrap;\n text-align: center;\n background-color: #777777;\n border-radius: 10px;\n}\n.badge:empty {\n display: none;\n}\n.btn .badge {\n position: relative;\n top: -1px;\n}\n.btn-xs .badge {\n top: 0;\n padding: 1px 5px;\n}\na.badge:hover,\na.badge:focus {\n color: #ffffff;\n text-decoration: none;\n cursor: pointer;\n}\n.list-group-item.active > .badge,\n.nav-pills > .active > a > .badge {\n color: #337ab7;\n background-color: #ffffff;\n}\n.list-group-item > .badge {\n float: right;\n}\n.list-group-item > .badge + .badge {\n margin-right: 5px;\n}\n.nav-pills > li > a > .badge {\n margin-left: 3px;\n}\n.jumbotron {\n padding: 30px 15px;\n margin-bottom: 30px;\n color: inherit;\n background-color: #eeeeee;\n}\n.jumbotron h1,\n.jumbotron .h1 {\n color: inherit;\n}\n.jumbotron p {\n margin-bottom: 15px;\n font-size: 21px;\n font-weight: 200;\n}\n.jumbotron > hr {\n border-top-color: #d5d5d5;\n}\n.container .jumbotron,\n.container-fluid .jumbotron {\n border-radius: 6px;\n}\n.jumbotron .container {\n max-width: 100%;\n}\n@media screen and (min-width: 768px) {\n .jumbotron {\n padding: 48px 0;\n }\n .container .jumbotron,\n .container-fluid .jumbotron {\n padding-left: 60px;\n padding-right: 60px;\n }\n .jumbotron h1,\n .jumbotron .h1 {\n font-size: 63px;\n }\n}\n.thumbnail {\n display: block;\n padding: 4px;\n margin-bottom: 20px;\n line-height: 1.42857143;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n border-radius: 4px;\n -webkit-transition: border 0.2s ease-in-out;\n -o-transition: border 0.2s ease-in-out;\n transition: border 0.2s ease-in-out;\n}\n.thumbnail > img,\n.thumbnail a > img {\n margin-left: auto;\n margin-right: auto;\n}\na.thumbnail:hover,\na.thumbnail:focus,\na.thumbnail.active {\n border-color: #337ab7;\n}\n.thumbnail .caption {\n padding: 9px;\n color: #333333;\n}\n.alert {\n padding: 15px;\n margin-bottom: 20px;\n border: 1px solid transparent;\n border-radius: 4px;\n}\n.alert h4 {\n margin-top: 0;\n color: inherit;\n}\n.alert .alert-link {\n font-weight: bold;\n}\n.alert > p,\n.alert > ul {\n margin-bottom: 0;\n}\n.alert > p + p {\n margin-top: 5px;\n}\n.alert-dismissable,\n.alert-dismissible {\n padding-right: 35px;\n}\n.alert-dismissable .close,\n.alert-dismissible .close {\n position: relative;\n top: -2px;\n right: -21px;\n color: inherit;\n}\n.alert-success {\n background-color: #dff0d8;\n border-color: #d6e9c6;\n color: #3c763d;\n}\n.alert-success hr {\n border-top-color: #c9e2b3;\n}\n.alert-success .alert-link {\n color: #2b542c;\n}\n.alert-info {\n background-color: #d9edf7;\n border-color: #bce8f1;\n color: #31708f;\n}\n.alert-info hr {\n border-top-color: #a6e1ec;\n}\n.alert-info .alert-link {\n color: #245269;\n}\n.alert-warning {\n background-color: #fcf8e3;\n border-color: #faebcc;\n color: #8a6d3b;\n}\n.alert-warning hr {\n border-top-color: #f7e1b5;\n}\n.alert-warning .alert-link {\n color: #66512c;\n}\n.alert-danger {\n background-color: #f2dede;\n border-color: #ebccd1;\n color: #a94442;\n}\n.alert-danger hr {\n border-top-color: #e4b9c0;\n}\n.alert-danger .alert-link {\n color: #843534;\n}\n@-webkit-keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n@keyframes progress-bar-stripes {\n from {\n background-position: 40px 0;\n }\n to {\n background-position: 0 0;\n }\n}\n.progress {\n overflow: hidden;\n height: 20px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1);\n}\n.progress-bar {\n float: left;\n width: 0%;\n height: 100%;\n font-size: 12px;\n line-height: 20px;\n color: #ffffff;\n text-align: center;\n background-color: #337ab7;\n -webkit-box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n box-shadow: inset 0 -1px 0 rgba(0, 0, 0, 0.15);\n -webkit-transition: width 0.6s ease;\n -o-transition: width 0.6s ease;\n transition: width 0.6s ease;\n}\n.progress-striped .progress-bar,\n.progress-bar-striped {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-size: 40px 40px;\n}\n.progress.active .progress-bar,\n.progress-bar.active {\n -webkit-animation: progress-bar-stripes 2s linear infinite;\n -o-animation: progress-bar-stripes 2s linear infinite;\n animation: progress-bar-stripes 2s linear infinite;\n}\n.progress-bar-success {\n background-color: #5cb85c;\n}\n.progress-striped .progress-bar-success {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-info {\n background-color: #5bc0de;\n}\n.progress-striped .progress-bar-info {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-warning {\n background-color: #f0ad4e;\n}\n.progress-striped .progress-bar-warning {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.progress-bar-danger {\n background-color: #d9534f;\n}\n.progress-striped .progress-bar-danger {\n background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);\n}\n.media {\n margin-top: 15px;\n}\n.media:first-child {\n margin-top: 0;\n}\n.media-right,\n.media > .pull-right {\n padding-left: 10px;\n}\n.media-left,\n.media > .pull-left {\n padding-right: 10px;\n}\n.media-left,\n.media-right,\n.media-body {\n display: table-cell;\n vertical-align: top;\n}\n.media-middle {\n vertical-align: middle;\n}\n.media-bottom {\n vertical-align: bottom;\n}\n.media-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.media-list {\n padding-left: 0;\n list-style: none;\n}\n.list-group {\n margin-bottom: 20px;\n padding-left: 0;\n}\n.list-group-item {\n position: relative;\n display: block;\n padding: 10px 15px;\n margin-bottom: -1px;\n background-color: #ffffff;\n border: 1px solid #dddddd;\n}\n.list-group-item:first-child {\n border-top-right-radius: 4px;\n border-top-left-radius: 4px;\n}\n.list-group-item:last-child {\n margin-bottom: 0;\n border-bottom-right-radius: 4px;\n border-bottom-left-radius: 4px;\n}\na.list-group-item {\n color: #555555;\n}\na.list-group-item .list-group-item-heading {\n color: #333333;\n}\na.list-group-item:hover,\na.list-group-item:focus {\n text-decoration: none;\n color: #555555;\n background-color: #f5f5f5;\n}\n.list-group-item.disabled,\n.list-group-item.disabled:hover,\n.list-group-item.disabled:focus {\n background-color: #eeeeee;\n color: #777777;\n cursor: not-allowed;\n}\n.list-group-item.disabled .list-group-item-heading,\n.list-group-item.disabled:hover .list-group-item-heading,\n.list-group-item.disabled:focus .list-group-item-heading {\n color: inherit;\n}\n.list-group-item.disabled .list-group-item-text,\n.list-group-item.disabled:hover .list-group-item-text,\n.list-group-item.disabled:focus .list-group-item-text {\n color: #777777;\n}\n.list-group-item.active,\n.list-group-item.active:hover,\n.list-group-item.active:focus {\n z-index: 2;\n color: #ffffff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.list-group-item.active .list-group-item-heading,\n.list-group-item.active:hover .list-group-item-heading,\n.list-group-item.active:focus .list-group-item-heading,\n.list-group-item.active .list-group-item-heading > small,\n.list-group-item.active:hover .list-group-item-heading > small,\n.list-group-item.active:focus .list-group-item-heading > small,\n.list-group-item.active .list-group-item-heading > .small,\n.list-group-item.active:hover .list-group-item-heading > .small,\n.list-group-item.active:focus .list-group-item-heading > .small {\n color: inherit;\n}\n.list-group-item.active .list-group-item-text,\n.list-group-item.active:hover .list-group-item-text,\n.list-group-item.active:focus .list-group-item-text {\n color: #c7ddef;\n}\n.list-group-item-success {\n color: #3c763d;\n background-color: #dff0d8;\n}\na.list-group-item-success {\n color: #3c763d;\n}\na.list-group-item-success .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-success:hover,\na.list-group-item-success:focus {\n color: #3c763d;\n background-color: #d0e9c6;\n}\na.list-group-item-success.active,\na.list-group-item-success.active:hover,\na.list-group-item-success.active:focus {\n color: #fff;\n background-color: #3c763d;\n border-color: #3c763d;\n}\n.list-group-item-info {\n color: #31708f;\n background-color: #d9edf7;\n}\na.list-group-item-info {\n color: #31708f;\n}\na.list-group-item-info .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-info:hover,\na.list-group-item-info:focus {\n color: #31708f;\n background-color: #c4e3f3;\n}\na.list-group-item-info.active,\na.list-group-item-info.active:hover,\na.list-group-item-info.active:focus {\n color: #fff;\n background-color: #31708f;\n border-color: #31708f;\n}\n.list-group-item-warning {\n color: #8a6d3b;\n background-color: #fcf8e3;\n}\na.list-group-item-warning {\n color: #8a6d3b;\n}\na.list-group-item-warning .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-warning:hover,\na.list-group-item-warning:focus {\n color: #8a6d3b;\n background-color: #faf2cc;\n}\na.list-group-item-warning.active,\na.list-group-item-warning.active:hover,\na.list-group-item-warning.active:focus {\n color: #fff;\n background-color: #8a6d3b;\n border-color: #8a6d3b;\n}\n.list-group-item-danger {\n color: #a94442;\n background-color: #f2dede;\n}\na.list-group-item-danger {\n color: #a94442;\n}\na.list-group-item-danger .list-group-item-heading {\n color: inherit;\n}\na.list-group-item-danger:hover,\na.list-group-item-danger:focus {\n color: #a94442;\n background-color: #ebcccc;\n}\na.list-group-item-danger.active,\na.list-group-item-danger.active:hover,\na.list-group-item-danger.active:focus {\n color: #fff;\n background-color: #a94442;\n border-color: #a94442;\n}\n.list-group-item-heading {\n margin-top: 0;\n margin-bottom: 5px;\n}\n.list-group-item-text {\n margin-bottom: 0;\n line-height: 1.3;\n}\n.panel {\n margin-bottom: 20px;\n background-color: #ffffff;\n border: 1px solid transparent;\n border-radius: 4px;\n -webkit-box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.panel-body {\n padding: 15px;\n}\n.panel-heading {\n padding: 10px 15px;\n border-bottom: 1px solid transparent;\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel-heading > .dropdown .dropdown-toggle {\n color: inherit;\n}\n.panel-title {\n margin-top: 0;\n margin-bottom: 0;\n font-size: 16px;\n color: inherit;\n}\n.panel-title > a {\n color: inherit;\n}\n.panel-footer {\n padding: 10px 15px;\n background-color: #f5f5f5;\n border-top: 1px solid #dddddd;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .list-group,\n.panel > .panel-collapse > .list-group {\n margin-bottom: 0;\n}\n.panel > .list-group .list-group-item,\n.panel > .panel-collapse > .list-group .list-group-item {\n border-width: 1px 0;\n border-radius: 0;\n}\n.panel > .list-group:first-child .list-group-item:first-child,\n.panel > .panel-collapse > .list-group:first-child .list-group-item:first-child {\n border-top: 0;\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel > .list-group:last-child .list-group-item:last-child,\n.panel > .panel-collapse > .list-group:last-child .list-group-item:last-child {\n border-bottom: 0;\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel-heading + .list-group .list-group-item:first-child {\n border-top-width: 0;\n}\n.list-group + .panel-footer {\n border-top-width: 0;\n}\n.panel > .table,\n.panel > .table-responsive > .table,\n.panel > .panel-collapse > .table {\n margin-bottom: 0;\n}\n.panel > .table caption,\n.panel > .table-responsive > .table caption,\n.panel > .panel-collapse > .table caption {\n padding-left: 15px;\n padding-right: 15px;\n}\n.panel > .table:first-child,\n.panel > .table-responsive:first-child > .table:first-child {\n border-top-right-radius: 3px;\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child {\n border-top-left-radius: 3px;\n border-top-right-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:first-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:first-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:first-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:first-child {\n border-top-left-radius: 3px;\n}\n.panel > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child td:last-child,\n.panel > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > thead:first-child > tr:first-child th:last-child,\n.panel > .table:first-child > tbody:first-child > tr:first-child th:last-child,\n.panel > .table-responsive:first-child > .table:first-child > tbody:first-child > tr:first-child th:last-child {\n border-top-right-radius: 3px;\n}\n.panel > .table:last-child,\n.panel > .table-responsive:last-child > .table:last-child {\n border-bottom-right-radius: 3px;\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child {\n border-bottom-left-radius: 3px;\n border-bottom-right-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:first-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:first-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:first-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:first-child {\n border-bottom-left-radius: 3px;\n}\n.panel > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child td:last-child,\n.panel > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tbody:last-child > tr:last-child th:last-child,\n.panel > .table:last-child > tfoot:last-child > tr:last-child th:last-child,\n.panel > .table-responsive:last-child > .table:last-child > tfoot:last-child > tr:last-child th:last-child {\n border-bottom-right-radius: 3px;\n}\n.panel > .panel-body + .table,\n.panel > .panel-body + .table-responsive,\n.panel > .table + .panel-body,\n.panel > .table-responsive + .panel-body {\n border-top: 1px solid #dddddd;\n}\n.panel > .table > tbody:first-child > tr:first-child th,\n.panel > .table > tbody:first-child > tr:first-child td {\n border-top: 0;\n}\n.panel > .table-bordered,\n.panel > .table-responsive > .table-bordered {\n border: 0;\n}\n.panel > .table-bordered > thead > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:first-child,\n.panel > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:first-child,\n.panel > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:first-child,\n.panel > .table-bordered > thead > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:first-child,\n.panel > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:first-child,\n.panel > .table-bordered > tfoot > tr > td:first-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:first-child {\n border-left: 0;\n}\n.panel > .table-bordered > thead > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > th:last-child,\n.panel > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > th:last-child,\n.panel > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > th:last-child,\n.panel > .table-bordered > thead > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > thead > tr > td:last-child,\n.panel > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tbody > tr > td:last-child,\n.panel > .table-bordered > tfoot > tr > td:last-child,\n.panel > .table-responsive > .table-bordered > tfoot > tr > td:last-child {\n border-right: 0;\n}\n.panel > .table-bordered > thead > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > td,\n.panel > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > td,\n.panel > .table-bordered > thead > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > thead > tr:first-child > th,\n.panel > .table-bordered > tbody > tr:first-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:first-child > th {\n border-bottom: 0;\n}\n.panel > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > td,\n.panel > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > td,\n.panel > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tbody > tr:last-child > th,\n.panel > .table-bordered > tfoot > tr:last-child > th,\n.panel > .table-responsive > .table-bordered > tfoot > tr:last-child > th {\n border-bottom: 0;\n}\n.panel > .table-responsive {\n border: 0;\n margin-bottom: 0;\n}\n.panel-group {\n margin-bottom: 20px;\n}\n.panel-group .panel {\n margin-bottom: 0;\n border-radius: 4px;\n}\n.panel-group .panel + .panel {\n margin-top: 5px;\n}\n.panel-group .panel-heading {\n border-bottom: 0;\n}\n.panel-group .panel-heading + .panel-collapse > .panel-body,\n.panel-group .panel-heading + .panel-collapse > .list-group {\n border-top: 1px solid #dddddd;\n}\n.panel-group .panel-footer {\n border-top: 0;\n}\n.panel-group .panel-footer + .panel-collapse .panel-body {\n border-bottom: 1px solid #dddddd;\n}\n.panel-default {\n border-color: #dddddd;\n}\n.panel-default > .panel-heading {\n color: #333333;\n background-color: #f5f5f5;\n border-color: #dddddd;\n}\n.panel-default > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #dddddd;\n}\n.panel-default > .panel-heading .badge {\n color: #f5f5f5;\n background-color: #333333;\n}\n.panel-default > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #dddddd;\n}\n.panel-primary {\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading {\n color: #ffffff;\n background-color: #337ab7;\n border-color: #337ab7;\n}\n.panel-primary > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #337ab7;\n}\n.panel-primary > .panel-heading .badge {\n color: #337ab7;\n background-color: #ffffff;\n}\n.panel-primary > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #337ab7;\n}\n.panel-success {\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading {\n color: #3c763d;\n background-color: #dff0d8;\n border-color: #d6e9c6;\n}\n.panel-success > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #d6e9c6;\n}\n.panel-success > .panel-heading .badge {\n color: #dff0d8;\n background-color: #3c763d;\n}\n.panel-success > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #d6e9c6;\n}\n.panel-info {\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading {\n color: #31708f;\n background-color: #d9edf7;\n border-color: #bce8f1;\n}\n.panel-info > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #bce8f1;\n}\n.panel-info > .panel-heading .badge {\n color: #d9edf7;\n background-color: #31708f;\n}\n.panel-info > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #bce8f1;\n}\n.panel-warning {\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading {\n color: #8a6d3b;\n background-color: #fcf8e3;\n border-color: #faebcc;\n}\n.panel-warning > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #faebcc;\n}\n.panel-warning > .panel-heading .badge {\n color: #fcf8e3;\n background-color: #8a6d3b;\n}\n.panel-warning > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #faebcc;\n}\n.panel-danger {\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading {\n color: #a94442;\n background-color: #f2dede;\n border-color: #ebccd1;\n}\n.panel-danger > .panel-heading + .panel-collapse > .panel-body {\n border-top-color: #ebccd1;\n}\n.panel-danger > .panel-heading .badge {\n color: #f2dede;\n background-color: #a94442;\n}\n.panel-danger > .panel-footer + .panel-collapse > .panel-body {\n border-bottom-color: #ebccd1;\n}\n.embed-responsive {\n position: relative;\n display: block;\n height: 0;\n padding: 0;\n overflow: hidden;\n}\n.embed-responsive .embed-responsive-item,\n.embed-responsive iframe,\n.embed-responsive embed,\n.embed-responsive object,\n.embed-responsive video {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n height: 100%;\n width: 100%;\n border: 0;\n}\n.embed-responsive.embed-responsive-16by9 {\n padding-bottom: 56.25%;\n}\n.embed-responsive.embed-responsive-4by3 {\n padding-bottom: 75%;\n}\n.well {\n min-height: 20px;\n padding: 19px;\n margin-bottom: 20px;\n background-color: #f5f5f5;\n border: 1px solid #e3e3e3;\n border-radius: 4px;\n -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05);\n}\n.well blockquote {\n border-color: #ddd;\n border-color: rgba(0, 0, 0, 0.15);\n}\n.well-lg {\n padding: 24px;\n border-radius: 6px;\n}\n.well-sm {\n padding: 9px;\n border-radius: 3px;\n}\n.close {\n float: right;\n font-size: 21px;\n font-weight: bold;\n line-height: 1;\n color: #000000;\n text-shadow: 0 1px 0 #ffffff;\n opacity: 0.2;\n filter: alpha(opacity=20);\n}\n.close:hover,\n.close:focus {\n color: #000000;\n text-decoration: none;\n cursor: pointer;\n opacity: 0.5;\n filter: alpha(opacity=50);\n}\nbutton.close {\n padding: 0;\n cursor: pointer;\n background: transparent;\n border: 0;\n -webkit-appearance: none;\n}\n.modal-open {\n overflow: hidden;\n}\n.modal {\n display: none;\n overflow: hidden;\n position: fixed;\n top: 0;\n right: 0;\n bottom: 0;\n left: 0;\n z-index: 1040;\n -webkit-overflow-scrolling: touch;\n outline: 0;\n}\n.modal.fade .modal-dialog {\n -webkit-transform: translate(0, -25%);\n -ms-transform: translate(0, -25%);\n -o-transform: translate(0, -25%);\n transform: translate(0, -25%);\n -webkit-transition: -webkit-transform 0.3s ease-out;\n -moz-transition: -moz-transform 0.3s ease-out;\n -o-transition: -o-transform 0.3s ease-out;\n transition: transform 0.3s ease-out;\n}\n.modal.in .modal-dialog {\n -webkit-transform: translate(0, 0);\n -ms-transform: translate(0, 0);\n -o-transform: translate(0, 0);\n transform: translate(0, 0);\n}\n.modal-open .modal {\n overflow-x: hidden;\n overflow-y: auto;\n}\n.modal-dialog {\n position: relative;\n width: auto;\n margin: 10px;\n}\n.modal-content {\n position: relative;\n background-color: #ffffff;\n border: 1px solid #999999;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n box-shadow: 0 3px 9px rgba(0, 0, 0, 0.5);\n background-clip: padding-box;\n outline: 0;\n}\n.modal-backdrop {\n position: absolute;\n top: 0;\n right: 0;\n left: 0;\n background-color: #000000;\n}\n.modal-backdrop.fade {\n opacity: 0;\n filter: alpha(opacity=0);\n}\n.modal-backdrop.in {\n opacity: 0.5;\n filter: alpha(opacity=50);\n}\n.modal-header {\n padding: 15px;\n border-bottom: 1px solid #e5e5e5;\n min-height: 16.42857143px;\n}\n.modal-header .close {\n margin-top: -2px;\n}\n.modal-title {\n margin: 0;\n line-height: 1.42857143;\n}\n.modal-body {\n position: relative;\n padding: 15px;\n}\n.modal-footer {\n padding: 15px;\n text-align: right;\n border-top: 1px solid #e5e5e5;\n}\n.modal-footer .btn + .btn {\n margin-left: 5px;\n margin-bottom: 0;\n}\n.modal-footer .btn-group .btn + .btn {\n margin-left: -1px;\n}\n.modal-footer .btn-block + .btn-block {\n margin-left: 0;\n}\n.modal-scrollbar-measure {\n position: absolute;\n top: -9999px;\n width: 50px;\n height: 50px;\n overflow: scroll;\n}\n@media (min-width: 768px) {\n .modal-dialog {\n width: 600px;\n margin: 30px auto;\n }\n .modal-content {\n -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);\n }\n .modal-sm {\n width: 300px;\n }\n}\n@media (min-width: 992px) {\n .modal-lg {\n width: 900px;\n }\n}\n.tooltip {\n position: absolute;\n z-index: 1070;\n display: block;\n visibility: visible;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 12px;\n font-weight: normal;\n line-height: 1.4;\n opacity: 0;\n filter: alpha(opacity=0);\n}\n.tooltip.in {\n opacity: 0.9;\n filter: alpha(opacity=90);\n}\n.tooltip.top {\n margin-top: -3px;\n padding: 5px 0;\n}\n.tooltip.right {\n margin-left: 3px;\n padding: 0 5px;\n}\n.tooltip.bottom {\n margin-top: 3px;\n padding: 5px 0;\n}\n.tooltip.left {\n margin-left: -3px;\n padding: 0 5px;\n}\n.tooltip-inner {\n max-width: 200px;\n padding: 3px 8px;\n color: #ffffff;\n text-align: center;\n text-decoration: none;\n background-color: #000000;\n border-radius: 4px;\n}\n.tooltip-arrow {\n position: absolute;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.tooltip.top .tooltip-arrow {\n bottom: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000000;\n}\n.tooltip.top-left .tooltip-arrow {\n bottom: 0;\n right: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000000;\n}\n.tooltip.top-right .tooltip-arrow {\n bottom: 0;\n left: 5px;\n margin-bottom: -5px;\n border-width: 5px 5px 0;\n border-top-color: #000000;\n}\n.tooltip.right .tooltip-arrow {\n top: 50%;\n left: 0;\n margin-top: -5px;\n border-width: 5px 5px 5px 0;\n border-right-color: #000000;\n}\n.tooltip.left .tooltip-arrow {\n top: 50%;\n right: 0;\n margin-top: -5px;\n border-width: 5px 0 5px 5px;\n border-left-color: #000000;\n}\n.tooltip.bottom .tooltip-arrow {\n top: 0;\n left: 50%;\n margin-left: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000000;\n}\n.tooltip.bottom-left .tooltip-arrow {\n top: 0;\n right: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000000;\n}\n.tooltip.bottom-right .tooltip-arrow {\n top: 0;\n left: 5px;\n margin-top: -5px;\n border-width: 0 5px 5px;\n border-bottom-color: #000000;\n}\n.popover {\n position: absolute;\n top: 0;\n left: 0;\n z-index: 1060;\n display: none;\n max-width: 276px;\n padding: 1px;\n font-family: \"Helvetica Neue\", Helvetica, Arial, sans-serif;\n font-size: 14px;\n font-weight: normal;\n line-height: 1.42857143;\n text-align: left;\n background-color: #ffffff;\n background-clip: padding-box;\n border: 1px solid #cccccc;\n border: 1px solid rgba(0, 0, 0, 0.2);\n border-radius: 6px;\n -webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);\n white-space: normal;\n}\n.popover.top {\n margin-top: -10px;\n}\n.popover.right {\n margin-left: 10px;\n}\n.popover.bottom {\n margin-top: 10px;\n}\n.popover.left {\n margin-left: -10px;\n}\n.popover-title {\n margin: 0;\n padding: 8px 14px;\n font-size: 14px;\n background-color: #f7f7f7;\n border-bottom: 1px solid #ebebeb;\n border-radius: 5px 5px 0 0;\n}\n.popover-content {\n padding: 9px 14px;\n}\n.popover > .arrow,\n.popover > .arrow:after {\n position: absolute;\n display: block;\n width: 0;\n height: 0;\n border-color: transparent;\n border-style: solid;\n}\n.popover > .arrow {\n border-width: 11px;\n}\n.popover > .arrow:after {\n border-width: 10px;\n content: \"\";\n}\n.popover.top > .arrow {\n left: 50%;\n margin-left: -11px;\n border-bottom-width: 0;\n border-top-color: #999999;\n border-top-color: rgba(0, 0, 0, 0.25);\n bottom: -11px;\n}\n.popover.top > .arrow:after {\n content: \" \";\n bottom: 1px;\n margin-left: -10px;\n border-bottom-width: 0;\n border-top-color: #ffffff;\n}\n.popover.right > .arrow {\n top: 50%;\n left: -11px;\n margin-top: -11px;\n border-left-width: 0;\n border-right-color: #999999;\n border-right-color: rgba(0, 0, 0, 0.25);\n}\n.popover.right > .arrow:after {\n content: \" \";\n left: 1px;\n bottom: -10px;\n border-left-width: 0;\n border-right-color: #ffffff;\n}\n.popover.bottom > .arrow {\n left: 50%;\n margin-left: -11px;\n border-top-width: 0;\n border-bottom-color: #999999;\n border-bottom-color: rgba(0, 0, 0, 0.25);\n top: -11px;\n}\n.popover.bottom > .arrow:after {\n content: \" \";\n top: 1px;\n margin-left: -10px;\n border-top-width: 0;\n border-bottom-color: #ffffff;\n}\n.popover.left > .arrow {\n top: 50%;\n right: -11px;\n margin-top: -11px;\n border-right-width: 0;\n border-left-color: #999999;\n border-left-color: rgba(0, 0, 0, 0.25);\n}\n.popover.left > .arrow:after {\n content: \" \";\n right: 1px;\n border-right-width: 0;\n border-left-color: #ffffff;\n bottom: -10px;\n}\n.carousel {\n position: relative;\n}\n.carousel-inner {\n position: relative;\n overflow: hidden;\n width: 100%;\n}\n.carousel-inner > .item {\n display: none;\n position: relative;\n -webkit-transition: 0.6s ease-in-out left;\n -o-transition: 0.6s ease-in-out left;\n transition: 0.6s ease-in-out left;\n}\n.carousel-inner > .item > img,\n.carousel-inner > .item > a > img {\n line-height: 1;\n}\n@media all and (transform-3d), (-webkit-transform-3d) {\n .carousel-inner > .item {\n transition: transform 0.6s ease-in-out;\n backface-visibility: hidden;\n perspective: 1000;\n }\n .carousel-inner > .item.next,\n .carousel-inner > .item.active.right {\n transform: translate3d(100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.prev,\n .carousel-inner > .item.active.left {\n transform: translate3d(-100%, 0, 0);\n left: 0;\n }\n .carousel-inner > .item.next.left,\n .carousel-inner > .item.prev.right,\n .carousel-inner > .item.active {\n transform: translate3d(0, 0, 0);\n left: 0;\n }\n}\n.carousel-inner > .active,\n.carousel-inner > .next,\n.carousel-inner > .prev {\n display: block;\n}\n.carousel-inner > .active {\n left: 0;\n}\n.carousel-inner > .next,\n.carousel-inner > .prev {\n position: absolute;\n top: 0;\n width: 100%;\n}\n.carousel-inner > .next {\n left: 100%;\n}\n.carousel-inner > .prev {\n left: -100%;\n}\n.carousel-inner > .next.left,\n.carousel-inner > .prev.right {\n left: 0;\n}\n.carousel-inner > .active.left {\n left: -100%;\n}\n.carousel-inner > .active.right {\n left: 100%;\n}\n.carousel-control {\n position: absolute;\n top: 0;\n left: 0;\n bottom: 0;\n width: 15%;\n opacity: 0.5;\n filter: alpha(opacity=50);\n font-size: 20px;\n color: #ffffff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-control.left {\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.5) 0%, rgba(0, 0, 0, 0.0001) 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1);\n}\n.carousel-control.right {\n left: auto;\n right: 0;\n background-image: -webkit-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: -o-linear-gradient(left, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-image: linear-gradient(to right, rgba(0, 0, 0, 0.0001) 0%, rgba(0, 0, 0, 0.5) 100%);\n background-repeat: repeat-x;\n filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1);\n}\n.carousel-control:hover,\n.carousel-control:focus {\n outline: 0;\n color: #ffffff;\n text-decoration: none;\n opacity: 0.9;\n filter: alpha(opacity=90);\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-left,\n.carousel-control .glyphicon-chevron-right {\n position: absolute;\n top: 50%;\n z-index: 5;\n display: inline-block;\n}\n.carousel-control .icon-prev,\n.carousel-control .glyphicon-chevron-left {\n left: 50%;\n margin-left: -10px;\n}\n.carousel-control .icon-next,\n.carousel-control .glyphicon-chevron-right {\n right: 50%;\n margin-right: -10px;\n}\n.carousel-control .icon-prev,\n.carousel-control .icon-next {\n width: 20px;\n height: 20px;\n margin-top: -10px;\n font-family: serif;\n}\n.carousel-control .icon-prev:before {\n content: '\\2039';\n}\n.carousel-control .icon-next:before {\n content: '\\203a';\n}\n.carousel-indicators {\n position: absolute;\n bottom: 10px;\n left: 50%;\n z-index: 15;\n width: 60%;\n margin-left: -30%;\n padding-left: 0;\n list-style: none;\n text-align: center;\n}\n.carousel-indicators li {\n display: inline-block;\n width: 10px;\n height: 10px;\n margin: 1px;\n text-indent: -999px;\n border: 1px solid #ffffff;\n border-radius: 10px;\n cursor: pointer;\n background-color: #000 \\9;\n background-color: rgba(0, 0, 0, 0);\n}\n.carousel-indicators .active {\n margin: 0;\n width: 12px;\n height: 12px;\n background-color: #ffffff;\n}\n.carousel-caption {\n position: absolute;\n left: 15%;\n right: 15%;\n bottom: 20px;\n z-index: 10;\n padding-top: 20px;\n padding-bottom: 20px;\n color: #ffffff;\n text-align: center;\n text-shadow: 0 1px 2px rgba(0, 0, 0, 0.6);\n}\n.carousel-caption .btn {\n text-shadow: none;\n}\n@media screen and (min-width: 768px) {\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-prev,\n .carousel-control .icon-next {\n width: 30px;\n height: 30px;\n margin-top: -15px;\n font-size: 30px;\n }\n .carousel-control .glyphicon-chevron-left,\n .carousel-control .icon-prev {\n margin-left: -15px;\n }\n .carousel-control .glyphicon-chevron-right,\n .carousel-control .icon-next {\n margin-right: -15px;\n }\n .carousel-caption {\n left: 20%;\n right: 20%;\n padding-bottom: 30px;\n }\n .carousel-indicators {\n bottom: 20px;\n }\n}\n.clearfix:before,\n.clearfix:after,\n.dl-horizontal dd:before,\n.dl-horizontal dd:after,\n.container:before,\n.container:after,\n.container-fluid:before,\n.container-fluid:after,\n.row:before,\n.row:after,\n.form-horizontal .form-group:before,\n.form-horizontal .form-group:after,\n.btn-toolbar:before,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:before,\n.btn-group-vertical > .btn-group:after,\n.nav:before,\n.nav:after,\n.navbar:before,\n.navbar:after,\n.navbar-header:before,\n.navbar-header:after,\n.navbar-collapse:before,\n.navbar-collapse:after,\n.pager:before,\n.pager:after,\n.panel-body:before,\n.panel-body:after,\n.modal-footer:before,\n.modal-footer:after {\n content: \" \";\n display: table;\n}\n.clearfix:after,\n.dl-horizontal dd:after,\n.container:after,\n.container-fluid:after,\n.row:after,\n.form-horizontal .form-group:after,\n.btn-toolbar:after,\n.btn-group-vertical > .btn-group:after,\n.nav:after,\n.navbar:after,\n.navbar-header:after,\n.navbar-collapse:after,\n.pager:after,\n.panel-body:after,\n.modal-footer:after {\n clear: both;\n}\n.center-block {\n display: block;\n margin-left: auto;\n margin-right: auto;\n}\n.pull-right {\n float: right !important;\n}\n.pull-left {\n float: left !important;\n}\n.hide {\n display: none !important;\n}\n.show {\n display: block !important;\n}\n.invisible {\n visibility: hidden;\n}\n.text-hide {\n font: 0/0 a;\n color: transparent;\n text-shadow: none;\n background-color: transparent;\n border: 0;\n}\n.hidden {\n display: none !important;\n visibility: hidden !important;\n}\n.affix {\n position: fixed;\n}\n@-ms-viewport {\n width: device-width;\n}\n.visible-xs,\n.visible-sm,\n.visible-md,\n.visible-lg {\n display: none !important;\n}\n.visible-xs-block,\n.visible-xs-inline,\n.visible-xs-inline-block,\n.visible-sm-block,\n.visible-sm-inline,\n.visible-sm-inline-block,\n.visible-md-block,\n.visible-md-inline,\n.visible-md-inline-block,\n.visible-lg-block,\n.visible-lg-inline,\n.visible-lg-inline-block {\n display: none !important;\n}\n@media (max-width: 767px) {\n .visible-xs {\n display: block !important;\n }\n table.visible-xs {\n display: table;\n }\n tr.visible-xs {\n display: table-row !important;\n }\n th.visible-xs,\n td.visible-xs {\n display: table-cell !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-block {\n display: block !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline {\n display: inline !important;\n }\n}\n@media (max-width: 767px) {\n .visible-xs-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm {\n display: block !important;\n }\n table.visible-sm {\n display: table;\n }\n tr.visible-sm {\n display: table-row !important;\n }\n th.visible-sm,\n td.visible-sm {\n display: table-cell !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-block {\n display: block !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline {\n display: inline !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .visible-sm-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md {\n display: block !important;\n }\n table.visible-md {\n display: table;\n }\n tr.visible-md {\n display: table-row !important;\n }\n th.visible-md,\n td.visible-md {\n display: table-cell !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-block {\n display: block !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline {\n display: inline !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .visible-md-inline-block {\n display: inline-block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg {\n display: block !important;\n }\n table.visible-lg {\n display: table;\n }\n tr.visible-lg {\n display: table-row !important;\n }\n th.visible-lg,\n td.visible-lg {\n display: table-cell !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-block {\n display: block !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline {\n display: inline !important;\n }\n}\n@media (min-width: 1200px) {\n .visible-lg-inline-block {\n display: inline-block !important;\n }\n}\n@media (max-width: 767px) {\n .hidden-xs {\n display: none !important;\n }\n}\n@media (min-width: 768px) and (max-width: 991px) {\n .hidden-sm {\n display: none !important;\n }\n}\n@media (min-width: 992px) and (max-width: 1199px) {\n .hidden-md {\n display: none !important;\n }\n}\n@media (min-width: 1200px) {\n .hidden-lg {\n display: none !important;\n }\n}\n.visible-print {\n display: none !important;\n}\n@media print {\n .visible-print {\n display: block !important;\n }\n table.visible-print {\n display: table;\n }\n tr.visible-print {\n display: table-row !important;\n }\n th.visible-print,\n td.visible-print {\n display: table-cell !important;\n }\n}\n.visible-print-block {\n display: none !important;\n}\n@media print {\n .visible-print-block {\n display: block !important;\n }\n}\n.visible-print-inline {\n display: none !important;\n}\n@media print {\n .visible-print-inline {\n display: inline !important;\n }\n}\n.visible-print-inline-block {\n display: none !important;\n}\n@media print {\n .visible-print-inline-block {\n display: inline-block !important;\n }\n}\n@media print {\n .hidden-print {\n display: none !important;\n }\n}\n/*# sourceMappingURL=bootstrap.css.map */","/*! normalize.css v3.0.2 | MIT License | git.io/normalize */\n\n//\n// 1. Set default font family to sans-serif.\n// 2. Prevent iOS text size adjust after orientation change, without disabling\n// user zoom.\n//\n\nhtml {\n font-family: sans-serif; // 1\n -ms-text-size-adjust: 100%; // 2\n -webkit-text-size-adjust: 100%; // 2\n}\n\n//\n// Remove default margin.\n//\n\nbody {\n margin: 0;\n}\n\n// HTML5 display definitions\n// ==========================================================================\n\n//\n// Correct `block` display not defined for any HTML5 element in IE 8/9.\n// Correct `block` display not defined for `details` or `summary` in IE 10/11\n// and Firefox.\n// Correct `block` display not defined for `main` in IE 11.\n//\n\narticle,\naside,\ndetails,\nfigcaption,\nfigure,\nfooter,\nheader,\nhgroup,\nmain,\nmenu,\nnav,\nsection,\nsummary {\n display: block;\n}\n\n//\n// 1. Correct `inline-block` display not defined in IE 8/9.\n// 2. Normalize vertical alignment of `progress` in Chrome, Firefox, and Opera.\n//\n\naudio,\ncanvas,\nprogress,\nvideo {\n display: inline-block; // 1\n vertical-align: baseline; // 2\n}\n\n//\n// Prevent modern browsers from displaying `audio` without controls.\n// Remove excess height in iOS 5 devices.\n//\n\naudio:not([controls]) {\n display: none;\n height: 0;\n}\n\n//\n// Address `[hidden]` styling not present in IE 8/9/10.\n// Hide the `template` element in IE 8/9/11, Safari, and Firefox < 22.\n//\n\n[hidden],\ntemplate {\n display: none;\n}\n\n// Links\n// ==========================================================================\n\n//\n// Remove the gray background color from active links in IE 10.\n//\n\na {\n background-color: transparent;\n}\n\n//\n// Improve readability when focused and also mouse hovered in all browsers.\n//\n\na:active,\na:hover {\n outline: 0;\n}\n\n// Text-level semantics\n// ==========================================================================\n\n//\n// Address styling not present in IE 8/9/10/11, Safari, and Chrome.\n//\n\nabbr[title] {\n border-bottom: 1px dotted;\n}\n\n//\n// Address style set to `bolder` in Firefox 4+, Safari, and Chrome.\n//\n\nb,\nstrong {\n font-weight: bold;\n}\n\n//\n// Address styling not present in Safari and Chrome.\n//\n\ndfn {\n font-style: italic;\n}\n\n//\n// Address variable `h1` font-size and margin within `section` and `article`\n// contexts in Firefox 4+, Safari, and Chrome.\n//\n\nh1 {\n font-size: 2em;\n margin: 0.67em 0;\n}\n\n//\n// Address styling not present in IE 8/9.\n//\n\nmark {\n background: #ff0;\n color: #000;\n}\n\n//\n// Address inconsistent and variable font size in all browsers.\n//\n\nsmall {\n font-size: 80%;\n}\n\n//\n// Prevent `sub` and `sup` affecting `line-height` in all browsers.\n//\n\nsub,\nsup {\n font-size: 75%;\n line-height: 0;\n position: relative;\n vertical-align: baseline;\n}\n\nsup {\n top: -0.5em;\n}\n\nsub {\n bottom: -0.25em;\n}\n\n// Embedded content\n// ==========================================================================\n\n//\n// Remove border when inside `a` element in IE 8/9/10.\n//\n\nimg {\n border: 0;\n}\n\n//\n// Correct overflow not hidden in IE 9/10/11.\n//\n\nsvg:not(:root) {\n overflow: hidden;\n}\n\n// Grouping content\n// ==========================================================================\n\n//\n// Address margin not present in IE 8/9 and Safari.\n//\n\nfigure {\n margin: 1em 40px;\n}\n\n//\n// Address differences between Firefox and other browsers.\n//\n\nhr {\n -moz-box-sizing: content-box;\n box-sizing: content-box;\n height: 0;\n}\n\n//\n// Contain overflow in all browsers.\n//\n\npre {\n overflow: auto;\n}\n\n//\n// Address odd `em`-unit font size rendering in all browsers.\n//\n\ncode,\nkbd,\npre,\nsamp {\n font-family: monospace, monospace;\n font-size: 1em;\n}\n\n// Forms\n// ==========================================================================\n\n//\n// Known limitation: by default, Chrome and Safari on OS X allow very limited\n// styling of `select`, unless a `border` property is set.\n//\n\n//\n// 1. Correct color not being inherited.\n// Known issue: affects color of disabled elements.\n// 2. Correct font properties not being inherited.\n// 3. Address margins set differently in Firefox 4+, Safari, and Chrome.\n//\n\nbutton,\ninput,\noptgroup,\nselect,\ntextarea {\n color: inherit; // 1\n font: inherit; // 2\n margin: 0; // 3\n}\n\n//\n// Address `overflow` set to `hidden` in IE 8/9/10/11.\n//\n\nbutton {\n overflow: visible;\n}\n\n//\n// Address inconsistent `text-transform` inheritance for `button` and `select`.\n// All other form control elements do not inherit `text-transform` values.\n// Correct `button` style inheritance in Firefox, IE 8/9/10/11, and Opera.\n// Correct `select` style inheritance in Firefox.\n//\n\nbutton,\nselect {\n text-transform: none;\n}\n\n//\n// 1. Avoid the WebKit bug in Android 4.0.* where (2) destroys native `audio`\n// and `video` controls.\n// 2. Correct inability to style clickable `input` types in iOS.\n// 3. Improve usability and consistency of cursor style between image-type\n// `input` and others.\n//\n\nbutton,\nhtml input[type=\"button\"], // 1\ninput[type=\"reset\"],\ninput[type=\"submit\"] {\n -webkit-appearance: button; // 2\n cursor: pointer; // 3\n}\n\n//\n// Re-set default cursor for disabled elements.\n//\n\nbutton[disabled],\nhtml input[disabled] {\n cursor: default;\n}\n\n//\n// Remove inner padding and border in Firefox 4+.\n//\n\nbutton::-moz-focus-inner,\ninput::-moz-focus-inner {\n border: 0;\n padding: 0;\n}\n\n//\n// Address Firefox 4+ setting `line-height` on `input` using `!important` in\n// the UA stylesheet.\n//\n\ninput {\n line-height: normal;\n}\n\n//\n// It's recommended that you don't attempt to style these elements.\n// Firefox's implementation doesn't respect box-sizing, padding, or width.\n//\n// 1. Address box sizing set to `content-box` in IE 8/9/10.\n// 2. Remove excess padding in IE 8/9/10.\n//\n\ninput[type=\"checkbox\"],\ninput[type=\"radio\"] {\n box-sizing: border-box; // 1\n padding: 0; // 2\n}\n\n//\n// Fix the cursor style for Chrome's increment/decrement buttons. For certain\n// `font-size` values of the `input`, it causes the cursor style of the\n// decrement button to change from `default` to `text`.\n//\n\ninput[type=\"number\"]::-webkit-inner-spin-button,\ninput[type=\"number\"]::-webkit-outer-spin-button {\n height: auto;\n}\n\n//\n// 1. Address `appearance` set to `searchfield` in Safari and Chrome.\n// 2. Address `box-sizing` set to `border-box` in Safari and Chrome\n// (include `-moz` to future-proof).\n//\n\ninput[type=\"search\"] {\n -webkit-appearance: textfield; // 1\n -moz-box-sizing: content-box;\n -webkit-box-sizing: content-box; // 2\n box-sizing: content-box;\n}\n\n//\n// Remove inner padding and search cancel button in Safari and Chrome on OS X.\n// Safari (but not Chrome) clips the cancel button when the search input has\n// padding (and `textfield` appearance).\n//\n\ninput[type=\"search\"]::-webkit-search-cancel-button,\ninput[type=\"search\"]::-webkit-search-decoration {\n -webkit-appearance: none;\n}\n\n//\n// Define consistent border, margin, and padding.\n//\n\nfieldset {\n border: 1px solid #c0c0c0;\n margin: 0 2px;\n padding: 0.35em 0.625em 0.75em;\n}\n\n//\n// 1. Correct `color` not being inherited in IE 8/9/10/11.\n// 2. Remove padding so people aren't caught out if they zero out fieldsets.\n//\n\nlegend {\n border: 0; // 1\n padding: 0; // 2\n}\n\n//\n// Remove default vertical scrollbar in IE 8/9/10/11.\n//\n\ntextarea {\n overflow: auto;\n}\n\n//\n// Don't inherit the `font-weight` (applied by a rule above).\n// NOTE: the default cannot safely be changed in Chrome and Safari on OS X.\n//\n\noptgroup {\n font-weight: bold;\n}\n\n// Tables\n// ==========================================================================\n\n//\n// Remove most spacing between table cells.\n//\n\ntable {\n border-collapse: collapse;\n border-spacing: 0;\n}\n\ntd,\nth {\n padding: 0;\n}\n","/*! Source: https://github.com/h5bp/html5-boilerplate/blob/master/src/css/main.css */\n\n// ==========================================================================\n// Print styles.\n// Inlined to avoid the additional HTTP request: h5bp.com/r\n// ==========================================================================\n\n@media print {\n *,\n *:before,\n *:after {\n background: transparent !important;\n color: #000 !important; // Black prints faster: h5bp.com/s\n box-shadow: none !important;\n text-shadow: none !important;\n }\n\n a,\n a:visited {\n text-decoration: underline;\n }\n\n a[href]:after {\n content: \" (\" attr(href) \")\";\n }\n\n abbr[title]:after {\n content: \" (\" attr(title) \")\";\n }\n\n // Don't show links that are fragment identifiers,\n // or use the `javascript:` pseudo protocol\n a[href^=\"#\"]:after,\n a[href^=\"javascript:\"]:after {\n content: \"\";\n }\n\n pre,\n blockquote {\n border: 1px solid #999;\n page-break-inside: avoid;\n }\n\n thead {\n display: table-header-group; // h5bp.com/t\n }\n\n tr,\n img {\n page-break-inside: avoid;\n }\n\n img {\n max-width: 100% !important;\n }\n\n p,\n h2,\n h3 {\n orphans: 3;\n widows: 3;\n }\n\n h2,\n h3 {\n page-break-after: avoid;\n }\n\n // Bootstrap specific changes start\n //\n // Chrome (OSX) fix for https://github.com/twbs/bootstrap/issues/11245\n // Once fixed, we can just straight up remove this.\n select {\n background: #fff !important;\n }\n\n // Bootstrap components\n .navbar {\n display: none;\n }\n .btn,\n .dropup > .btn {\n > .caret {\n border-top-color: #000 !important;\n }\n }\n .label {\n border: 1px solid #000;\n }\n\n .table {\n border-collapse: collapse !important;\n\n td,\n th {\n background-color: #fff !important;\n }\n }\n .table-bordered {\n th,\n td {\n border: 1px solid #ddd !important;\n }\n }\n\n // Bootstrap specific changes end\n}\n","//\n// Glyphicons for Bootstrap\n//\n// Since icons are fonts, they can be placed anywhere text is placed and are\n// thus automatically sized to match the surrounding child. To use, create an\n// inline element with the appropriate classes, like so:\n//\n// Star\n\n// Import the fonts\n@font-face {\n font-family: 'Glyphicons Halflings';\n src: url('@{icon-font-path}@{icon-font-name}.eot');\n src: url('@{icon-font-path}@{icon-font-name}.eot?#iefix') format('embedded-opentype'),\n url('@{icon-font-path}@{icon-font-name}.woff') format('woff'),\n url('@{icon-font-path}@{icon-font-name}.ttf') format('truetype'),\n url('@{icon-font-path}@{icon-font-name}.svg#@{icon-font-svg-id}') format('svg');\n}\n\n// Catchall baseclass\n.glyphicon {\n position: relative;\n top: 1px;\n display: inline-block;\n font-family: 'Glyphicons Halflings';\n font-style: normal;\n font-weight: normal;\n line-height: 1;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n\n// Individual icons\n.glyphicon-asterisk { &:before { content: \"\\2a\"; } }\n.glyphicon-plus { &:before { content: \"\\2b\"; } }\n.glyphicon-euro,\n.glyphicon-eur { &:before { content: \"\\20ac\"; } }\n.glyphicon-minus { &:before { content: \"\\2212\"; } }\n.glyphicon-cloud { &:before { content: \"\\2601\"; } }\n.glyphicon-envelope { &:before { content: \"\\2709\"; } }\n.glyphicon-pencil { &:before { content: \"\\270f\"; } }\n.glyphicon-glass { &:before { content: \"\\e001\"; } }\n.glyphicon-music { &:before { content: \"\\e002\"; } }\n.glyphicon-search { &:before { content: \"\\e003\"; } }\n.glyphicon-heart { &:before { content: \"\\e005\"; } }\n.glyphicon-star { &:before { content: \"\\e006\"; } }\n.glyphicon-star-empty { &:before { content: \"\\e007\"; } }\n.glyphicon-user { &:before { content: \"\\e008\"; } }\n.glyphicon-film { &:before { content: \"\\e009\"; } }\n.glyphicon-th-large { &:before { content: \"\\e010\"; } }\n.glyphicon-th { &:before { content: \"\\e011\"; } }\n.glyphicon-th-list { &:before { content: \"\\e012\"; } }\n.glyphicon-ok { &:before { content: \"\\e013\"; } }\n.glyphicon-remove { &:before { content: \"\\e014\"; } }\n.glyphicon-zoom-in { &:before { content: \"\\e015\"; } }\n.glyphicon-zoom-out { &:before { content: \"\\e016\"; } }\n.glyphicon-off { &:before { content: \"\\e017\"; } }\n.glyphicon-signal { &:before { content: \"\\e018\"; } }\n.glyphicon-cog { &:before { content: \"\\e019\"; } }\n.glyphicon-trash { &:before { content: \"\\e020\"; } }\n.glyphicon-home { &:before { content: \"\\e021\"; } }\n.glyphicon-file { &:before { content: \"\\e022\"; } }\n.glyphicon-time { &:before { content: \"\\e023\"; } }\n.glyphicon-road { &:before { content: \"\\e024\"; } }\n.glyphicon-download-alt { &:before { content: \"\\e025\"; } }\n.glyphicon-download { &:before { content: \"\\e026\"; } }\n.glyphicon-upload { &:before { content: \"\\e027\"; } }\n.glyphicon-inbox { &:before { content: \"\\e028\"; } }\n.glyphicon-play-circle { &:before { content: \"\\e029\"; } }\n.glyphicon-repeat { &:before { content: \"\\e030\"; } }\n.glyphicon-refresh { &:before { content: \"\\e031\"; } }\n.glyphicon-list-alt { &:before { content: \"\\e032\"; } }\n.glyphicon-lock { &:before { content: \"\\e033\"; } }\n.glyphicon-flag { &:before { content: \"\\e034\"; } }\n.glyphicon-headphones { &:before { content: \"\\e035\"; } }\n.glyphicon-volume-off { &:before { content: \"\\e036\"; } }\n.glyphicon-volume-down { &:before { content: \"\\e037\"; } }\n.glyphicon-volume-up { &:before { content: \"\\e038\"; } }\n.glyphicon-qrcode { &:before { content: \"\\e039\"; } }\n.glyphicon-barcode { &:before { content: \"\\e040\"; } }\n.glyphicon-tag { &:before { content: \"\\e041\"; } }\n.glyphicon-tags { &:before { content: \"\\e042\"; } }\n.glyphicon-book { &:before { content: \"\\e043\"; } }\n.glyphicon-bookmark { &:before { content: \"\\e044\"; } }\n.glyphicon-print { &:before { content: \"\\e045\"; } }\n.glyphicon-camera { &:before { content: \"\\e046\"; } }\n.glyphicon-font { &:before { content: \"\\e047\"; } }\n.glyphicon-bold { &:before { content: \"\\e048\"; } }\n.glyphicon-italic { &:before { content: \"\\e049\"; } }\n.glyphicon-text-height { &:before { content: \"\\e050\"; } }\n.glyphicon-text-width { &:before { content: \"\\e051\"; } }\n.glyphicon-align-left { &:before { content: \"\\e052\"; } }\n.glyphicon-align-center { &:before { content: \"\\e053\"; } }\n.glyphicon-align-right { &:before { content: \"\\e054\"; } }\n.glyphicon-align-justify { &:before { content: \"\\e055\"; } }\n.glyphicon-list { &:before { content: \"\\e056\"; } }\n.glyphicon-indent-left { &:before { content: \"\\e057\"; } }\n.glyphicon-indent-right { &:before { content: \"\\e058\"; } }\n.glyphicon-facetime-video { &:before { content: \"\\e059\"; } }\n.glyphicon-picture { &:before { content: \"\\e060\"; } }\n.glyphicon-map-marker { &:before { content: \"\\e062\"; } }\n.glyphicon-adjust { &:before { content: \"\\e063\"; } }\n.glyphicon-tint { &:before { content: \"\\e064\"; } }\n.glyphicon-edit { &:before { content: \"\\e065\"; } }\n.glyphicon-share { &:before { content: \"\\e066\"; } }\n.glyphicon-check { &:before { content: \"\\e067\"; } }\n.glyphicon-move { &:before { content: \"\\e068\"; } }\n.glyphicon-step-backward { &:before { content: \"\\e069\"; } }\n.glyphicon-fast-backward { &:before { content: \"\\e070\"; } }\n.glyphicon-backward { &:before { content: \"\\e071\"; } }\n.glyphicon-play { &:before { content: \"\\e072\"; } }\n.glyphicon-pause { &:before { content: \"\\e073\"; } }\n.glyphicon-stop { &:before { content: \"\\e074\"; } }\n.glyphicon-forward { &:before { content: \"\\e075\"; } }\n.glyphicon-fast-forward { &:before { content: \"\\e076\"; } }\n.glyphicon-step-forward { &:before { content: \"\\e077\"; } }\n.glyphicon-eject { &:before { content: \"\\e078\"; } }\n.glyphicon-chevron-left { &:before { content: \"\\e079\"; } }\n.glyphicon-chevron-right { &:before { content: \"\\e080\"; } }\n.glyphicon-plus-sign { &:before { content: \"\\e081\"; } }\n.glyphicon-minus-sign { &:before { content: \"\\e082\"; } }\n.glyphicon-remove-sign { &:before { content: \"\\e083\"; } }\n.glyphicon-ok-sign { &:before { content: \"\\e084\"; } }\n.glyphicon-question-sign { &:before { content: \"\\e085\"; } }\n.glyphicon-info-sign { &:before { content: \"\\e086\"; } }\n.glyphicon-screenshot { &:before { content: \"\\e087\"; } }\n.glyphicon-remove-circle { &:before { content: \"\\e088\"; } }\n.glyphicon-ok-circle { &:before { content: \"\\e089\"; } }\n.glyphicon-ban-circle { &:before { content: \"\\e090\"; } }\n.glyphicon-arrow-left { &:before { content: \"\\e091\"; } }\n.glyphicon-arrow-right { &:before { content: \"\\e092\"; } }\n.glyphicon-arrow-up { &:before { content: \"\\e093\"; } }\n.glyphicon-arrow-down { &:before { content: \"\\e094\"; } }\n.glyphicon-share-alt { &:before { content: \"\\e095\"; } }\n.glyphicon-resize-full { &:before { content: \"\\e096\"; } }\n.glyphicon-resize-small { &:before { content: \"\\e097\"; } }\n.glyphicon-exclamation-sign { &:before { content: \"\\e101\"; } }\n.glyphicon-gift { &:before { content: \"\\e102\"; } }\n.glyphicon-leaf { &:before { content: \"\\e103\"; } }\n.glyphicon-fire { &:before { content: \"\\e104\"; } }\n.glyphicon-eye-open { &:before { content: \"\\e105\"; } }\n.glyphicon-eye-close { &:before { content: \"\\e106\"; } }\n.glyphicon-warning-sign { &:before { content: \"\\e107\"; } }\n.glyphicon-plane { &:before { content: \"\\e108\"; } }\n.glyphicon-calendar { &:before { content: \"\\e109\"; } }\n.glyphicon-random { &:before { content: \"\\e110\"; } }\n.glyphicon-comment { &:before { content: \"\\e111\"; } }\n.glyphicon-magnet { &:before { content: \"\\e112\"; } }\n.glyphicon-chevron-up { &:before { content: \"\\e113\"; } }\n.glyphicon-chevron-down { &:before { content: \"\\e114\"; } }\n.glyphicon-retweet { &:before { content: \"\\e115\"; } }\n.glyphicon-shopping-cart { &:before { content: \"\\e116\"; } }\n.glyphicon-folder-close { &:before { content: \"\\e117\"; } }\n.glyphicon-folder-open { &:before { content: \"\\e118\"; } }\n.glyphicon-resize-vertical { &:before { content: \"\\e119\"; } }\n.glyphicon-resize-horizontal { &:before { content: \"\\e120\"; } }\n.glyphicon-hdd { &:before { content: \"\\e121\"; } }\n.glyphicon-bullhorn { &:before { content: \"\\e122\"; } }\n.glyphicon-bell { &:before { content: \"\\e123\"; } }\n.glyphicon-certificate { &:before { content: \"\\e124\"; } }\n.glyphicon-thumbs-up { &:before { content: \"\\e125\"; } }\n.glyphicon-thumbs-down { &:before { content: \"\\e126\"; } }\n.glyphicon-hand-right { &:before { content: \"\\e127\"; } }\n.glyphicon-hand-left { &:before { content: \"\\e128\"; } }\n.glyphicon-hand-up { &:before { content: \"\\e129\"; } }\n.glyphicon-hand-down { &:before { content: \"\\e130\"; } }\n.glyphicon-circle-arrow-right { &:before { content: \"\\e131\"; } }\n.glyphicon-circle-arrow-left { &:before { content: \"\\e132\"; } }\n.glyphicon-circle-arrow-up { &:before { content: \"\\e133\"; } }\n.glyphicon-circle-arrow-down { &:before { content: \"\\e134\"; } }\n.glyphicon-globe { &:before { content: \"\\e135\"; } }\n.glyphicon-wrench { &:before { content: \"\\e136\"; } }\n.glyphicon-tasks { &:before { content: \"\\e137\"; } }\n.glyphicon-filter { &:before { content: \"\\e138\"; } }\n.glyphicon-briefcase { &:before { content: \"\\e139\"; } }\n.glyphicon-fullscreen { &:before { content: \"\\e140\"; } }\n.glyphicon-dashboard { &:before { content: \"\\e141\"; } }\n.glyphicon-paperclip { &:before { content: \"\\e142\"; } }\n.glyphicon-heart-empty { &:before { content: \"\\e143\"; } }\n.glyphicon-link { &:before { content: \"\\e144\"; } }\n.glyphicon-phone { &:before { content: \"\\e145\"; } }\n.glyphicon-pushpin { &:before { content: \"\\e146\"; } }\n.glyphicon-usd { &:before { content: \"\\e148\"; } }\n.glyphicon-gbp { &:before { content: \"\\e149\"; } }\n.glyphicon-sort { &:before { content: \"\\e150\"; } }\n.glyphicon-sort-by-alphabet { &:before { content: \"\\e151\"; } }\n.glyphicon-sort-by-alphabet-alt { &:before { content: \"\\e152\"; } }\n.glyphicon-sort-by-order { &:before { content: \"\\e153\"; } }\n.glyphicon-sort-by-order-alt { &:before { content: \"\\e154\"; } }\n.glyphicon-sort-by-attributes { &:before { content: \"\\e155\"; } }\n.glyphicon-sort-by-attributes-alt { &:before { content: \"\\e156\"; } }\n.glyphicon-unchecked { &:before { content: \"\\e157\"; } }\n.glyphicon-expand { &:before { content: \"\\e158\"; } }\n.glyphicon-collapse-down { &:before { content: \"\\e159\"; } }\n.glyphicon-collapse-up { &:before { content: \"\\e160\"; } }\n.glyphicon-log-in { &:before { content: \"\\e161\"; } }\n.glyphicon-flash { &:before { content: \"\\e162\"; } }\n.glyphicon-log-out { &:before { content: \"\\e163\"; } }\n.glyphicon-new-window { &:before { content: \"\\e164\"; } }\n.glyphicon-record { &:before { content: \"\\e165\"; } }\n.glyphicon-save { &:before { content: \"\\e166\"; } }\n.glyphicon-open { &:before { content: \"\\e167\"; } }\n.glyphicon-saved { &:before { content: \"\\e168\"; } }\n.glyphicon-import { &:before { content: \"\\e169\"; } }\n.glyphicon-export { &:before { content: \"\\e170\"; } }\n.glyphicon-send { &:before { content: \"\\e171\"; } }\n.glyphicon-floppy-disk { &:before { content: \"\\e172\"; } }\n.glyphicon-floppy-saved { &:before { content: \"\\e173\"; } }\n.glyphicon-floppy-remove { &:before { content: \"\\e174\"; } }\n.glyphicon-floppy-save { &:before { content: \"\\e175\"; } }\n.glyphicon-floppy-open { &:before { content: \"\\e176\"; } }\n.glyphicon-credit-card { &:before { content: \"\\e177\"; } }\n.glyphicon-transfer { &:before { content: \"\\e178\"; } }\n.glyphicon-cutlery { &:before { content: \"\\e179\"; } }\n.glyphicon-header { &:before { content: \"\\e180\"; } }\n.glyphicon-compressed { &:before { content: \"\\e181\"; } }\n.glyphicon-earphone { &:before { content: \"\\e182\"; } }\n.glyphicon-phone-alt { &:before { content: \"\\e183\"; } }\n.glyphicon-tower { &:before { content: \"\\e184\"; } }\n.glyphicon-stats { &:before { content: \"\\e185\"; } }\n.glyphicon-sd-video { &:before { content: \"\\e186\"; } }\n.glyphicon-hd-video { &:before { content: \"\\e187\"; } }\n.glyphicon-subtitles { &:before { content: \"\\e188\"; } }\n.glyphicon-sound-stereo { &:before { content: \"\\e189\"; } }\n.glyphicon-sound-dolby { &:before { content: \"\\e190\"; } }\n.glyphicon-sound-5-1 { &:before { content: \"\\e191\"; } }\n.glyphicon-sound-6-1 { &:before { content: \"\\e192\"; } }\n.glyphicon-sound-7-1 { &:before { content: \"\\e193\"; } }\n.glyphicon-copyright-mark { &:before { content: \"\\e194\"; } }\n.glyphicon-registration-mark { &:before { content: \"\\e195\"; } }\n.glyphicon-cloud-download { &:before { content: \"\\e197\"; } }\n.glyphicon-cloud-upload { &:before { content: \"\\e198\"; } }\n.glyphicon-tree-conifer { &:before { content: \"\\e199\"; } }\n.glyphicon-tree-deciduous { &:before { content: \"\\e200\"; } }\n","//\n// Scaffolding\n// --------------------------------------------------\n\n\n// Reset the box-sizing\n//\n// Heads up! This reset may cause conflicts with some third-party widgets.\n// For recommendations on resolving such conflicts, see\n// http://getbootstrap.com/getting-started/#third-box-sizing\n* {\n .box-sizing(border-box);\n}\n*:before,\n*:after {\n .box-sizing(border-box);\n}\n\n\n// Body reset\n\nhtml {\n font-size: 10px;\n -webkit-tap-highlight-color: rgba(0,0,0,0);\n}\n\nbody {\n font-family: @font-family-base;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @text-color;\n background-color: @body-bg;\n}\n\n// Reset fonts for relevant elements\ninput,\nbutton,\nselect,\ntextarea {\n font-family: inherit;\n font-size: inherit;\n line-height: inherit;\n}\n\n\n// Links\n\na {\n color: @link-color;\n text-decoration: none;\n\n &:hover,\n &:focus {\n color: @link-hover-color;\n text-decoration: @link-hover-decoration;\n }\n\n &:focus {\n .tab-focus();\n }\n}\n\n\n// Figures\n//\n// We reset this here because previously Normalize had no `figure` margins. This\n// ensures we don't break anyone's use of the element.\n\nfigure {\n margin: 0;\n}\n\n\n// Images\n\nimg {\n vertical-align: middle;\n}\n\n// Responsive images (ensure images don't scale beyond their parents)\n.img-responsive {\n .img-responsive();\n}\n\n// Rounded corners\n.img-rounded {\n border-radius: @border-radius-large;\n}\n\n// Image thumbnails\n//\n// Heads up! This is mixin-ed into thumbnails.less for `.thumbnail`.\n.img-thumbnail {\n padding: @thumbnail-padding;\n line-height: @line-height-base;\n background-color: @thumbnail-bg;\n border: 1px solid @thumbnail-border;\n border-radius: @thumbnail-border-radius;\n .transition(all .2s ease-in-out);\n\n // Keep them at most 100% wide\n .img-responsive(inline-block);\n}\n\n// Perfect circle\n.img-circle {\n border-radius: 50%; // set radius in percents\n}\n\n\n// Horizontal rules\n\nhr {\n margin-top: @line-height-computed;\n margin-bottom: @line-height-computed;\n border: 0;\n border-top: 1px solid @hr-border;\n}\n\n\n// Only display content to screen readers\n//\n// See: http://a11yproject.com/posts/how-to-hide-content/\n\n.sr-only {\n position: absolute;\n width: 1px;\n height: 1px;\n margin: -1px;\n padding: 0;\n overflow: hidden;\n clip: rect(0,0,0,0);\n border: 0;\n}\n\n// Use in conjunction with .sr-only to only display content when it's focused.\n// Useful for \"Skip to main content\" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1\n// Credit: HTML5 Boilerplate\n\n.sr-only-focusable {\n &:active,\n &:focus {\n position: static;\n width: auto;\n height: auto;\n margin: 0;\n overflow: visible;\n clip: auto;\n }\n}\n","// Vendor Prefixes\n//\n// All vendor mixins are deprecated as of v3.2.0 due to the introduction of\n// Autoprefixer in our Gruntfile. They will be removed in v4.\n\n// - Animations\n// - Backface visibility\n// - Box shadow\n// - Box sizing\n// - Content columns\n// - Hyphens\n// - Placeholder text\n// - Transformations\n// - Transitions\n// - User Select\n\n\n// Animations\n.animation(@animation) {\n -webkit-animation: @animation;\n -o-animation: @animation;\n animation: @animation;\n}\n.animation-name(@name) {\n -webkit-animation-name: @name;\n animation-name: @name;\n}\n.animation-duration(@duration) {\n -webkit-animation-duration: @duration;\n animation-duration: @duration;\n}\n.animation-timing-function(@timing-function) {\n -webkit-animation-timing-function: @timing-function;\n animation-timing-function: @timing-function;\n}\n.animation-delay(@delay) {\n -webkit-animation-delay: @delay;\n animation-delay: @delay;\n}\n.animation-iteration-count(@iteration-count) {\n -webkit-animation-iteration-count: @iteration-count;\n animation-iteration-count: @iteration-count;\n}\n.animation-direction(@direction) {\n -webkit-animation-direction: @direction;\n animation-direction: @direction;\n}\n.animation-fill-mode(@fill-mode) {\n -webkit-animation-fill-mode: @fill-mode;\n animation-fill-mode: @fill-mode;\n}\n\n// Backface visibility\n// Prevent browsers from flickering when using CSS 3D transforms.\n// Default value is `visible`, but can be changed to `hidden`\n\n.backface-visibility(@visibility){\n -webkit-backface-visibility: @visibility;\n -moz-backface-visibility: @visibility;\n backface-visibility: @visibility;\n}\n\n// Drop shadows\n//\n// Note: Deprecated `.box-shadow()` as of v3.1.0 since all of Bootstrap's\n// supported browsers that have box shadow capabilities now support it.\n\n.box-shadow(@shadow) {\n -webkit-box-shadow: @shadow; // iOS <4.3 & Android <4.1\n box-shadow: @shadow;\n}\n\n// Box sizing\n.box-sizing(@boxmodel) {\n -webkit-box-sizing: @boxmodel;\n -moz-box-sizing: @boxmodel;\n box-sizing: @boxmodel;\n}\n\n// CSS3 Content Columns\n.content-columns(@column-count; @column-gap: @grid-gutter-width) {\n -webkit-column-count: @column-count;\n -moz-column-count: @column-count;\n column-count: @column-count;\n -webkit-column-gap: @column-gap;\n -moz-column-gap: @column-gap;\n column-gap: @column-gap;\n}\n\n// Optional hyphenation\n.hyphens(@mode: auto) {\n word-wrap: break-word;\n -webkit-hyphens: @mode;\n -moz-hyphens: @mode;\n -ms-hyphens: @mode; // IE10+\n -o-hyphens: @mode;\n hyphens: @mode;\n}\n\n// Placeholder text\n.placeholder(@color: @input-color-placeholder) {\n // Firefox\n &::-moz-placeholder {\n color: @color;\n opacity: 1; // See https://github.com/twbs/bootstrap/pull/11526\n }\n &:-ms-input-placeholder { color: @color; } // Internet Explorer 10+\n &::-webkit-input-placeholder { color: @color; } // Safari and Chrome\n}\n\n// Transformations\n.scale(@ratio) {\n -webkit-transform: scale(@ratio);\n -ms-transform: scale(@ratio); // IE9 only\n -o-transform: scale(@ratio);\n transform: scale(@ratio);\n}\n.scale(@ratioX; @ratioY) {\n -webkit-transform: scale(@ratioX, @ratioY);\n -ms-transform: scale(@ratioX, @ratioY); // IE9 only\n -o-transform: scale(@ratioX, @ratioY);\n transform: scale(@ratioX, @ratioY);\n}\n.scaleX(@ratio) {\n -webkit-transform: scaleX(@ratio);\n -ms-transform: scaleX(@ratio); // IE9 only\n -o-transform: scaleX(@ratio);\n transform: scaleX(@ratio);\n}\n.scaleY(@ratio) {\n -webkit-transform: scaleY(@ratio);\n -ms-transform: scaleY(@ratio); // IE9 only\n -o-transform: scaleY(@ratio);\n transform: scaleY(@ratio);\n}\n.skew(@x; @y) {\n -webkit-transform: skewX(@x) skewY(@y);\n -ms-transform: skewX(@x) skewY(@y); // See https://github.com/twbs/bootstrap/issues/4885; IE9+\n -o-transform: skewX(@x) skewY(@y);\n transform: skewX(@x) skewY(@y);\n}\n.translate(@x; @y) {\n -webkit-transform: translate(@x, @y);\n -ms-transform: translate(@x, @y); // IE9 only\n -o-transform: translate(@x, @y);\n transform: translate(@x, @y);\n}\n.translate3d(@x; @y; @z) {\n -webkit-transform: translate3d(@x, @y, @z);\n transform: translate3d(@x, @y, @z);\n}\n.rotate(@degrees) {\n -webkit-transform: rotate(@degrees);\n -ms-transform: rotate(@degrees); // IE9 only\n -o-transform: rotate(@degrees);\n transform: rotate(@degrees);\n}\n.rotateX(@degrees) {\n -webkit-transform: rotateX(@degrees);\n -ms-transform: rotateX(@degrees); // IE9 only\n -o-transform: rotateX(@degrees);\n transform: rotateX(@degrees);\n}\n.rotateY(@degrees) {\n -webkit-transform: rotateY(@degrees);\n -ms-transform: rotateY(@degrees); // IE9 only\n -o-transform: rotateY(@degrees);\n transform: rotateY(@degrees);\n}\n.perspective(@perspective) {\n -webkit-perspective: @perspective;\n -moz-perspective: @perspective;\n perspective: @perspective;\n}\n.perspective-origin(@perspective) {\n -webkit-perspective-origin: @perspective;\n -moz-perspective-origin: @perspective;\n perspective-origin: @perspective;\n}\n.transform-origin(@origin) {\n -webkit-transform-origin: @origin;\n -moz-transform-origin: @origin;\n -ms-transform-origin: @origin; // IE9 only\n transform-origin: @origin;\n}\n\n\n// Transitions\n\n.transition(@transition) {\n -webkit-transition: @transition;\n -o-transition: @transition;\n transition: @transition;\n}\n.transition-property(@transition-property) {\n -webkit-transition-property: @transition-property;\n transition-property: @transition-property;\n}\n.transition-delay(@transition-delay) {\n -webkit-transition-delay: @transition-delay;\n transition-delay: @transition-delay;\n}\n.transition-duration(@transition-duration) {\n -webkit-transition-duration: @transition-duration;\n transition-duration: @transition-duration;\n}\n.transition-timing-function(@timing-function) {\n -webkit-transition-timing-function: @timing-function;\n transition-timing-function: @timing-function;\n}\n.transition-transform(@transition) {\n -webkit-transition: -webkit-transform @transition;\n -moz-transition: -moz-transform @transition;\n -o-transition: -o-transform @transition;\n transition: transform @transition;\n}\n\n\n// User select\n// For selecting text on the page\n\n.user-select(@select) {\n -webkit-user-select: @select;\n -moz-user-select: @select;\n -ms-user-select: @select; // IE10+\n user-select: @select;\n}\n","// WebKit-style focus\n\n.tab-focus() {\n // Default\n outline: thin dotted;\n // WebKit\n outline: 5px auto -webkit-focus-ring-color;\n outline-offset: -2px;\n}\n","// Image Mixins\n// - Responsive image\n// - Retina image\n\n\n// Responsive image\n//\n// Keep images from scaling beyond the width of their parents.\n.img-responsive(@display: block) {\n display: @display;\n max-width: 100%; // Part 1: Set a maximum relative to the parent\n height: auto; // Part 2: Scale the height according to the width, otherwise you get stretching\n}\n\n\n// Retina image\n//\n// Short retina mixin for setting background-image and -size. Note that the\n// spelling of `min--moz-device-pixel-ratio` is intentional.\n.img-retina(@file-1x; @file-2x; @width-1x; @height-1x) {\n background-image: url(\"@{file-1x}\");\n\n @media\n only screen and (-webkit-min-device-pixel-ratio: 2),\n only screen and ( min--moz-device-pixel-ratio: 2),\n only screen and ( -o-min-device-pixel-ratio: 2/1),\n only screen and ( min-device-pixel-ratio: 2),\n only screen and ( min-resolution: 192dpi),\n only screen and ( min-resolution: 2dppx) {\n background-image: url(\"@{file-2x}\");\n background-size: @width-1x @height-1x;\n }\n}\n","//\n// Typography\n// --------------------------------------------------\n\n\n// Headings\n// -------------------------\n\nh1, h2, h3, h4, h5, h6,\n.h1, .h2, .h3, .h4, .h5, .h6 {\n font-family: @headings-font-family;\n font-weight: @headings-font-weight;\n line-height: @headings-line-height;\n color: @headings-color;\n\n small,\n .small {\n font-weight: normal;\n line-height: 1;\n color: @headings-small-color;\n }\n}\n\nh1, .h1,\nh2, .h2,\nh3, .h3 {\n margin-top: @line-height-computed;\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 65%;\n }\n}\nh4, .h4,\nh5, .h5,\nh6, .h6 {\n margin-top: (@line-height-computed / 2);\n margin-bottom: (@line-height-computed / 2);\n\n small,\n .small {\n font-size: 75%;\n }\n}\n\nh1, .h1 { font-size: @font-size-h1; }\nh2, .h2 { font-size: @font-size-h2; }\nh3, .h3 { font-size: @font-size-h3; }\nh4, .h4 { font-size: @font-size-h4; }\nh5, .h5 { font-size: @font-size-h5; }\nh6, .h6 { font-size: @font-size-h6; }\n\n\n// Body text\n// -------------------------\n\np {\n margin: 0 0 (@line-height-computed / 2);\n}\n\n.lead {\n margin-bottom: @line-height-computed;\n font-size: floor((@font-size-base * 1.15));\n font-weight: 300;\n line-height: 1.4;\n\n @media (min-width: @screen-sm-min) {\n font-size: (@font-size-base * 1.5);\n }\n}\n\n\n// Emphasis & misc\n// -------------------------\n\n// Ex: (12px small font / 14px base font) * 100% = about 85%\nsmall,\n.small {\n font-size: floor((100% * @font-size-small / @font-size-base));\n}\n\nmark,\n.mark {\n background-color: @state-warning-bg;\n padding: .2em;\n}\n\n// Alignment\n.text-left { text-align: left; }\n.text-right { text-align: right; }\n.text-center { text-align: center; }\n.text-justify { text-align: justify; }\n.text-nowrap { white-space: nowrap; }\n\n// Transformation\n.text-lowercase { text-transform: lowercase; }\n.text-uppercase { text-transform: uppercase; }\n.text-capitalize { text-transform: capitalize; }\n\n// Contextual colors\n.text-muted {\n color: @text-muted;\n}\n.text-primary {\n .text-emphasis-variant(@brand-primary);\n}\n.text-success {\n .text-emphasis-variant(@state-success-text);\n}\n.text-info {\n .text-emphasis-variant(@state-info-text);\n}\n.text-warning {\n .text-emphasis-variant(@state-warning-text);\n}\n.text-danger {\n .text-emphasis-variant(@state-danger-text);\n}\n\n// Contextual backgrounds\n// For now we'll leave these alongside the text classes until v4 when we can\n// safely shift things around (per SemVer rules).\n.bg-primary {\n // Given the contrast here, this is the only class to have its color inverted\n // automatically.\n color: #fff;\n .bg-variant(@brand-primary);\n}\n.bg-success {\n .bg-variant(@state-success-bg);\n}\n.bg-info {\n .bg-variant(@state-info-bg);\n}\n.bg-warning {\n .bg-variant(@state-warning-bg);\n}\n.bg-danger {\n .bg-variant(@state-danger-bg);\n}\n\n\n// Page header\n// -------------------------\n\n.page-header {\n padding-bottom: ((@line-height-computed / 2) - 1);\n margin: (@line-height-computed * 2) 0 @line-height-computed;\n border-bottom: 1px solid @page-header-border-color;\n}\n\n\n// Lists\n// -------------------------\n\n// Unordered and Ordered lists\nul,\nol {\n margin-top: 0;\n margin-bottom: (@line-height-computed / 2);\n ul,\n ol {\n margin-bottom: 0;\n }\n}\n\n// List options\n\n// Unstyled keeps list items block level, just removes default browser padding and list-style\n.list-unstyled {\n padding-left: 0;\n list-style: none;\n}\n\n// Inline turns list items into inline-block\n.list-inline {\n .list-unstyled();\n margin-left: -5px;\n\n > li {\n display: inline-block;\n padding-left: 5px;\n padding-right: 5px;\n }\n}\n\n// Description Lists\ndl {\n margin-top: 0; // Remove browser default\n margin-bottom: @line-height-computed;\n}\ndt,\ndd {\n line-height: @line-height-base;\n}\ndt {\n font-weight: bold;\n}\ndd {\n margin-left: 0; // Undo browser default\n}\n\n// Horizontal description lists\n//\n// Defaults to being stacked without any of the below styles applied, until the\n// grid breakpoint is reached (default of ~768px).\n\n.dl-horizontal {\n dd {\n &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present\n }\n\n @media (min-width: @grid-float-breakpoint) {\n dt {\n float: left;\n width: (@dl-horizontal-offset - 20);\n clear: left;\n text-align: right;\n .text-overflow();\n }\n dd {\n margin-left: @dl-horizontal-offset;\n }\n }\n}\n\n\n// Misc\n// -------------------------\n\n// Abbreviations and acronyms\nabbr[title],\n// Add data-* attribute to help out our tooltip plugin, per https://github.com/twbs/bootstrap/issues/5257\nabbr[data-original-title] {\n cursor: help;\n border-bottom: 1px dotted @abbr-border-color;\n}\n.initialism {\n font-size: 90%;\n text-transform: uppercase;\n}\n\n// Blockquotes\nblockquote {\n padding: (@line-height-computed / 2) @line-height-computed;\n margin: 0 0 @line-height-computed;\n font-size: @blockquote-font-size;\n border-left: 5px solid @blockquote-border-color;\n\n p,\n ul,\n ol {\n &:last-child {\n margin-bottom: 0;\n }\n }\n\n // Note: Deprecated small and .small as of v3.1.0\n // Context: https://github.com/twbs/bootstrap/issues/11660\n footer,\n small,\n .small {\n display: block;\n font-size: 80%; // back to default font-size\n line-height: @line-height-base;\n color: @blockquote-small-color;\n\n &:before {\n content: '\\2014 \\00A0'; // em dash, nbsp\n }\n }\n}\n\n// Opposite alignment of blockquote\n//\n// Heads up: `blockquote.pull-right` has been deprecated as of v3.1.0.\n.blockquote-reverse,\nblockquote.pull-right {\n padding-right: 15px;\n padding-left: 0;\n border-right: 5px solid @blockquote-border-color;\n border-left: 0;\n text-align: right;\n\n // Account for citation\n footer,\n small,\n .small {\n &:before { content: ''; }\n &:after {\n content: '\\00A0 \\2014'; // nbsp, em dash\n }\n }\n}\n\n// Addresses\naddress {\n margin-bottom: @line-height-computed;\n font-style: normal;\n line-height: @line-height-base;\n}\n","// Typography\n\n.text-emphasis-variant(@color) {\n color: @color;\n a&:hover {\n color: darken(@color, 10%);\n }\n}\n","// Contextual backgrounds\n\n.bg-variant(@color) {\n background-color: @color;\n a&:hover {\n background-color: darken(@color, 10%);\n }\n}\n","// Text overflow\n// Requires inline-block or block for proper styling\n\n.text-overflow() {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n","//\n// Code (inline and block)\n// --------------------------------------------------\n\n\n// Inline and block code styles\ncode,\nkbd,\npre,\nsamp {\n font-family: @font-family-monospace;\n}\n\n// Inline code\ncode {\n padding: 2px 4px;\n font-size: 90%;\n color: @code-color;\n background-color: @code-bg;\n border-radius: @border-radius-base;\n}\n\n// User input typically entered via keyboard\nkbd {\n padding: 2px 4px;\n font-size: 90%;\n color: @kbd-color;\n background-color: @kbd-bg;\n border-radius: @border-radius-small;\n box-shadow: inset 0 -1px 0 rgba(0,0,0,.25);\n\n kbd {\n padding: 0;\n font-size: 100%;\n font-weight: bold;\n box-shadow: none;\n }\n}\n\n// Blocks of code\npre {\n display: block;\n padding: ((@line-height-computed - 1) / 2);\n margin: 0 0 (@line-height-computed / 2);\n font-size: (@font-size-base - 1); // 14px to 13px\n line-height: @line-height-base;\n word-break: break-all;\n word-wrap: break-word;\n color: @pre-color;\n background-color: @pre-bg;\n border: 1px solid @pre-border-color;\n border-radius: @border-radius-base;\n\n // Account for some code outputs that place code tags in pre tags\n code {\n padding: 0;\n font-size: inherit;\n color: inherit;\n white-space: pre-wrap;\n background-color: transparent;\n border-radius: 0;\n }\n}\n\n// Enable scrollable blocks of code\n.pre-scrollable {\n max-height: @pre-scrollable-max-height;\n overflow-y: scroll;\n}\n","//\n// Grid system\n// --------------------------------------------------\n\n\n// Container widths\n//\n// Set the container width, and override it for fixed navbars in media queries.\n\n.container {\n .container-fixed();\n\n @media (min-width: @screen-sm-min) {\n width: @container-sm;\n }\n @media (min-width: @screen-md-min) {\n width: @container-md;\n }\n @media (min-width: @screen-lg-min) {\n width: @container-lg;\n }\n}\n\n\n// Fluid container\n//\n// Utilizes the mixin meant for fixed width containers, but without any defined\n// width for fluid, full width layouts.\n\n.container-fluid {\n .container-fixed();\n}\n\n\n// Row\n//\n// Rows contain and clear the floats of your columns.\n\n.row {\n .make-row();\n}\n\n\n// Columns\n//\n// Common styles for small and large grid columns\n\n.make-grid-columns();\n\n\n// Extra small grid\n//\n// Columns, offsets, pushes, and pulls for extra small devices like\n// smartphones.\n\n.make-grid(xs);\n\n\n// Small grid\n//\n// Columns, offsets, pushes, and pulls for the small device range, from phones\n// to tablets.\n\n@media (min-width: @screen-sm-min) {\n .make-grid(sm);\n}\n\n\n// Medium grid\n//\n// Columns, offsets, pushes, and pulls for the desktop device range.\n\n@media (min-width: @screen-md-min) {\n .make-grid(md);\n}\n\n\n// Large grid\n//\n// Columns, offsets, pushes, and pulls for the large desktop device range.\n\n@media (min-width: @screen-lg-min) {\n .make-grid(lg);\n}\n","// Grid system\n//\n// Generate semantic grid columns with these mixins.\n\n// Centered container element\n.container-fixed(@gutter: @grid-gutter-width) {\n margin-right: auto;\n margin-left: auto;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n &:extend(.clearfix all);\n}\n\n// Creates a wrapper for a series of columns\n.make-row(@gutter: @grid-gutter-width) {\n margin-left: (@gutter / -2);\n margin-right: (@gutter / -2);\n &:extend(.clearfix all);\n}\n\n// Generate the extra small columns\n.make-xs-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n float: left;\n width: percentage((@columns / @grid-columns));\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n}\n.make-xs-column-offset(@columns) {\n margin-left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-push(@columns) {\n left: percentage((@columns / @grid-columns));\n}\n.make-xs-column-pull(@columns) {\n right: percentage((@columns / @grid-columns));\n}\n\n// Generate the small columns\n.make-sm-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-sm-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-offset(@columns) {\n @media (min-width: @screen-sm-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-push(@columns) {\n @media (min-width: @screen-sm-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-sm-column-pull(@columns) {\n @media (min-width: @screen-sm-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the medium columns\n.make-md-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-md-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-offset(@columns) {\n @media (min-width: @screen-md-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-push(@columns) {\n @media (min-width: @screen-md-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-md-column-pull(@columns) {\n @media (min-width: @screen-md-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n\n// Generate the large columns\n.make-lg-column(@columns; @gutter: @grid-gutter-width) {\n position: relative;\n min-height: 1px;\n padding-left: (@gutter / 2);\n padding-right: (@gutter / 2);\n\n @media (min-width: @screen-lg-min) {\n float: left;\n width: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-offset(@columns) {\n @media (min-width: @screen-lg-min) {\n margin-left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-push(@columns) {\n @media (min-width: @screen-lg-min) {\n left: percentage((@columns / @grid-columns));\n }\n}\n.make-lg-column-pull(@columns) {\n @media (min-width: @screen-lg-min) {\n right: percentage((@columns / @grid-columns));\n }\n}\n","// Framework grid generation\n//\n// Used only by Bootstrap to generate the correct number of grid classes given\n// any value of `@grid-columns`.\n\n.make-grid-columns() {\n // Common styles for all sizes of grid columns, widths 1-12\n .col(@index) { // initial\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general; \"=<\" isn't a typo\n @item: ~\".col-xs-@{index}, .col-sm-@{index}, .col-md-@{index}, .col-lg-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n position: relative;\n // Prevent columns from collapsing when empty\n min-height: 1px;\n // Inner gutter via padding\n padding-left: (@grid-gutter-width / 2);\n padding-right: (@grid-gutter-width / 2);\n }\n }\n .col(1); // kickstart it\n}\n\n.float-grid-columns(@class) {\n .col(@index) { // initial\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), @item);\n }\n .col(@index, @list) when (@index =< @grid-columns) { // general\n @item: ~\".col-@{class}-@{index}\";\n .col((@index + 1), ~\"@{list}, @{item}\");\n }\n .col(@index, @list) when (@index > @grid-columns) { // terminal\n @{list} {\n float: left;\n }\n }\n .col(1); // kickstart it\n}\n\n.calc-grid-column(@index, @class, @type) when (@type = width) and (@index > 0) {\n .col-@{class}-@{index} {\n width: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index > 0) {\n .col-@{class}-push-@{index} {\n left: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = push) and (@index = 0) {\n .col-@{class}-push-0 {\n left: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index > 0) {\n .col-@{class}-pull-@{index} {\n right: percentage((@index / @grid-columns));\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = pull) and (@index = 0) {\n .col-@{class}-pull-0 {\n right: auto;\n }\n}\n.calc-grid-column(@index, @class, @type) when (@type = offset) {\n .col-@{class}-offset-@{index} {\n margin-left: percentage((@index / @grid-columns));\n }\n}\n\n// Basic looping in LESS\n.loop-grid-columns(@index, @class, @type) when (@index >= 0) {\n .calc-grid-column(@index, @class, @type);\n // next iteration\n .loop-grid-columns((@index - 1), @class, @type);\n}\n\n// Create grid for specific class\n.make-grid(@class) {\n .float-grid-columns(@class);\n .loop-grid-columns(@grid-columns, @class, width);\n .loop-grid-columns(@grid-columns, @class, pull);\n .loop-grid-columns(@grid-columns, @class, push);\n .loop-grid-columns(@grid-columns, @class, offset);\n}\n","//\n// Tables\n// --------------------------------------------------\n\n\ntable {\n background-color: @table-bg;\n}\ncaption {\n padding-top: @table-cell-padding;\n padding-bottom: @table-cell-padding;\n color: @text-muted;\n text-align: left;\n}\nth {\n text-align: left;\n}\n\n\n// Baseline styles\n\n.table {\n width: 100%;\n max-width: 100%;\n margin-bottom: @line-height-computed;\n // Cells\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-cell-padding;\n line-height: @line-height-base;\n vertical-align: top;\n border-top: 1px solid @table-border-color;\n }\n }\n }\n // Bottom align for column headings\n > thead > tr > th {\n vertical-align: bottom;\n border-bottom: 2px solid @table-border-color;\n }\n // Remove top border from thead by default\n > caption + thead,\n > colgroup + thead,\n > thead:first-child {\n > tr:first-child {\n > th,\n > td {\n border-top: 0;\n }\n }\n }\n // Account for multiple tbody instances\n > tbody + tbody {\n border-top: 2px solid @table-border-color;\n }\n\n // Nesting\n .table {\n background-color: @body-bg;\n }\n}\n\n\n// Condensed table w/ half padding\n\n.table-condensed {\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n padding: @table-condensed-cell-padding;\n }\n }\n }\n}\n\n\n// Bordered version\n//\n// Add borders all around the table and between all the columns.\n\n.table-bordered {\n border: 1px solid @table-border-color;\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n border: 1px solid @table-border-color;\n }\n }\n }\n > thead > tr {\n > th,\n > td {\n border-bottom-width: 2px;\n }\n }\n}\n\n\n// Zebra-striping\n//\n// Default zebra-stripe styles (alternating gray and transparent backgrounds)\n\n.table-striped {\n > tbody > tr:nth-child(odd) {\n background-color: @table-bg-accent;\n }\n}\n\n\n// Hover effect\n//\n// Placed here since it has to come after the potential zebra striping\n\n.table-hover {\n > tbody > tr:hover {\n background-color: @table-bg-hover;\n }\n}\n\n\n// Table cell sizing\n//\n// Reset default table behavior\n\ntable col[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623)\n float: none;\n display: table-column;\n}\ntable {\n td,\n th {\n &[class*=\"col-\"] {\n position: static; // Prevent border hiding in Firefox and IE9/10 (see https://github.com/twbs/bootstrap/issues/11623)\n float: none;\n display: table-cell;\n }\n }\n}\n\n\n// Table backgrounds\n//\n// Exact selectors below required to override `.table-striped` and prevent\n// inheritance to nested tables.\n\n// Generate the contextual variants\n.table-row-variant(active; @table-bg-active);\n.table-row-variant(success; @state-success-bg);\n.table-row-variant(info; @state-info-bg);\n.table-row-variant(warning; @state-warning-bg);\n.table-row-variant(danger; @state-danger-bg);\n\n\n// Responsive tables\n//\n// Wrap your tables in `.table-responsive` and we'll make them mobile friendly\n// by enabling horizontal scrolling. Only applies <768px. Everything above that\n// will display normally.\n\n.table-responsive {\n overflow-x: auto;\n min-height: 0.01%; // Workaround for IE9 bug (see https://github.com/twbs/bootstrap/issues/14837)\n\n @media screen and (max-width: @screen-xs-max) {\n width: 100%;\n margin-bottom: (@line-height-computed * 0.75);\n overflow-y: hidden;\n -ms-overflow-style: -ms-autohiding-scrollbar;\n border: 1px solid @table-border-color;\n\n // Tighten up spacing\n > .table {\n margin-bottom: 0;\n\n // Ensure the content doesn't wrap\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th,\n > td {\n white-space: nowrap;\n }\n }\n }\n }\n\n // Special overrides for the bordered tables\n > .table-bordered {\n border: 0;\n\n // Nuke the appropriate borders so that the parent can handle them\n > thead,\n > tbody,\n > tfoot {\n > tr {\n > th:first-child,\n > td:first-child {\n border-left: 0;\n }\n > th:last-child,\n > td:last-child {\n border-right: 0;\n }\n }\n }\n\n // Only nuke the last row's bottom-border in `tbody` and `tfoot` since\n // chances are there will be only one `tr` in a `thead` and that would\n // remove the border altogether.\n > tbody,\n > tfoot {\n > tr:last-child {\n > th,\n > td {\n border-bottom: 0;\n }\n }\n }\n\n }\n }\n}\n","// Tables\n\n.table-row-variant(@state; @background) {\n // Exact selectors below required to override `.table-striped` and prevent\n // inheritance to nested tables.\n .table > thead > tr,\n .table > tbody > tr,\n .table > tfoot > tr {\n > td.@{state},\n > th.@{state},\n &.@{state} > td,\n &.@{state} > th {\n background-color: @background;\n }\n }\n\n // Hover states for `.table-hover`\n // Note: this is not available for cells or rows within `thead` or `tfoot`.\n .table-hover > tbody > tr {\n > td.@{state}:hover,\n > th.@{state}:hover,\n &.@{state}:hover > td,\n &:hover > .@{state},\n &.@{state}:hover > th {\n background-color: darken(@background, 5%);\n }\n }\n}\n","//\n// Forms\n// --------------------------------------------------\n\n\n// Normalize non-controls\n//\n// Restyle and baseline non-control form elements.\n\nfieldset {\n padding: 0;\n margin: 0;\n border: 0;\n // Chrome and Firefox set a `min-width: min-content;` on fieldsets,\n // so we reset that to ensure it behaves more like a standard block element.\n // See https://github.com/twbs/bootstrap/issues/12359.\n min-width: 0;\n}\n\nlegend {\n display: block;\n width: 100%;\n padding: 0;\n margin-bottom: @line-height-computed;\n font-size: (@font-size-base * 1.5);\n line-height: inherit;\n color: @legend-color;\n border: 0;\n border-bottom: 1px solid @legend-border-color;\n}\n\nlabel {\n display: inline-block;\n max-width: 100%; // Force IE8 to wrap long content (see https://github.com/twbs/bootstrap/issues/13141)\n margin-bottom: 5px;\n font-weight: bold;\n}\n\n\n// Normalize form controls\n//\n// While most of our form styles require extra classes, some basic normalization\n// is required to ensure optimum display with or without those classes to better\n// address browser inconsistencies.\n\n// Override content-box in Normalize (* isn't specific enough)\ninput[type=\"search\"] {\n .box-sizing(border-box);\n}\n\n// Position radios and checkboxes better\ninput[type=\"radio\"],\ninput[type=\"checkbox\"] {\n margin: 4px 0 0;\n margin-top: 1px \\9; // IE8-9\n line-height: normal;\n}\n\n// Set the height of file controls to match text inputs\ninput[type=\"file\"] {\n display: block;\n}\n\n// Make range inputs behave like textual form controls\ninput[type=\"range\"] {\n display: block;\n width: 100%;\n}\n\n// Make multiple select elements height not fixed\nselect[multiple],\nselect[size] {\n height: auto;\n}\n\n// Focus for file, radio, and checkbox\ninput[type=\"file\"]:focus,\ninput[type=\"radio\"]:focus,\ninput[type=\"checkbox\"]:focus {\n .tab-focus();\n}\n\n// Adjust output element\noutput {\n display: block;\n padding-top: (@padding-base-vertical + 1);\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @input-color;\n}\n\n\n// Common form controls\n//\n// Shared size and type resets for form controls. Apply `.form-control` to any\n// of the following form controls:\n//\n// select\n// textarea\n// input[type=\"text\"]\n// input[type=\"password\"]\n// input[type=\"datetime\"]\n// input[type=\"datetime-local\"]\n// input[type=\"date\"]\n// input[type=\"month\"]\n// input[type=\"time\"]\n// input[type=\"week\"]\n// input[type=\"number\"]\n// input[type=\"email\"]\n// input[type=\"url\"]\n// input[type=\"search\"]\n// input[type=\"tel\"]\n// input[type=\"color\"]\n\n.form-control {\n display: block;\n width: 100%;\n height: @input-height-base; // Make inputs at least the height of their button counterpart (base line-height + padding + border)\n padding: @padding-base-vertical @padding-base-horizontal;\n font-size: @font-size-base;\n line-height: @line-height-base;\n color: @input-color;\n background-color: @input-bg;\n background-image: none; // Reset unusual Firefox-on-Android default style; see https://github.com/necolas/normalize.css/issues/214\n border: 1px solid @input-border;\n border-radius: @input-border-radius;\n .box-shadow(inset 0 1px 1px rgba(0,0,0,.075));\n .transition(~\"border-color ease-in-out .15s, box-shadow ease-in-out .15s\");\n\n // Customize the `:focus` state to imitate native WebKit styles.\n .form-control-focus();\n\n // Placeholder\n .placeholder();\n\n // Disabled and read-only inputs\n //\n // HTML5 says that controls under a fieldset > legend:first-child won't be\n // disabled if the fieldset is disabled. Due to implementation difficulty, we\n // don't honor that edge case; we style them as disabled anyway.\n &[disabled],\n &[readonly],\n fieldset[disabled] & {\n cursor: @cursor-disabled;\n background-color: @input-bg-disabled;\n opacity: 1; // iOS fix for unreadable disabled content\n }\n\n // Reset height for `textarea`s\n textarea& {\n height: auto;\n }\n}\n\n\n// Search inputs in iOS\n//\n// This overrides the extra rounded corners on search inputs in iOS so that our\n// `.form-control` class can properly style them. Note that this cannot simply\n// be added to `.form-control` as it's not specific enough. For details, see\n// https://github.com/twbs/bootstrap/issues/11586.\n\ninput[type=\"search\"] {\n -webkit-appearance: none;\n}\n\n\n// Special styles for iOS temporal inputs\n//\n// In Mobile Safari, setting `display: block` on temporal inputs causes the\n// text within the input to become vertically misaligned. As a workaround, we\n// set a pixel line-height that matches the given height of the input, but only\n// for Safari.\n\n@media screen and (-webkit-min-device-pixel-ratio: 0) {\n input[type=\"date\"],\n input[type=\"time\"],\n input[type=\"datetime-local\"],\n input[type=\"month\"] {\n line-height: @input-height-base;\n }\n input[type=\"date\"].input-sm,\n input[type=\"time\"].input-sm,\n input[type=\"datetime-local\"].input-sm,\n input[type=\"month\"].input-sm {\n line-height: @input-height-small;\n }\n input[type=\"date\"].input-lg,\n input[type=\"time\"].input-lg,\n input[type=\"datetime-local\"].input-lg,\n input[type=\"month\"].input-lg {\n line-height: @input-height-large;\n }\n}\n\n\n// Form groups\n//\n// Designed to help with the organization and spacing of vertical forms. For\n// horizontal forms, use the predefined grid classes.\n\n.form-group {\n margin-bottom: 15px;\n}\n\n\n// Checkboxes and radios\n//\n// Indent the labels to position radios/checkboxes as hanging controls.\n\n.radio,\n.checkbox {\n position: relative;\n display: block;\n margin-top: 10px;\n margin-bottom: 10px;\n\n label {\n min-height: @line-height-computed; // Ensure the input doesn't jump when there is no text\n padding-left: 20px;\n margin-bottom: 0;\n font-weight: normal;\n cursor: pointer;\n }\n}\n.radio input[type=\"radio\"],\n.radio-inline input[type=\"radio\"],\n.checkbox input[type=\"checkbox\"],\n.checkbox-inline input[type=\"checkbox\"] {\n position: absolute;\n margin-left: -20px;\n margin-top: 4px \\9;\n}\n\n.radio + .radio,\n.checkbox + .checkbox {\n margin-top: -5px; // Move up sibling radios or checkboxes for tighter spacing\n}\n\n// Radios and checkboxes on same line\n.radio-inline,\n.checkbox-inline {\n display: inline-block;\n padding-left: 20px;\n margin-bottom: 0;\n vertical-align: middle;\n font-weight: normal;\n cursor: pointer;\n}\n.radio-inline + .radio-inline,\n.checkbox-inline + .checkbox-inline {\n margin-top: 0;\n margin-left: 10px; // space out consecutive inline controls\n}\n\n// Apply same disabled cursor tweak as for inputs\n// Some special care is needed because
      ').appendTo(t.getContainerElm())),o.setTimeout(function(){n.addClass(r+"in"),i(t.getEl()).addClass(r+"in")}),b=!0),f(!0,t)}}),t.on("show",function(){t.parents().each(function(e){if(e.state.get("fixed"))return t.fixed(!0),!1})}),e.popover&&(t._preBodyHtml='
      ',t.classes.add("popover").add("bottom").add(t.isRtl()?"end":"start")),t.aria("label",e.ariaLabel),t.aria("labelledby",t._id),t.aria("describedby",t.describedBy||t._id+"-none")},fixed:function(e){var t=this;if(t.state.get("fixed")!=e){if(t.state.get("rendered")){var n=r.getViewPort();e?t.layoutRect().y-=n.y:t.layoutRect().y+=n.y}t.classes.toggle("fixed",e),t.state.set("fixed",e)}return t},show:function(){var e=this,t,n=e._super();for(t=v.length;t--&&v[t]!==e;);return t===-1&&v.push(e),n},hide:function(){return p(this),f(!1,this),this._super()},hideAll:function(){C.hideAll()},close:function(){var e=this;return e.fire("close").isDefaultPrevented()||(e.remove(),f(!1,e)),e},remove:function(){p(this),this._super()},postRender:function(){var e=this;return e.settings.bodyRole&&this.getEl("body").setAttribute("role",e.settings.bodyRole),e._super()}});return C.hideAll=function(){for(var e=v.length;e--;){var t=v[e];t&&t.settings.autohide&&(t.hide(),v.splice(e,1))}},C}),r(Be,[Ae,ke,ve,g,_e,ye,d,c],function(e,t,n,r,i,o,a,s){function l(e){var t="width=device-width,initial-scale=1.0,user-scalable=0,minimum-scale=1.0,maximum-scale=1.0",n=r("meta[name=viewport]")[0],i;a.overrideViewPort!==!1&&(n||(n=document.createElement("meta"),n.setAttribute("name","viewport"),document.getElementsByTagName("head")[0].appendChild(n)),i=n.getAttribute("content"),i&&"undefined"!=typeof p&&(p=i),n.setAttribute("content",e?t:p))}function u(e,t){c()&&t===!1&&r([document.documentElement,document.body]).removeClass(e+"fullscreen")}function c(){for(var e=0;er.w&&(o=r.x-Math.max(0,i/2),e.layoutRect({w:i,x:o}),a=!0)),t&&(t.layoutRect({w:e.layoutRect().innerW}).recalc(),i=t.layoutRect().minW+r.deltaW,i>r.w&&(o=r.x-Math.max(0,i-r.w),e.layoutRect({w:i,x:o}),a=!0)),a&&e.recalc()},initLayoutRect:function(){var e=this,t=e._super(),r=0,i;if(e.settings.title&&!e._fullscreen){i=e.getEl("head");var o=n.getSize(i);t.headerW=o.width,t.headerH=o.height,r+=t.headerH}e.statusbar&&(r+=e.statusbar.layoutRect().h),t.deltaH+=r,t.minH+=r,t.h+=r;var a=n.getWindowSize();return t.x=e.settings.x||Math.max(0,a.w/2-t.w/2),t.y=e.settings.y||Math.max(0,a.h/2-t.h/2),t},renderHtml:function(){var e=this,t=e._layout,n=e._id,r=e.classPrefix,i=e.settings,o="",a="",s=i.html;return e.preRender(),t.preRender(e),i.title&&(o='
      '+e.encode(i.title)+'
      '),i.url&&(s=''),"undefined"==typeof s&&(s=t.renderHtml(e)),e.statusbar&&(a=e.statusbar.renderHtml()),'
      '+o+'
      '+s+"
      "+a+"
      "},fullscreen:function(e){var t=this,i=document.documentElement,a,l=t.classPrefix,u;if(e!=t._fullscreen)if(r(window).on("resize",function(){var e;if(t._fullscreen)if(a)t._timer||(t._timer=s.setTimeout(function(){var e=n.getWindowSize();t.moveTo(0,0).resizeTo(e.w,e.h),t._timer=0},50));else{e=(new Date).getTime();var r=n.getWindowSize();t.moveTo(0,0).resizeTo(r.w,r.h),(new Date).getTime()-e>50&&(a=!0)}}),u=t.layoutRect(),t._fullscreen=e,e){t._initial={x:u.x,y:u.y,w:u.w,h:u.h},t.borderBox=o.parseBox("0"),t.getEl("head").style.display="none",u.deltaH-=u.headerH+2,r([i,document.body]).addClass(l+"fullscreen"),t.classes.add("fullscreen");var c=n.getWindowSize();t.moveTo(0,0).resizeTo(c.w,c.h)}else t.borderBox=o.parseBox(t.settings.border),t.getEl("head").style.display="",u.deltaH+=u.headerH,r([i,document.body]).removeClass(l+"fullscreen"),t.classes.remove("fullscreen"),t.moveTo(t._initial.x,t._initial.y).resizeTo(t._initial.w,t._initial.h);return t.reflow()},postRender:function(){var e=this,t;setTimeout(function(){e.classes.add("in"),e.fire("open")},0),e._super(),e.statusbar&&e.statusbar.postRender(),e.focus(),this.dragHelper=new i(e._id+"-dragh",{start:function(){t={x:e.layoutRect().x,y:e.layoutRect().y}},drag:function(n){e.moveTo(t.x+n.deltaX,t.y+n.deltaY)}}),e.on("submit",function(t){t.isDefaultPrevented()||e.close()}),f.push(e),l(!0)},submit:function(){return this.fire("submit",{data:this.toJSON()})},remove:function(){var e=this,t;for(e.dragHelper.destroy(),e._super(),e.statusbar&&this.statusbar.remove(),u(e.classPrefix,!1),t=f.length;t--;)f[t]===e&&f.splice(t,1);l(f.length>0)},getContentWindow:function(){var e=this.getEl().getElementsByTagName("iframe")[0];return e?e.contentWindow:null}});return d(),h}),r(De,[Be],function(e){var t=e.extend({init:function(e){e={border:1,padding:20,layout:"flex",pack:"center",align:"center",containerCls:"panel",autoScroll:!0,buttons:{type:"button",text:"Ok",action:"ok"},items:{type:"label",multiline:!0,maxWidth:500,maxHeight:200}},this._super(e)},Statics:{OK:1,OK_CANCEL:2,YES_NO:3,YES_NO_CANCEL:4,msgBox:function(n){function r(e,t,n){return{type:"button",text:e,subtype:n?"primary":"",onClick:function(e){e.control.parents()[1].close(),o(t)}}}var i,o=n.callback||function(){};switch(n.buttons){case t.OK_CANCEL:i=[r("Ok",!0,!0),r("Cancel",!1)];break;case t.YES_NO:case t.YES_NO_CANCEL:i=[r("Yes",1,!0),r("No",0)],n.buttons==t.YES_NO_CANCEL&&i.push(r("Cancel",-1));break;default:i=[r("Ok",!0,!0)]}return new e({padding:20,x:n.x,y:n.y,minWidth:300,minHeight:100,layout:"flex",pack:"center",align:"center",buttons:i,title:n.title,role:"alertdialog",items:{type:"label",multiline:!0,maxWidth:500,maxHeight:200,text:n.text},onPostRender:function(){this.aria("describedby",this.items()[0]._id)},onClose:n.onClose,onCancel:function(){o(!1)}}).renderTo(document.body).reflow()},alert:function(e,n){return"string"==typeof e&&(e={text:e}),e.callback=n,t.msgBox(e)},confirm:function(e,n){return"string"==typeof e&&(e={text:e}),e.callback=n,e.buttons=t.OK_CANCEL,t.msgBox(e)}}});return t}),r(Le,[Be,De],function(e,t){return function(n){function r(){if(s.length)return s[s.length-1]}function i(e){n.fire("OpenWindow",{win:e})}function o(e){n.fire("CloseWindow",{win:e})}var a=this,s=[];a.windows=s,n.on("remove",function(){for(var e=s.length;e--;)s[e].close()}),a.open=function(t,r){var a;return n.editorManager.setActive(n),t.title=t.title||" ",t.url=t.url||t.file,t.url&&(t.width=parseInt(t.width||320,10),t.height=parseInt(t.height||240,10)),t.body&&(t.items={defaults:t.defaults,type:t.bodyType||"form",items:t.body,data:t.data,callbacks:t.commands}),t.url||t.buttons||(t.buttons=[{text:"Ok",subtype:"primary",onclick:function(){a.find("form")[0].submit(); +}},{text:"Cancel",onclick:function(){a.close()}}]),a=new e(t),s.push(a),a.on("close",function(){for(var e=s.length;e--;)s[e]===a&&s.splice(e,1);s.length||n.focus(),o(a)}),t.data&&a.on("postRender",function(){this.find("*").each(function(e){var n=e.name();n in t.data&&e.value(t.data[n])})}),a.features=t||{},a.params=r||{},1===s.length&&n.nodeChanged(),a=a.renderTo().reflow(),i(a),a},a.alert=function(e,r,a){var s;s=t.alert(e,function(){r?r.call(a||this):n.focus()}),s.on("close",function(){o(s)}),i(s)},a.confirm=function(e,n,r){var a;a=t.confirm(e,function(e){n.call(r||this,e)}),a.on("close",function(){o(a)}),i(a)},a.close=function(){r()&&r().close()},a.getParams=function(){return r()?r().params:null},a.setParams=function(e){r()&&(r().params=e)},a.getWindows=function(){return s}}}),r(Me,[xe,Te],function(e,t){return e.extend({Mixins:[t],Defaults:{classes:"widget tooltip tooltip-n"},renderHtml:function(){var e=this,t=e.classPrefix;return'"},bindStates:function(){var e=this;return e.state.on("change:text",function(t){e.getEl().lastChild.innerHTML=e.encode(t.value)}),e._super()},repaint:function(){var e=this,t,n;t=e.getEl().style,n=e._layoutRect,t.left=n.x+"px",t.top=n.y+"px",t.zIndex=131070}})}),r(Pe,[xe,Me],function(e,t){var n,r=e.extend({init:function(e){var t=this;t._super(e),e=t.settings,t.canFocus=!0,e.tooltip&&r.tooltips!==!1&&(t.on("mouseenter",function(n){var r=t.tooltip().moveTo(-65535);if(n.control==t){var i=r.text(e.tooltip).show().testMoveRel(t.getEl(),["bc-tc","bc-tl","bc-tr"]);r.classes.toggle("tooltip-n","bc-tc"==i),r.classes.toggle("tooltip-nw","bc-tl"==i),r.classes.toggle("tooltip-ne","bc-tr"==i),r.moveRel(t.getEl(),i)}else r.hide()}),t.on("mouseleave mousedown click",function(){t.tooltip().hide()})),t.aria("label",e.ariaLabel||e.tooltip)},tooltip:function(){return n||(n=new t({type:"tooltip"}),n.renderTo()),n},postRender:function(){var e=this,t=e.settings;e._super(),e.parent()||!t.width&&!t.height||(e.initLayoutRect(),e.repaint()),t.autofocus&&e.focus()},bindStates:function(){function e(e){n.aria("disabled",e),n.classes.toggle("disabled",e)}function t(e){n.aria("pressed",e),n.classes.toggle("active",e)}var n=this;return n.state.on("change:disabled",function(t){e(t.value)}),n.state.on("change:active",function(e){t(e.value)}),n.state.get("disabled")&&e(!0),n.state.get("active")&&t(!0),n._super()},remove:function(){this._super(),n&&(n.remove(),n=null)}});return r}),r(Oe,[Pe],function(e){return e.extend({Defaults:{value:0},init:function(e){var t=this;t._super(e),t.classes.add("progress"),t.settings.filter||(t.settings.filter=function(e){return Math.round(e)})},renderHtml:function(){var e=this,t=e._id,n=this.classPrefix;return'
      0%
      '},postRender:function(){var e=this;return e._super(),e.value(e.settings.value),e},bindStates:function(){function e(e){e=t.settings.filter(e),t.getEl().lastChild.innerHTML=e+"%",t.getEl().firstChild.firstChild.style.width=e+"%"}var t=this;return t.state.on("change:value",function(t){e(t.value)}),e(t.state.get("value")),t._super()}})}),r(He,[xe,Te,Oe,c],function(e,t,n,r){return e.extend({Mixins:[t],Defaults:{classes:"widget notification"},init:function(e){var t=this;t._super(e),e.text&&t.text(e.text),e.icon&&(t.icon=e.icon),e.color&&(t.color=e.color),e.type&&t.classes.add("notification-"+e.type),e.timeout&&(e.timeout<0||e.timeout>0)&&!e.closeButton?t.closeButton=!1:(t.classes.add("has-close"),t.closeButton=!0),e.progressBar&&(t.progressBar=new n),t.on("click",function(e){e.target.className.indexOf(t.classPrefix+"close")!=-1&&t.close()})},renderHtml:function(){var e=this,t=e.classPrefix,n="",r="",i="",o="";return e.icon&&(n=''),e.color&&(o=' style="background-color: '+e.color+'"'),e.closeButton&&(r=''),e.progressBar&&(i=e.progressBar.renderHtml()),'"},postRender:function(){var e=this;return r.setTimeout(function(){e.$el.addClass(e.classPrefix+"in")}),e._super()},bindStates:function(){var e=this;return e.state.on("change:text",function(t){e.getEl().childNodes[1].innerHTML=t.value}),e.progressBar&&e.progressBar.bindStates(),e._super()},close:function(){var e=this;return e.fire("close").isDefaultPrevented()||e.remove(),e},repaint:function(){var e=this,t,n;t=e.getEl().style,n=e._layoutRect,t.left=n.x+"px",t.top=n.y+"px",t.zIndex=65534}})}),r(Ie,[He,c,m],function(e,t,n){return function(r){function i(){if(f.length)return f[f.length-1]}function o(){t.requestAnimationFrame(function(){a(),s()})}function a(){for(var e=0;e0){var e=f.slice(0,1)[0],t=r.inline?r.getElement():r.getContentAreaContainer();if(e.moveRel(t,"tc-tc"),f.length>1)for(var n=1;n0&&(n.timer=setTimeout(function(){n.close()},t.timeout)),n.on("close",function(){var e=f.length;for(n.timer&&r.getWin().clearTimeout(n.timer);e--;)f[e]===n&&f.splice(e,1);s()}),n.renderTo(),s()):n=i,n}},d.close=function(){i()&&i().close()},d.getNotifications=function(){return f},r.on("SkinLoaded",function(){var e=r.settings.service_message;e&&r.notificationManager.open({text:e,type:"warning",timeout:0,icon:""})})}}),r(Fe,[w],function(e){function t(t,n,r){for(var i=[];n&&n!=t;n=n.parentNode)i.push(e.nodeIndex(n,r));return i}function n(e,t){var n,r,i;for(r=e,n=t.length-1;n>=0;n--){if(i=r.childNodes,t[n]>i.length-1)return null;r=i[t[n]]}return r}return{create:t,resolve:n}}),r(ze,[I,T,y,Fe,A,C,d,m,c,k,$,oe],function(e,t,n,r,i,o,a,s,l,u,c,d){return function(f){function p(e,t){try{f.getDoc().execCommand(e,!1,t)}catch(n){}}function h(){var e=f.getDoc().documentMode;return e?e:6}function m(e){return e.isDefaultPrevented()}function g(e){var t,n;e.dataTransfer&&(f.selection.isCollapsed()&&"IMG"==e.target.tagName&&re.select(e.target),t=f.selection.getContent(),t.length>0&&(n=ce+escape(f.id)+","+escape(t),e.dataTransfer.setData(de,n)))}function v(e){var t;return e.dataTransfer&&(t=e.dataTransfer.getData(de),t&&t.indexOf(ce)>=0)?(t=t.substr(ce.length).split(","),{id:unescape(t[0]),html:unescape(t[1])}):null}function y(e){f.queryCommandSupported("mceInsertClipboardContent")?f.execCommand("mceInsertClipboardContent",!1,{content:e}):f.execCommand("mceInsertContent",!1,e)}function b(){function i(e){var t=x.schema.getBlockElements(),n=f.getBody();if("BR"!=e.nodeName)return!1;for(;e!=n&&!t[e.nodeName];e=e.parentNode)if(e.nextSibling)return!1;return!0}function o(e,t){var n;for(n=e.nextSibling;n&&n!=t;n=n.nextSibling)if((3!=n.nodeType||0!==Z.trim(n.data).length)&&n!==t)return!1;return n===t}function a(e,t,r){var o,a,s;if(x.isChildOf(e,f.getBody()))for(s=x.schema.getNonEmptyElements(),o=new n(r||e,e);a=o[t?"next":"prev"]();){if(s[a.nodeName]&&!i(a))return a;if(3==a.nodeType&&a.data.length>0)return a}}function u(e){var n,r,i,o,s;if(!e.collapsed&&(n=x.getParent(t.getNode(e.startContainer,e.startOffset),x.isBlock),r=x.getParent(t.getNode(e.endContainer,e.endOffset),x.isBlock),s=f.schema.getTextBlockElements(),n!=r&&s[n.nodeName]&&s[r.nodeName]&&"false"!==x.getContentEditable(n)&&"false"!==x.getContentEditable(r)))return e.deleteContents(),i=a(n,!1),o=a(r,!0),x.isEmpty(r)||Z(n).append(r.childNodes),Z(r).remove(),i?1==i.nodeType?"BR"==i.nodeName?(e.setStartBefore(i),e.setEndBefore(i)):(e.setStartAfter(i),e.setEndAfter(i)):(e.setStart(i,i.data.length),e.setEnd(i,i.data.length)):o&&(1==o.nodeType?(e.setStartBefore(o),e.setEndBefore(o)):(e.setStart(o,0),e.setEnd(o,0))),w.setRng(e),!0}function c(e,n){var r,i,s,l,u,c;if(!e.collapsed)return e;if(u=e.startContainer,c=e.startOffset,3==u.nodeType)if(n){if(c0)return e;r=t.getNode(u,c),s=x.getParent(r,x.isBlock),i=a(f.getBody(),n,r),l=x.getParent(i,x.isBlock);var d=1===u.nodeType&&c>u.childNodes.length-1;if(!r||!i)return e;if(l&&s!=l)if(n){if(!o(s,l))return e;1==r.nodeType?"BR"==r.nodeName?e.setStartBefore(r):e.setStartAfter(r):e.setStart(r,r.data.length),1==i.nodeType?e.setEnd(i,0):e.setEndBefore(i)}else{if(!o(l,s))return e;1==i.nodeType?"BR"==i.nodeName?e.setStartBefore(i):e.setStartAfter(i):e.setStart(i,i.data.length),1==r.nodeType&&d?e.setEndAfter(r):e.setEndBefore(r)}return e}function d(e){var t=w.getRng();if(t=c(t,e),u(t))return!0}function p(e,t){function n(e,n){return m=Z(n).parents().filter(function(e,t){return!!f.schema.getTextInlineElements()[t.nodeName]}),l=e.cloneNode(!1),m=s.map(m,function(e){return e=e.cloneNode(!1),l.hasChildNodes()?(e.appendChild(l.firstChild),l.appendChild(e)):l.appendChild(e),l.appendChild(e),e}),m.length?(h=x.create("br"),m[0].appendChild(h),x.replace(l,e),t.setStartBefore(h),t.setEndBefore(h),f.selection.setRng(t),h):null}function i(e){return e&&f.schema.getTextBlockElements()[e.tagName]}var o,a,l,u,c,d,p,h,m;if(t.collapsed&&(d=t.startContainer,p=t.startOffset,a=x.getParent(d,x.isBlock),i(a)))if(1==d.nodeType){if(d=d.childNodes[p],d&&"BR"!=d.tagName)return;if(c=e?a.nextSibling:a.previousSibling,x.isEmpty(a)&&i(c)&&x.isEmpty(c)&&n(a,d))return x.remove(c),!0}else if(3==d.nodeType){if(o=r.create(a,d),u=a.cloneNode(!0),d=r.resolve(u,o),e){if(p>=d.data.length)return;d.deleteData(p,1)}else{if(p<=0)return;d.deleteData(p-1,1)}if(x.isEmpty(u))return n(a,d)}}function h(e){var t,n,r;d(e)||(s.each(f.getBody().getElementsByTagName("*"),function(e){"SPAN"==e.tagName&&e.setAttribute("mce-data-marked",1),!e.hasAttribute("data-mce-style")&&e.hasAttribute("style")&&f.dom.setAttrib(e,"style",f.dom.getAttrib(e,"style"))}),t=new E(function(){}),t.observe(f.getDoc(),{childList:!0,attributes:!0,subtree:!0,attributeFilter:["style"]}),f.getDoc().execCommand(e?"ForwardDelete":"Delete",!1,null),n=f.selection.getRng(),r=n.startContainer.parentNode,s.each(t.takeRecords(),function(e){if(x.isChildOf(e.target,f.getBody())){if("style"==e.attributeName){var t=e.target.getAttribute("data-mce-style");t?e.target.setAttribute("style",t):e.target.removeAttribute("style")}s.each(e.addedNodes,function(e){if("SPAN"==e.nodeName&&!e.getAttribute("mce-data-marked")){var t,i;e==r&&(t=n.startOffset,i=e.firstChild),x.remove(e,!0),i&&(n.setStart(i,t),n.setEnd(i,t),f.selection.setRng(n))}})}}),t.disconnect(),s.each(f.dom.select("span[mce-data-marked]"),function(e){e.removeAttribute("mce-data-marked")}))}function b(e){f.undoManager.transact(function(){h(e)})}var C=f.getDoc(),x=f.dom,w=f.selection,E=window.MutationObserver,N,_;E||(N=!0,E=function(){function e(e){var t=e.relatedNode||e.target;n.push({target:t,addedNodes:[t]})}function t(e){var t=e.relatedNode||e.target;n.push({target:t,attributeName:e.attrName})}var n=[],r;this.observe=function(n){r=n,r.addEventListener("DOMSubtreeModified",e,!1),r.addEventListener("DOMNodeInsertedIntoDocument",e,!1),r.addEventListener("DOMNodeInserted",e,!1),r.addEventListener("DOMAttrModified",t,!1)},this.disconnect=function(){r.removeEventListener("DOMSubtreeModified",e,!1),r.removeEventListener("DOMNodeInsertedIntoDocument",e,!1),r.removeEventListener("DOMNodeInserted",e,!1),r.removeEventListener("DOMAttrModified",t,!1)},this.takeRecords=function(){return n}}),f.on("keydown",function(e){var t=e.keyCode==te,n=e.ctrlKey||e.metaKey;if(!m(e)&&(t||e.keyCode==ee)){var r=f.selection.getRng(),i=r.startContainer,o=r.startOffset;if(t&&e.shiftKey)return;if(p(t,r))return void e.preventDefault();if(!n&&r.collapsed&&3==i.nodeType&&(t?o0))return;e.preventDefault(),n&&f.selection.getSel().modify("extend",t?"forward":"backward",e.metaKey?"lineboundary":"word"),h(t)}}),f.on("keypress",function(t){if(!m(t)&&!w.isCollapsed()&&t.charCode>31&&!e.metaKeyPressed(t)){var n,r,i,o,a,s;n=f.selection.getRng(),s=String.fromCharCode(t.charCode),t.preventDefault(),r=Z(n.startContainer).parents().filter(function(e,t){return!!f.schema.getTextInlineElements()[t.nodeName]}),h(!0),r=r.filter(function(e,t){return!Z.contains(f.getBody(),t)}),r.length?(i=x.createFragment(),r.each(function(e,t){t=t.cloneNode(!1),i.hasChildNodes()?(t.appendChild(i.firstChild),i.appendChild(t)):(a=t,i.appendChild(t)),i.appendChild(t)}),a.appendChild(f.getDoc().createTextNode(s)),o=x.getParent(n.startContainer,x.isBlock),x.isEmpty(o)?Z(o).empty().append(i):n.insertNode(i),n.setStart(a.firstChild,1),n.setEnd(a.firstChild,1),f.selection.setRng(n)):f.selection.setContent(s)}}),f.addCommand("Delete",function(){h()}),f.addCommand("ForwardDelete",function(){h(!0)}),N||(f.on("dragstart",function(e){_=w.getRng(),g(e)}),f.on("drop",function(e){if(!m(e)){var n=v(e);n&&(e.preventDefault(),l.setEditorTimeout(f,function(){var r=t.getCaretRangeFromPoint(e.x,e.y,C);_&&(w.setRng(_),_=null,b()),w.setRng(r),y(n.html)}))}}),f.on("cut",function(e){m(e)||!e.clipboardData||f.selection.isCollapsed()||(e.preventDefault(),e.clipboardData.clearData(),e.clipboardData.setData("text/html",f.selection.getContent()),e.clipboardData.setData("text/plain",f.selection.getContent({format:"text"})),l.setEditorTimeout(f,function(){b(!0)}))}))}function C(){function e(e){var t=ne.create("body"),n=e.cloneContents();return t.appendChild(n),re.serializer.serialize(t,{format:"html"})}function n(n){if(!n.setStart){if(n.item)return!1;var r=n.duplicate();return r.moveToElementText(f.getBody()),t.compareRanges(n,r)}var i=e(n),o=ne.createRng();o.selectNode(f.getBody());var a=e(o);return i===a}f.on("keydown",function(e){var t=e.keyCode,r,i;if(!m(e)&&(t==te||t==ee)){if(r=f.selection.isCollapsed(),i=f.getBody(),r&&!ne.isEmpty(i))return;if(!r&&!n(f.selection.getRng()))return;e.preventDefault(),f.setContent(""),i.firstChild&&ne.isBlock(i.firstChild)?f.selection.setCursorLocation(i.firstChild,0):f.selection.setCursorLocation(i,0),f.nodeChanged()}})}function x(){f.shortcuts.add("meta+a",null,"SelectAll")}function w(){f.settings.content_editable||ne.bind(f.getDoc(),"mousedown mouseup",function(e){var t;if(e.target==f.getDoc().documentElement)if(t=re.getRng(),f.getBody().focus(),"mousedown"==e.type){if(u.isCaretContainer(t.startContainer))return;re.placeCaretAt(e.clientX,e.clientY)}else re.setRng(t)})}function E(){f.on("keydown",function(e){if(!m(e)&&e.keyCode===ee){if(!f.getBody().getElementsByTagName("hr").length)return;if(re.isCollapsed()&&0===re.getRng(!0).startOffset){var t=re.getNode(),n=t.previousSibling;if("HR"==t.nodeName)return ne.remove(t),void e.preventDefault();n&&n.nodeName&&"hr"===n.nodeName.toLowerCase()&&(ne.remove(n),e.preventDefault())}}})}function N(){window.Range.prototype.getClientRects||f.on("mousedown",function(e){if(!m(e)&&"HTML"===e.target.nodeName){var t=f.getBody();t.blur(),l.setEditorTimeout(f,function(){t.focus()})}})}function _(){f.on("click",function(e){var t=e.target;/^(IMG|HR)$/.test(t.nodeName)&&"false"!==ne.getContentEditableParent(t)&&(e.preventDefault(),re.getSel().setBaseAndExtent(t,0,t,1),f.nodeChanged()),"A"==t.nodeName&&ne.hasClass(t,"mce-item-anchor")&&(e.preventDefault(),re.select(t))})}function S(){function e(){var e=ne.getAttribs(re.getStart().cloneNode(!1));return function(){var t=re.getStart();t!==f.getBody()&&(ne.setAttrib(t,"style",null),Q(e,function(e){t.setAttributeNode(e.cloneNode(!0))}))}}function t(){return!re.isCollapsed()&&ne.getParent(re.getStart(),ne.isBlock)!=ne.getParent(re.getEnd(),ne.isBlock)}f.on("keypress",function(n){var r;if(!m(n)&&(8==n.keyCode||46==n.keyCode)&&t())return r=e(),f.getDoc().execCommand("delete",!1,null),r(),n.preventDefault(),!1}),ne.bind(f.getDoc(),"cut",function(n){var r;!m(n)&&t()&&(r=e(),l.setEditorTimeout(f,function(){r()}))})}function k(){document.body.setAttribute("role","application")}function T(){f.on("keydown",function(e){if(!m(e)&&e.keyCode===ee&&re.isCollapsed()&&0===re.getRng(!0).startOffset){var t=re.getNode().previousSibling;if(t&&t.nodeName&&"table"===t.nodeName.toLowerCase())return e.preventDefault(),!1}})}function R(){h()>7||(p("RespectVisibilityInDesign",!0),f.contentStyles.push(".mceHideBrInPre pre br {display: none}"),ne.addClass(f.getBody(),"mceHideBrInPre"),oe.addNodeFilter("pre",function(e){for(var t=e.length,n,r,o,a;t--;)for(n=e[t].getAll("br"),r=n.length;r--;)o=n[r],a=o.prev,a&&3===a.type&&"\n"!=a.value.charAt(a.value-1)?a.value+="\n":o.parent.insert(new i("#text",3),o,!0).value="\n"}),ae.addNodeFilter("pre",function(e){for(var t=e.length,n,r,i,o;t--;)for(n=e[t].getAll("br"),r=n.length;r--;)i=n[r],o=i.prev,o&&3==o.type&&(o.value=o.value.replace(/\r?\n$/,""))}))}function A(){ne.bind(f.getBody(),"mouseup",function(){var e,t=re.getNode();"IMG"==t.nodeName&&((e=ne.getStyle(t,"width"))&&(ne.setAttrib(t,"width",e.replace(/[^0-9%]+/g,"")),ne.setStyle(t,"width","")),(e=ne.getStyle(t,"height"))&&(ne.setAttrib(t,"height",e.replace(/[^0-9%]+/g,"")),ne.setStyle(t,"height","")))})}function B(){f.on("keydown",function(t){var n,r,i,o,a;if(!m(t)&&t.keyCode==e.BACKSPACE&&(n=re.getRng(),r=n.startContainer,i=n.startOffset,o=ne.getRoot(),a=r,n.collapsed&&0===i)){for(;a&&a.parentNode&&a.parentNode.firstChild==a&&a.parentNode!=o;)a=a.parentNode;"BLOCKQUOTE"===a.tagName&&(f.formatter.toggle("blockquote",null,a),n=ne.createRng(),n.setStart(r,0),n.setEnd(r,0),re.setRng(n))}})}function D(){function e(){K(),p("StyleWithCSS",!1),p("enableInlineTableEditing",!1),ie.object_resizing||p("enableObjectResizing",!1)}ie.readonly||f.on("BeforeExecCommand MouseDown",e)}function L(){function e(){Q(ne.select("a"),function(e){var t=e.parentNode,n=ne.getRoot();if(t.lastChild===e){for(;t&&!ne.isBlock(t);){if(t.parentNode.lastChild!==t||t===n)return;t=t.parentNode}ne.add(t,"br",{"data-mce-bogus":1})}})}f.on("SetContent ExecCommand",function(t){"setcontent"!=t.type&&"mceInsertLink"!==t.command||e()})}function M(){ie.forced_root_block&&f.on("init",function(){p("DefaultParagraphSeparator",ie.forced_root_block)})}function P(){f.on("keydown",function(e){var t;m(e)||e.keyCode!=ee||(t=f.getDoc().selection.createRange(),t&&t.item&&(e.preventDefault(),f.undoManager.beforeChange(),ne.remove(t.item(0)),f.undoManager.add()))})}function O(){var e;h()>=10&&(e="",Q("p div h1 h2 h3 h4 h5 h6".split(" "),function(t,n){e+=(n>0?",":"")+t+":empty"}),f.contentStyles.push(e+"{padding-right: 1px !important}"))}function H(){h()<9&&(oe.addNodeFilter("noscript",function(e){for(var t=e.length,n,r;t--;)n=e[t],r=n.firstChild,r&&n.attr("data-mce-innertext",r.value)}),ae.addNodeFilter("noscript",function(e){for(var t=e.length,n,r,a;t--;)n=e[t],r=e[t].firstChild,r?r.value=o.decode(r.value):(a=n.attributes.map["data-mce-innertext"],a&&(n.attr("data-mce-innertext",null),r=new i("#text",3),r.value=a,r.raw=!0,n.append(r)))}))}function I(){function e(e,t){var n=i.createTextRange();try{n.moveToPoint(e,t)}catch(r){n=null}return n}function t(t){var r;t.button?(r=e(t.x,t.y),r&&(r.compareEndPoints("StartToStart",a)>0?r.setEndPoint("StartToStart",a):r.setEndPoint("EndToEnd",a),r.select())):n()}function n(){var e=r.selection.createRange();a&&!e.item&&0===e.compareEndPoints("StartToEnd",e)&&a.select(),ne.unbind(r,"mouseup",n),ne.unbind(r,"mousemove",t),a=o=0}var r=ne.doc,i=r.body,o,a,s;r.documentElement.unselectable=!0,ne.bind(r,"mousedown contextmenu",function(i){if("HTML"===i.target.nodeName){if(o&&n(),s=r.documentElement,s.scrollHeight>s.clientHeight)return;o=1,a=e(i.x,i.y),a&&(ne.bind(r,"mouseup",n),ne.bind(r,"mousemove",t),ne.getRoot().focus(),a.select())}})}function F(){f.on("keyup focusin mouseup",function(t){65==t.keyCode&&e.metaKeyPressed(t)||re.normalize()},!0)}function z(){f.contentStyles.push("img:-moz-broken {-moz-force-broken-image-icon:1;min-width:24px;min-height:24px}")}function U(){f.inline||f.on("keydown",function(){document.activeElement==document.body&&f.getWin().focus()})}function W(){f.inline||(f.contentStyles.push("body {min-height: 150px}"),f.on("click",function(e){var t;if("HTML"==e.target.nodeName){if(a.ie>11)return void f.getBody().focus();t=f.selection.getRng(),f.getBody().focus(),f.selection.setRng(t),f.selection.normalize(),f.nodeChanged()}}))}function V(){a.mac&&f.on("keydown",function(t){!e.metaKeyPressed(t)||t.shiftKey||37!=t.keyCode&&39!=t.keyCode||(t.preventDefault(),f.selection.getSel().modify("move",37==t.keyCode?"backward":"forward","lineboundary"))})}function $(){p("AutoUrlDetect",!1)}function q(){f.on("click",function(e){var t=e.target;do if("A"===t.tagName)return void e.preventDefault();while(t=t.parentNode)}),f.contentStyles.push(".mce-content-body {-webkit-touch-callout: none}")}function j(){f.on("init",function(){f.dom.bind(f.getBody(),"submit",function(e){e.preventDefault()})})}function Y(){oe.addNodeFilter("br",function(e){for(var t=e.length;t--;)"Apple-interchange-newline"==e[t].attr("class")&&e[t].remove()})}function X(){f.on("dragstart",function(e){g(e)}),f.on("drop",function(e){if(!m(e)){var n=v(e);if(n&&n.id!=f.id){e.preventDefault();var r=t.getCaretRangeFromPoint(e.x,e.y,f.getDoc());re.setRng(r),y(n.html)}}})}function K(){}function G(){var e;return se?(e=f.selection.getSel(),!e||!e.rangeCount||0===e.rangeCount):0}function J(){function t(e){var t=new d(e.getBody()),n=e.selection.getRng(),r=c.fromRangeStart(n),i=c.fromRangeEnd(n),o=t.prev(r),a=t.next(i);return!e.selection.isCollapsed()&&(!o||o.isAtStart())&&(!a||a.isAtEnd()&&r.getNode()!==a.getNode())}f.on("keypress",function(n){!m(n)&&!re.isCollapsed()&&n.charCode>31&&!e.metaKeyPressed(n)&&t(f)&&(n.preventDefault(),f.setContent(String.fromCharCode(n.charCode)),f.selection.select(f.getBody(),!0),f.selection.collapse(!1),f.nodeChanged())}),f.on("keydown",function(e){var n=e.keyCode;m(e)||n!=te&&n!=ee||t(f)&&(e.preventDefault(),f.setContent(""),f.nodeChanged())})}var Q=s.each,Z=f.$,ee=e.BACKSPACE,te=e.DELETE,ne=f.dom,re=f.selection,ie=f.settings,oe=f.parser,ae=f.serializer,se=a.gecko,le=a.ie,ue=a.webkit,ce="data:text/mce-internal,",de=le?"Text":"URL";return B(),C(),a.windowsPhone||F(),ue&&(J(),b(),w(),_(),M(),j(),T(),Y(),a.iOS?(U(),W(),q()):x()),le&&a.ie<11&&(E(),k(),R(),A(),P(),O(),H(),I()),a.ie>=11&&(W(),T()),a.ie&&(x(),$(),X()),se&&(J(),E(),N(),S(),D(),L(),z(),V(),T()),{refreshContentEditable:K,isHidden:G}}}),r(Ue,[pe,w,m],function(e,t,n){function r(e,t){return"selectionchange"==t?e.getDoc():!e.inline&&/^mouse|touch|click|contextmenu|drop|dragover|dragend/.test(t)?e.getDoc().documentElement:e.settings.event_root?(e.eventRoot||(e.eventRoot=o.select(e.settings.event_root)[0]),e.eventRoot):e.getBody()}function i(e,t){function n(e){return!e.hidden&&!e.readonly}var i=r(e,t),s;if(e.delegates||(e.delegates={}),!e.delegates[t])if(e.settings.event_root){if(a||(a={},e.editorManager.on("removeEditor",function(){var t;if(!e.editorManager.activeEditor&&a){for(t in a)e.dom.unbind(r(e,t));a=null}})),a[t])return;s=function(r){for(var i=r.target,a=e.editorManager.editors,s=a.length;s--;){var l=a[s].getBody();(l===i||o.isChildOf(i,l))&&n(a[s])&&a[s].fire(t,r)}},a[t]=s,o.bind(i,t,s)}else s=function(r){n(e)&&e.fire(t,r)},o.bind(i,t,s),e.delegates[t]=s}var o=t.DOM,a,s={bindPendingEventDelegates:function(){var e=this;n.each(e._pendingNativeEvents,function(t){i(e,t)})},toggleNativeEvent:function(e,t){var n=this;"focus"!=e&&"blur"!=e&&(t?n.initialized?i(n,e):n._pendingNativeEvents?n._pendingNativeEvents.push(e):n._pendingNativeEvents=[e]:n.initialized&&(n.dom.unbind(r(n,e),e,n.delegates[e]),delete n.delegates[e]))},unbindAllNativeEvents:function(){var e=this,t;if(e.delegates){for(t in e.delegates)e.dom.unbind(r(e,t),t,e.delegates[t]);delete e.delegates}e.inline||(e.getBody().onload=null,e.dom.unbind(e.getWin()),e.dom.unbind(e.getDoc())),e.dom.unbind(e.getBody()),e.dom.unbind(e.getContainer())}};return s=n.extend({},e,s)}),r(We,[],function(){function e(e,t,n){try{e.getDoc().execCommand(t,!1,n)}catch(r){}}function t(e){var t,n;return t=e.getBody(),n=function(t){e.dom.getParents(t.target,"a").length>0&&t.preventDefault()},e.dom.bind(t,"click",n),{unbind:function(){e.dom.unbind(t,"click",n)}}}function n(n,r){n._clickBlocker&&(n._clickBlocker.unbind(),n._clickBlocker=null),r?(n._clickBlocker=t(n),n.selection.controlSelection.hideResizeRect(),n.readonly=!0,n.getBody().contentEditable=!1):(n.readonly=!1,n.getBody().contentEditable=!0,e(n,"StyleWithCSS",!1),e(n,"enableInlineTableEditing",!1),e(n,"enableObjectResizing",!1),n.focus(),n.nodeChanged())}function r(e,t){var r=e.readonly?"readonly":"design";t!=r&&(e.initialized?n(e,"readonly"==t):e.on("init",function(){n(e,"readonly"==t)}),e.fire("SwitchMode",{mode:t}))}return{setMode:r}}),r(Ve,[m,d],function(e,t){var n=e.each,r=e.explode,i={f9:120,f10:121,f11:122},o=e.makeMap("alt,ctrl,shift,meta,access");return function(a){function s(e){var a,s,l={};n(r(e,"+"),function(e){e in o?l[e]=!0:/^[0-9]{2,}$/.test(e)?l.keyCode=parseInt(e,10):(l.charCode=e.charCodeAt(0),l.keyCode=i[e]||e.toUpperCase().charCodeAt(0))}),a=[l.keyCode];for(s in o)l[s]?a.push(s):l[s]=!1;return l.id=a.join(","),l.access&&(l.alt=!0,t.mac?l.ctrl=!0:l.shift=!0),l.meta&&(t.mac?l.meta=!0:(l.ctrl=!0,l.meta=!1)),l}function l(t,n,i,o){var l;return l=e.map(r(t,">"),s),l[l.length-1]=e.extend(l[l.length-1],{func:i,scope:o||a}),e.extend(l[0],{desc:a.translate(n),subpatterns:l.slice(1)})}function u(e){return e.altKey||e.ctrlKey||e.metaKey}function c(e){return"keydown"===e.type&&e.keyCode>=112&&e.keyCode<=123}function d(e,t){return!!t&&(t.ctrl==e.ctrlKey&&t.meta==e.metaKey&&(t.alt==e.altKey&&t.shift==e.shiftKey&&(!!(e.keyCode==t.keyCode||e.charCode&&e.charCode==t.charCode)&&(e.preventDefault(),!0))))}function f(e){return e.func?e.func.call(e.scope):null}var p=this,h={},m=[];a.on("keyup keypress keydown",function(e){!u(e)&&!c(e)||e.isDefaultPrevented()||(n(h,function(t){if(d(e,t))return m=t.subpatterns.slice(0),"keydown"==e.type&&f(t),!0}),d(e,m[0])&&(1===m.length&&"keydown"==e.type&&f(m[0]),m.shift()))}),p.add=function(t,i,o,s){var u;return u=o,"string"==typeof o?o=function(){a.execCommand(u,!1,null)}:e.isArray(u)&&(o=function(){a.execCommand(u[0],u[1],u[2])}),n(r(e.trim(t.toLowerCase())),function(e){var t=l(e,i,o,s);h[t.id]=t}),!0},p.remove=function(e){var t=l(e);return!!h[t.id]&&(delete h[t.id],!0)}}}),r($e,[u,m,z],function(e,t,n){return function(r,i){function o(e){var t,n;return n={"image/jpeg":"jpg","image/jpg":"jpg","image/gif":"gif","image/png":"png"},t=n[e.blob().type.toLowerCase()]||"dat",e.filename()+"."+t}function a(e,t){return e?e.replace(/\/$/,"")+"/"+t.replace(/^\//,""):t}function s(e){return{id:e.id,blob:e.blob,base64:e.base64,filename:n.constant(o(e))}}function l(e,t,n,r){var o,s;o=new XMLHttpRequest,o.open("POST",i.url),o.withCredentials=i.credentials,o.upload.onprogress=function(e){r(e.loaded/e.total*100)},o.onerror=function(){n("Image upload failed due to a XHR Transport error. Code: "+o.status)},o.onload=function(){var e;return 200!=o.status?void n("HTTP Error: "+o.status):(e=JSON.parse(o.responseText),e&&"string"==typeof e.location?void t(a(i.basePath,e.location)):void n("Invalid JSON: "+o.responseText))},s=new FormData,s.append("file",e.blob(),e.filename()),o.send(s)}function u(){return new e(function(e){e([])})}function c(e,t){return{url:t,blobInfo:e,status:!0}}function d(e,t){return{url:"",blobInfo:e,status:!1,error:t}}function f(e,n){t.each(y[e],function(e){e(n)}),delete y[e]}function p(t,n,i){return r.markPending(t.blobUri()),new e(function(e){var o,a,l=function(){};try{var u=function(){o&&(o.close(),a=l)},p=function(n){u(),r.markUploaded(t.blobUri(),n),f(t.blobUri(),c(t,n)),e(c(t,n))},h=function(n){u(),r.removeFailed(t.blobUri()),f(t.blobUri(),d(t,n)),e(d(t,n))};a=function(e){e<0||e>100||(o||(o=i()),o.progressBar.value(e))},n(s(t),p,h,a)}catch(m){e(d(t,m.message))}})}function h(e){return e===l}function m(t){var n=t.blobUri();return new e(function(e){y[n]=y[n]||[],y[n].push(e)})}function g(n,o){return n=t.grep(n,function(e){return!r.isUploaded(e.blobUri())}),e.all(t.map(n,function(e){return r.isPending(e.blobUri())?m(e):p(e,i.handler,o)}))}function v(e,t){return!i.url&&h(i.handler)?u():g(e,t)}var y={};return i=t.extend({credentials:!1,handler:l},i),{upload:v}}}),r(qe,[u],function(e){function t(t){return new e(function(e){var n=new XMLHttpRequest;n.open("GET",t,!0),n.responseType="blob",n.onload=function(){200==this.status&&e(this.response)},n.send()})}function n(e){var t,n;return e=decodeURIComponent(e).split(","),n=/data:([^;]+)/.exec(e[0]),n&&(t=n[1]),{type:t,data:e[1]}}function r(t){return new e(function(e){var r,i,o;t=n(t);try{r=atob(t.data)}catch(a){return void e(new Blob([]))}for(i=new Uint8Array(r.length),o=0;o0&&(n&&(l*=-1),r.left+=l,r.right+=l),r}function l(){var n,r,o,a,s;for(n=i("*[contentEditable=false]",t),a=0;a
      ').css(l).appendTo(t),o&&m.addClass("mce-visual-caret-before"),d(),u=a.ownerDocument.createRange(),u.setStart(g,0),u.setEnd(g,0),u):(g=e.insertInline(a,o),u=a.ownerDocument.createRange(),s(g.nextSibling)?(u.setStart(g,0),u.setEnd(g,0)):(u.setStart(g,1),u.setEnd(g,1)),u)}function c(){l(),g&&(e.remove(g),g=null),m&&(m.remove(),m=null),clearInterval(h)}function d(){h=a.setInterval(function(){i("div.mce-visual-caret",t).toggleClass("mce-visual-caret-hidden")},500)}function f(){a.clearInterval(h)}function p(){return".mce-visual-caret {position: absolute;background-color: black;background-color: currentcolor;}.mce-visual-caret-hidden {display: none;}*[data-mce-caret] {position: absolute;left: -1000px;right: auto;top: 0;margin: 0;padding: 0;}"}var h,m,g;return{show:u,hide:c,getCss:p,destroy:f}}}),r(Qe,[h,_,W],function(e,t,n){function r(i){function o(t){return e.map(t,function(e){return e=n.clone(e),e.node=i,e})}if(e.isArray(i))return e.reduce(i,function(e,t){return e.concat(r(t))},[]);if(t.isElement(i))return o(i.getClientRects());if(t.isText(i)){var a=i.ownerDocument.createRange();return a.setStart(i,0),a.setEnd(i,i.data.length),o(a.getClientRects())}}return{getClientRects:r}}),r(Ze,[z,h,Qe,U,ie,oe,$,W],function(e,t,n,r,i,o,a,s){function l(e,t,n,o){for(;o=i.findNode(o,e,r.isEditableCaretCandidate,t);)if(n(o))return}function u(e,r,i,o,a,s){function u(o){var s,l,u;for(u=n.getClientRects(o),e==-1&&(u=u.reverse()),s=0;s0&&r(l,t.last(f))&&c++,l.line=c,a(l))return!0;f.push(l)}}var c=0,d,f=[],p;return(p=t.last(s.getClientRects()))?(d=s.getNode(),u(d),l(e,o,u,d),f):f}function c(e,t){return t.line>e}function d(e,t){return t.line===e}function f(e,n,r,i){function l(n){return 1==e?t.last(n.getClientRects()):t.last(n.getClientRects())}var u=new o(n),c,d,f,p,h=[],m=0,g,v;1==e?(c=u.next,d=s.isBelow,f=s.isAbove,p=a.after(i)):(c=u.prev,d=s.isAbove,f=s.isBelow,p=a.before(i)),v=l(p);do if(p.isVisible()&&(g=l(p),!f(g,v))){if(h.length>0&&d(g,t.last(h))&&m++,g=s.clone(g),g.position=p,g.line=m,r(g))return h;h.push(g)}while(p=c(p));return h}var p=e.curry,h=p(u,-1,s.isAbove,s.isBelow),m=p(u,1,s.isBelow,s.isAbove);return{upUntil:h,downUntil:m,positionsUntil:f,isAboveLine:p(c),isLine:p(d)}}),r(et,[z,h,_,Qe,W,ie,U],function(e,t,n,r,i,o,a){function s(e,t){return Math.abs(e.left-t)}function l(e,t){return Math.abs(e.right-t)}function u(e,n){function r(e,t){return e>=t.left&&e<=t.right}return t.reduce(e,function(e,t){var i,o;return i=Math.min(s(e,n),l(e,n)),o=Math.min(s(t,n),l(t,n)),r(n,t)?t:r(n,e)?e:o==i&&m(t.node)?t:o=e.top&&i<=e.bottom}),a=u(o,n),a&&(a=u(d(e,a),n),a&&m(a.node))?p(a,n):null}var m=n.isContentEditableFalse,g=o.findNode,v=e.curry;return{findClosestClientRect:u,findLineNodeRects:d,closestCaret:h}}),r(tt,[],function(){var e=function(e){var t,n,r,i;return i=e.getBoundingClientRect(),t=e.ownerDocument,n=t.documentElement,r=t.defaultView,{top:i.top+r.pageYOffset-n.clientTop,left:i.left+r.pageXOffset-n.clientLeft}},t=function(t){return t.inline?e(t.getBody()):{left:0,top:0}},n=function(e){var t=e.getBody();return e.inline?{left:t.scrollLeft,top:t.scrollTop}:{left:0,top:0}},r=function(e){var t=e.getBody(),n=e.getDoc().documentElement,r={left:t.scrollLeft,top:t.scrollTop},i={left:t.scrollLeft||n.scrollLeft,top:t.scrollTop||n.scrollTop};return e.inline?r:i},i=function(t,n){if(n.target.ownerDocument!==t.getDoc()){var i=e(t.getContentAreaContainer()),o=r(t);return{left:n.pageX-i.left+o.left,top:n.pageY-i.top+o.top}}return{left:n.pageX,top:n.pageY}},o=function(e,t,n){return{pageX:n.left-e.left+t.left,pageY:n.top-e.top+t.top}},a=function(e,r){return o(t(e),n(e),i(e,r))};return{calc:a}}),r(nt,[_,h,z,c,w,tt],function(e,t,n,r,i,o){var a=e.isContentEditableFalse,s=e.isContentEditableTrue,l=function(e){return a(e)},u=function(e,t,n){return t!==n&&!e.dom.isChildOf(t,n)&&!a(t)},c=function(e){var t=e.cloneNode(!0);return t.removeAttribute("data-mce-selected"),t},d=function(e,t,n,r){var i=t.cloneNode(!0);e.dom.setStyles(i,{width:n,height:r}),e.dom.setAttrib(i,"data-mce-selected",null);var o=e.dom.create("div",{"class":"mce-drag-container","data-mce-bogus":"all",unselectable:"on",contenteditable:"false"});return e.dom.setStyles(o,{position:"absolute",opacity:.5,overflow:"hidden",border:0,padding:0,margin:0,width:n,height:r}),e.dom.setStyles(i,{margin:0,boxSizing:"border-box"}),o.appendChild(i),o},f=function(e,t){e.parentNode!==t&&t.appendChild(e)},p=function(e,t,n,r,i,o){var a=0,s=0;e.style.left=t.pageX+"px",e.style.top=t.pageY+"px",t.pageX+n>i&&(a=t.pageX+n-i),t.pageY+r>o&&(s=t.pageY+r-o),e.style.width=n-a+"px",e.style.height=r-s+"px"},h=function(e){e&&e.parentNode&&e.parentNode.removeChild(e)},m=function(e){return 0===e.button},g=function(e){return e.element},v=function(e,t){return{pageX:t.pageX-e.relX,pageY:t.pageY+5}},y=function(e,r){return function(i){if(m(i)){var o=t.find(r.dom.getParents(i.target),n.or(a,s));if(l(o)){var u=r.dom.getPos(o),c=r.getBody(),f=r.getDoc().documentElement;e.element=o,e.screenX=i.screenX,e.screenY=i.screenY,e.maxX=(r.inline?c.scrollWidth:f.offsetWidth)-2,e.maxY=(r.inline?c.scrollHeight:f.offsetHeight)-2,e.relX=i.pageX-u.x,e.relY=i.pageY-u.y,e.width=o.offsetWidth,e.height=o.offsetHeight,e.ghost=d(r,o,e.width,e.height)}}}},b=function(e,t){var n=r.throttle(function(e,n){t._selectionOverrides.hideFakeCaret(),t.selection.placeCaretAt(e,n)},0);return function(r){var i=Math.max(Math.abs(r.screenX-e.screenX),Math.abs(r.screenY-e.screenY));if(g(e)&&!e.dragging&&i>10){var a=t.fire("dragstart",{target:e.element});if(a.isDefaultPrevented())return;e.dragging=!0,t.focus()}if(e.dragging){var s=v(e,o.calc(t,r));f(e.ghost,t.getBody()),p(e.ghost,s,e.width,e.height,e.maxX,e.maxY),n(r.clientX,r.clientY)}}},C=function(e){var t=e.getSel().getRangeAt(0),n=t.startContainer;return 3===n.nodeType?n.parentNode:n},x=function(e,t){return function(n){if(e.dragging&&u(t,C(t.selection),e.element)){var r=c(e.element),i=t.fire("drop",{targetClone:r,clientX:n.clientX,clientY:n.clientY});i.isDefaultPrevented()||(r=i.targetClone,t.undoManager.transact(function(){h(e.element),t.insertContent(t.dom.getOuterHTML(r)),t._selectionOverrides.hideFakeCaret()}))}E(e)}},w=function(e,t){return function(){E(e),e.dragging&&t.fire("dragend")}},E=function(e){e.dragging=!1,e.element=null,h(e.ghost)},N=function(e){var t={},n,r,o,a,s,l;n=i.DOM,l=document,r=y(t,e),o=b(t,e),a=x(t,e),s=w(t,e),e.on("mousedown",r),e.on("mousemove",o),e.on("mouseup",a),n.bind(l,"mousemove",o),n.bind(l,"mouseup",s),e.on("remove",function(){n.unbind(l,"mousemove",o),n.unbind(l,"mouseup",s)})},_=function(e){e.on("drop",function(t){var n="undefined"!=typeof t.clientX?e.getDoc().elementFromPoint(t.clientX,t.clientY):null;(a(n)||a(e.dom.getContentEditableParent(n)))&&t.preventDefault()})},S=function(e){N(e),_(e)};return{init:S}}),r(rt,[d,oe,$,k,ie,Je,Ze,et,_,T,W,I,z,h,c,nt],function(e,t,n,r,i,o,a,s,l,u,c,d,f,p,h,m){function g(e,t){for(;t=e(t);)if(t.isVisible())return t;return t}function v(u){function v(e){return u.dom.hasClass(e,"mce-offscreen-selection")}function _(){var e=u.dom.get(le);return e?e.getElementsByTagName("*")[0]:e}function S(e){return u.dom.isBlock(e)}function k(e){e&&u.selection.setRng(e)}function T(){return u.selection.getRng()}function R(e,t){u.selection.scrollIntoView(e,t)}function A(e,t,n){var r;return r=u.fire("ShowCaret",{target:t,direction:e,before:n}),r.isDefaultPrevented()?null:(R(t,e===-1),se.show(n,t))}function B(e){var t;return t=u.fire("BeforeObjectSelected",{target:e}),t.isDefaultPrevented()?null:D(e)}function D(e){var t=e.ownerDocument.createRange();return t.selectNode(e),t}function L(e,t){var n=i.isInSameBlock(e,t);return!(n||!l.isBr(e.getNode()))||n}function M(e,t){return t=i.normalizeRange(e,re,t),e==-1?n.fromRangeStart(t):n.fromRangeEnd(t)}function P(e){return r.isCaretContainerBlock(e.startContainer)}function O(e,t,n,r){var i,o,a,s;return!r.collapsed&&(i=N(r),C(i))?A(e,i,e==-1):(s=P(r),o=M(e,r),n(o)?B(o.getNode(e==-1)):(o=t(o))?n(o)?A(e,o.getNode(e==-1),1==e):(a=t(o),n(a)&&L(o,a)?A(e,a.getNode(e==-1),1==e):s?$(o.toRange()):null):s?r:null)}function H(e,t,n){var r,i,o,l,u,c,d,f,h;if(h=N(n),r=M(e,n),i=t(re,a.isAboveLine(1),r),o=p.filter(i,a.isLine(1)),u=p.last(r.getClientRects()),E(r)&&(h=r.getNode()),w(r)&&(h=r.getNode(!0)),!u)return null;if(c=u.left,l=s.findClosestClientRect(o,c),l&&C(l.node))return d=Math.abs(c-l.left),f=Math.abs(c-l.right),A(e,l.node,d=11)&&(t.innerHTML='
      '),t}var o,a,s;if(r.collapsed&&u.settings.forced_root_block){if(o=u.dom.getParent(r.startContainer,"PRE"),!o)return;a=1==t?oe(n.fromRangeStart(r)):ae(n.fromRangeStart(r)),a||(s=i(),1==t?u.$(o).after(s):u.$(o).before(s),u.selection.select(s,!0),u.selection.collapse())}}function F(e,t,n,r){var i;return(i=O(e,t,n,r))?i:(i=I(e,r),i?i:null)}function z(e,t,n){var r;return(r=H(e,t,n))?r:(r=I(e,n),r?r:null)}function U(){return ce("*[data-mce-caret]")[0]}function W(e){e.hasAttribute("data-mce-caret")&&(r.showCaretContainerBlock(e),k(T()),R(e[0]))}function V(e){var t,r;return e=i.normalizeRange(1,re,e),t=n.fromRangeStart(e),C(t.getNode())?A(1,t.getNode(),!t.isAtEnd()):C(t.getNode(!0))?A(1,t.getNode(!0),!1):(r=u.dom.getParent(t.getNode(),f.or(C,b)),C(r)?A(1,r,!1):null)}function $(e){var t;return e&&e.collapsed?(t=V(e),t?t:e):e}function q(e){var t,i,o,a;return C(e)?(C(e.previousSibling)&&(o=e.previousSibling),i=ae(n.before(e)),i||(t=oe(n.after(e))),t&&x(t.getNode())&&(a=t.getNode()),r.remove(e.previousSibling),r.remove(e.nextSibling),u.dom.remove(e),u.dom.isEmpty(u.getBody())?(u.setContent(""),void u.focus()):o?n.after(o).toRange():a?n.before(a).toRange():i?i.toRange():t?t.toRange():null):null}function j(e){var t=u.schema.getTextBlockElements();return e.nodeName in t}function Y(e){return u.dom.isEmpty(e)}function X(e,t,r){var i=u.dom,o,a,s,l;if(o=i.getParent(t.getNode(),i.isBlock),a=i.getParent(r.getNode(),i.isBlock),e===-1){if(l=r.getNode(!0),w(r)&&S(l))return j(o)?(Y(o)&&i.remove(o),n.after(l).toRange()):q(r.getNode(!0))}else if(l=t.getNode(),E(t)&&S(l))return j(a)?(Y(a)&&i.remove(a),n.before(l).toRange()):q(t.getNode());if(o===a||!j(o)||!j(a))return null;for(;s=o.firstChild;)a.appendChild(s);return u.dom.remove(o),r.toRange()}function K(e,t,n,i){var o,a,s,l;return!i.collapsed&&(o=N(i),C(o))?$(q(o)):(a=M(e,i),n(a)&&r.isCaretContainerBlock(i.startContainer)?(l=e==-1?ie.prev(a):ie.next(a),l?$(l.toRange()):i):t(a)?$(q(a.getNode(e==-1))):(s=e==-1?ie.prev(a):ie.next(a),t(s)?e===-1?X(e,a,s):X(e,s,a):void 0))}function G(){function i(e,t){var n=t(T());n&&!e.isDefaultPrevented()&&(e.preventDefault(),k(n))}function o(e){for(var t=u.getBody();e&&e!=t;){if(b(e)||C(e))return e;e=e.parentNode}return null}function l(e,t,n){return!n.collapsed&&p.reduce(n.getClientRects(),function(n,r){return n||c.containsXY(r,e,t)},!1)}function f(e){var t=!1;e.on("touchstart",function(){t=!1}),e.on("touchmove",function(){t=!0}),e.on("touchend",function(e){var n=o(e.target);C(n)&&(t||(e.preventDefault(),Z(B(n))))})}function g(){var e,t=o(u.selection.getNode());b(t)&&S(t)&&u.dom.isEmpty(t)&&(e=u.dom.create("br",{"data-mce-bogus":"1"}),u.$(t).empty().append(e),u.selection.setRng(n.before(e).toRange()))}function x(e){var t=U();if(t)return"compositionstart"==e.type?(e.preventDefault(),e.stopPropagation(),void W(t)):void(r.hasContent(t)&&W(t))}function N(e){var t;switch(e.keyCode){case d.DELETE:t=g();break;case d.BACKSPACE:t=g()}t&&e.preventDefault()}var R=y(F,1,oe,E),D=y(F,-1,ae,w),L=y(K,1,E,w),M=y(K,-1,w,E),P=y(z,-1,a.upUntil),O=y(z,1,a.downUntil);u.on("mouseup",function(){var e=T();e.collapsed&&k(V(e))}),u.on("click",function(e){var t;t=o(e.target),t&&(C(t)&&(e.preventDefault(),u.focus()),b(t)&&u.dom.isChildOf(t,u.selection.getNode())&&ee())}),u.on("blur NewBlock",function(){ee(),ne()});var H=function(e){var r=new t(e);if(!e.firstChild)return!1;var i=n.before(e.firstChild),o=r.next(i);return o&&!E(o)&&!w(o)},I=function(e,t){var n=u.dom.getParent(e,u.dom.isBlock),r=u.dom.getParent(t,u.dom.isBlock);return n===r},j=function(e){return!(e.keyCode>=112&&e.keyCode<=123)},Y=function(e,t){var n=u.dom.getParent(e,u.dom.isBlock),r=u.dom.getParent(t,u.dom.isBlock);return n&&!I(n,r)&&H(n)};f(u),u.on("mousedown",function(e){var t;if(t=o(e.target))C(t)?(e.preventDefault(),Z(B(t))):l(e.clientX,e.clientY,u.selection.getRng())||u.selection.placeCaretAt(e.clientX,e.clientY);else{ee(),ne();var n=s.closestCaret(re,e.clientX,e.clientY);n&&(Y(e.target,n.node)||(e.preventDefault(),u.getBody().focus(),k(A(1,n.node,n.before))))}}),u.on("keydown",function(e){if(!d.modifierPressed(e))switch(e.keyCode){case d.RIGHT:i(e,R);break;case d.DOWN:i(e,O);break;case d.LEFT:i(e,D);break;case d.UP:i(e,P);break;case d.DELETE:i(e,L);break;case d.BACKSPACE:i(e,M);break;default:C(u.selection.getNode())&&j(e)&&e.preventDefault()}}),u.on("keyup compositionstart",function(e){x(e),N(e)},!0),u.on("cut",function(){var e=u.selection.getNode();C(e)&&h.setEditorTimeout(u,function(){k($(q(e)))})}),u.on("getSelectionRange",function(e){var t=e.range;if(ue){if(!ue.parentNode)return void(ue=null);t=t.cloneRange(),t.selectNode(ue),e.range=t}}),u.on("setSelectionRange",function(e){var t;t=Z(e.range),t&&(e.range=t)}),u.on("AfterSetSelectionRange",function(e){var t=e.range;Q(t)||ne(),v(t.startContainer.parentNode)||ee()}),u.on("focus",function(){h.setEditorTimeout(u,function(){u.selection.setRng($(u.selection.getRng()))},0)}),u.on("copy",function(t){var n=t.clipboardData;if(!t.isDefaultPrevented()&&t.clipboardData&&!e.ie){var r=_();r&&(t.preventDefault(),n.clearData(),n.setData("text/html",r.outerHTML),n.setData("text/plain",r.outerText))}}),m.init(u)}function J(){var e=u.contentStyles,t=".mce-content-body";e.push(se.getCss()),e.push(t+" .mce-offscreen-selection {position: absolute;left: -9999999999px;max-width: 1000000px;}"+t+" *[contentEditable=false] {cursor: default;}"+t+" *[contentEditable=true] {cursor: text;}")}function Q(e){return r.isCaretContainer(e.startContainer)||r.isCaretContainer(e.endContainer)}function Z(t){var n,r=u.$,i=u.dom,o,a,s,l,c,d,f,p,h;if(!t)return null;if(t.collapsed){if(!Q(t)){if(f=M(1,t),C(f.getNode()))return A(1,f.getNode(),!f.isAtEnd());if(C(f.getNode(!0)))return A(1,f.getNode(!0),!1)}return null}return s=t.startContainer,l=t.startOffset,c=t.endOffset,3==s.nodeType&&0==l&&C(s.parentNode)&&(s=s.parentNode,l=i.nodeIndex(s),s=s.parentNode),1!=s.nodeType?null:(c==l+1&&(n=s.childNodes[l]),C(n)?(p=h=n.cloneNode(!0),d=u.fire("ObjectSelected",{target:n,targetClone:p}),d.isDefaultPrevented()?null:(p=d.targetClone,o=r("#"+le),0===o.length&&(o=r('
      ').attr("id",le),o.appendTo(u.getBody())),t=u.dom.createRng(),p===h&&e.ie?(o.empty().append('

      \xa0

      ').append(p),t.setStartAfter(o[0].firstChild.firstChild),t.setEndAfter(p)):(o.empty().append("\xa0").append(p).append("\xa0"),t.setStart(o[0].firstChild,1),t.setEnd(o[0].lastChild,0)),o.css({top:i.getPos(n,u.getBody()).y}),o[0].focus(),a=u.selection.getSel(),a.removeAllRanges(),a.addRange(t),u.$("*[data-mce-selected]").removeAttr("data-mce-selected"),n.setAttribute("data-mce-selected",1),ue=n,ne(),t)):null)}function ee(){ue&&(ue.removeAttribute("data-mce-selected"),u.$("#"+le).remove(),ue=null)}function te(){se.destroy(),ue=null}function ne(){se.hide()}var re=u.getBody(),ie=new t(re),oe=y(g,ie.next),ae=y(g,ie.prev),se=new o(u.getBody(),S),le="sel-"+u.dom.uniqueId(),ue,ce=u.$;return e.ceFalse&&(G(),J()),{showBlockCaretContainer:W,hideFakeCaret:ne,destroy:te}}var y=f.curry,b=l.isContentEditableTrue,C=l.isContentEditableFalse,x=l.isElement,w=i.isAfterContentEditableFalse,E=i.isBeforeContentEditableFalse,N=u.getSelectedNode;return v}),r(it,[],function(){var e=0,t=function(){var e=function(){return Math.round(4294967295*Math.random()).toString(36)},t=(new Date).getTime();return"s"+t.toString(36)+e()+e()+e()},n=function(n){return n+e++ +t()};return{uuid:n}}),r(ot,[],function(){var e=function(e,t,n){var r=e.sidebars?e.sidebars:[];r.push({name:t,settings:n}),e.sidebars=r};return{add:e}}),r(at,[w,g,N,R,A,O,P,Y,J,te,ne,re,le,ue,E,f,Le,Ie,B,L,ze,d,m,c,Ue,We,Ve,Ge,rt,it,ot,Ke],function(e,n,r,i,o,a,s,l,u,c,d,f,p,h,m,g,v,y,b,C,x,w,E,N,_,S,k,T,R,A,B,D){function L(e,t,i){var o=this,a,s,l;a=o.documentBaseUrl=i.documentBaseURL,s=i.baseURI,l=i.defaultSettings,t=H({id:e,theme:"modern",delta_width:0,delta_height:0,popup_css:"",plugins:"",document_base_url:a,add_form_submit_trigger:!0,submit_patch:!0,add_unload_trigger:!0,convert_urls:!0,relative_urls:!0,remove_script_host:!0,object_resizing:!0,doctype:"",visual:!0,font_size_style_values:"xx-small,x-small,small,medium,large,x-large,xx-large",font_size_legacy_values:"xx-small,small,medium,large,x-large,xx-large,300%",forced_root_block:"p",hidden_input:!0,padd_empty_editor:!0,render_ui:!0,indentation:"30px",inline_styles:!0,convert_fonts_to_spans:!0,indent:"simple",indent_before:"p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist",indent_after:"p,h1,h2,h3,h4,h5,h6,blockquote,div,title,style,pre,script,td,th,ul,ol,li,dl,dt,dd,area,table,thead,tfoot,tbody,tr,section,article,hgroup,aside,figure,figcaption,option,optgroup,datalist",validate:!0,entity_encoding:"named",url_converter:o.convertURL,url_converter_scope:o,ie7_compat:!0},l,t),l&&l.external_plugins&&t.external_plugins&&(t.external_plugins=H({},l.external_plugins,t.external_plugins)),o.settings=t,r.language=t.language||"en",r.languageLoad=t.language_load,r.baseURL=i.baseURL,o.id=t.id=e,o.setDirty(!1),o.plugins={},o.documentBaseURI=new h(t.document_base_url||a,{base_uri:s}),o.baseURI=s,o.contentCSS=[],o.contentStyles=[],o.shortcuts=new k(o),o.loadedCSS={},o.editorCommands=new p(o),o.suffix=i.suffix,o.editorManager=i,o.inline=t.inline,o.settings.content_editable=o.inline,t.cache_suffix&&(w.cacheSuffix=t.cache_suffix.replace(/^[\?\&]+/,"")),t.override_viewport===!1&&(w.overrideViewPort=!1),i.fire("SetupEditor",o),o.execCallback("setup",o),o.$=n.overrideDefaults(function(){return{context:o.inline?o.getBody():o.getDoc(),element:o.getBody()}})}var M=e.DOM,P=r.ThemeManager,O=r.PluginManager,H=E.extend,I=E.each,F=E.explode,z=E.inArray,U=E.trim,W=E.resolve,V=g.Event,$=w.gecko,q=w.ie;return L.prototype={render:function(){function e(){M.unbind(window,"ready",e),n.render()}function t(){var e=m.ScriptLoader;if(r.language&&"en"!=r.language&&!r.language_url&&(r.language_url=n.editorManager.baseURL+"/langs/"+r.language+".js"),r.language_url&&e.add(r.language_url),r.theme&&"function"!=typeof r.theme&&"-"!=r.theme.charAt(0)&&!P.urls[r.theme]){var t=r.theme_url;t=t?n.documentBaseURI.toAbsolute(t):"themes/"+r.theme+"/theme"+o+".js",P.load(r.theme,t)}E.isArray(r.plugins)&&(r.plugins=r.plugins.join(" ")),I(r.external_plugins,function(e,t){O.load(t,e),r.plugins+=" "+t}),I(r.plugins.split(/[ ,]/),function(e){if(e=U(e),e&&!O.urls[e])if("-"==e.charAt(0)){e=e.substr(1,e.length);var t=O.dependencies(e);I(t,function(e){var t={prefix:"plugins/",resource:e,suffix:"/plugin"+o+".js"};e=O.createUrl(t,e),O.load(e.resource,e)})}else O.load(e,{prefix:"plugins/",resource:e,suffix:"/plugin"+o+".js"})}),e.loadQueue(function(){n.removed||n.init()},n,function(e){D.pluginLoadError(n,e[0]),n.removed||n.init()})}var n=this,r=n.settings,i=n.id,o=n.suffix;if(!V.domLoaded)return void M.bind(window,"ready",e);if(n.getElement()&&w.contentEditable){r.inline?n.inline=!0:(n.orgVisibility=n.getElement().style.visibility,n.getElement().style.visibility="hidden");var a=n.getElement().form||M.getParent(i,"form");a&&(n.formElement=a,r.hidden_input&&!/TEXTAREA|INPUT/i.test(n.getElement().nodeName)&&(M.insertAfter(M.create("input",{type:"hidden",name:i}),i),n.hasHiddenInput=!0),n.formEventDelegate=function(e){n.fire(e.type,e)},M.bind(a,"submit reset",n.formEventDelegate),n.on("reset",function(){n.setContent(n.startContent,{format:"raw"})}),!r.submit_patch||a.submit.nodeType||a.submit.length||a._mceOldSubmit||(a._mceOldSubmit=a.submit,a.submit=function(){return n.editorManager.triggerSave(),n.setDirty(!1),a._mceOldSubmit(a)})),n.windowManager=new v(n),n.notificationManager=new y(n),"xml"==r.encoding&&n.on("GetContent",function(e){e.save&&(e.content=M.encode(e.content))}),r.add_form_submit_trigger&&n.on("submit",function(){n.initialized&&n.save()}),r.add_unload_trigger&&(n._beforeUnload=function(){!n.initialized||n.destroyed||n.isHidden()||n.save({format:"raw",no_events:!0,set_dirty:!1})},n.editorManager.on("BeforeUnload",n._beforeUnload)),n.editorManager.add(n),t()}},init:function(){function e(n){var r=O.get(n),i,o;if(i=O.urls[n]||t.documentBaseUrl.replace(/\/$/,""),n=U(n),r&&z(m,n)===-1){if(I(O.dependencies(n),function(t){e(t)}),t.plugins[n])return;o=new r(t,i,t.$),t.plugins[n]=o,o.init&&(o.init(t,i),m.push(n))}}var t=this,n=t.settings,r=t.getElement(),i,o,a,s,l,u,c,d,f,p,h,m=[];if(t.rtl=n.rtl_ui||t.editorManager.i18n.rtl,t.editorManager.i18n.setCode(n.language),n.aria_label=n.aria_label||M.getAttrib(r,"aria-label",t.getLang("aria.rich_text_area")),t.fire("ScriptsLoaded"),n.theme&&("function"!=typeof n.theme?(n.theme=n.theme.replace(/-/,""),u=P.get(n.theme),t.theme=new u(t,P.urls[n.theme]),t.theme.init&&t.theme.init(t,P.urls[n.theme]||t.documentBaseUrl.replace(/\/$/,""),t.$)):t.theme=n.theme),I(n.plugins.replace(/\-/g,"").split(/[ ,]/),e),n.render_ui&&t.theme&&(t.orgDisplay=r.style.display,"function"!=typeof n.theme?(i=n.width||r.style.width||r.offsetWidth,o=n.height||r.style.height||r.offsetHeight,a=n.min_height||100,p=/^[0-9\.]+(|px)$/i,p.test(""+i)&&(i=Math.max(parseInt(i,10),100)),p.test(""+o)&&(o=Math.max(parseInt(o,10),a)),l=t.theme.renderUI({targetNode:r,width:i,height:o,deltaWidth:n.delta_width,deltaHeight:n.delta_height}),n.content_editable||(o=(l.iframeHeight||o)+("number"==typeof o?l.deltaHeight||0:""),o",n.document_base_url!=t.documentBaseUrl&&(t.iframeHTML+=''),!w.caretAfter&&n.ie7_compat&&(t.iframeHTML+=''),t.iframeHTML+='',!/#$/.test(document.location.href))for(h=0;h',t.loadedCSS[g]=!0}d=n.body_id||"tinymce",d.indexOf("=")!=-1&&(d=t.getParam("body_id","","hash"),d=d[t.id]||d),f=n.body_class||"",f.indexOf("=")!=-1&&(f=t.getParam("body_class","","hash"),f=f[t.id]||""),n.content_security_policy&&(t.iframeHTML+=''),t.iframeHTML+='
      ';var v='javascript:(function(){document.open();document.domain="'+document.domain+'";var ed = window.parent.tinymce.get("'+t.id+'");document.write(ed.iframeHTML);document.close();ed.initContentBody(true);})()';document.domain!=location.hostname&&w.ie&&w.ie<12&&(c=v);var y=M.create("iframe",{id:t.id+"_ifr",frameBorder:"0",allowTransparency:"true",title:t.editorManager.translate("Rich Text Area. Press ALT-F9 for menu. Press ALT-F10 for toolbar. Press ALT-0 for help"),style:{width:"100%",height:o,display:"block"}});if(y.onload=function(){y.onload=null,t.fire("load")},M.setAttrib(y,"src",c||'javascript:""'),t.contentAreaContainer=l.iframeContainer,t.iframeElement=y,s=M.add(l.iframeContainer,y),q)try{t.getDoc()}catch(b){s.src=c=v}l.editorContainer&&(M.get(l.editorContainer).style.display=t.orgDisplay,t.hidden=M.isHidden(l.editorContainer)),t.getElement().style.display="none",M.setAttrib(t.id,"aria-hidden",!0),c||t.initContentBody(),r=s=l=null},initContentBody:function(t){var n=this,r=n.settings,s=n.getElement(),p=n.getDoc(),h,m;r.inline||(n.getElement().style.visibility=n.orgVisibility),t||r.content_editable||(p.open(),p.write(n.iframeHTML),p.close()),r.content_editable&&(n.on("remove",function(){var e=this.getBody();M.removeClass(e,"mce-content-body"),M.removeClass(e,"mce-edit-focus"),M.setAttrib(e,"contentEditable",null)}),M.addClass(s,"mce-content-body"),n.contentDocument=p=r.content_document||document,n.contentWindow=r.content_window||window,n.bodyElement=s,r.content_document=r.content_window=null,r.root_name=s.nodeName.toLowerCase()),h=n.getBody(),h.disabled=!0,n.readonly=r.readonly,n.readonly||(n.inline&&"static"==M.getStyle(h,"position",!0)&&(h.style.position="relative"),h.contentEditable=n.getParam("content_editable_state",!0)),h.disabled=!1,n.editorUpload=new T(n),n.schema=new b(r),n.dom=new e(p,{keep_values:!0,url_converter:n.convertURL,url_converter_scope:n,hex_colors:r.force_hex_style_colors,class_filter:r.class_filter,update_styles:!0,root_element:n.inline?n.getBody():null,collect:r.content_editable,schema:n.schema,onSetAttrib:function(e){n.fire("SetAttrib",e)}}),n.parser=new C(r,n.schema),n.parser.addAttributeFilter("src,href,style,tabindex",function(e,t){for(var r=e.length,i,o=n.dom,a,s;r--;)if(i=e[r],a=i.attr(t),s="data-mce-"+t,!i.attributes.map[s]){if(0===a.indexOf("data:")||0===a.indexOf("blob:"))continue;"style"===t?(a=o.serializeStyle(o.parseStyle(a),i.name),a.length||(a=null),i.attr(s,a),i.attr(t,a)):"tabindex"===t?(i.attr(s,a),i.attr(t,null)):i.attr(s,n.convertURL(a,t,i.name))}}),n.parser.addNodeFilter("script",function(e){for(var t=e.length,n,r;t--;)n=e[t],r=n.attr("type")||"no/type",0!==r.indexOf("mce-")&&n.attr("type","mce-"+r)}),n.parser.addNodeFilter("#cdata",function(e){for(var t=e.length,n;t--;)n=e[t],n.type=8,n.name="#comment",n.value="[CDATA["+n.value+"]]"}),n.parser.addNodeFilter("p,h1,h2,h3,h4,h5,h6,div",function(e){for(var t=e.length,r,i=n.schema.getNonEmptyElements();t--;)r=e[t],r.isEmpty(i)&&(r.append(new o("br",1)).shortEnded=!0)}),n.serializer=new a(r,n),n.selection=new l(n.dom,n.getWin(),n.serializer,n),n.formatter=new u(n),n.undoManager=new c(n),n.forceBlocks=new f(n),n.enterKey=new d(n),n._nodeChangeDispatcher=new i(n),n._selectionOverrides=new R(n),n.fire("PreInit"),r.browser_spellcheck||r.gecko_spellcheck||(p.body.spellcheck=!1,M.setAttrib(h,"spellcheck","false")),n.quirks=new x(n),n.fire("PostRender"),r.directionality&&(h.dir=r.directionality),r.nowrap&&(h.style.whiteSpace="nowrap"),r.protect&&n.on("BeforeSetContent",function(e){I(r.protect,function(t){e.content=e.content.replace(t,function(e){return""})})}),n.on("SetContent",function(){n.addVisual(n.getBody())}),r.padd_empty_editor&&n.on("PostProcess",function(e){e.content=e.content.replace(/^(]*>( | |\s|\u00a0|)<\/p>[\r\n]*|
      [\r\n]*)$/,"")}),n.load({initial:!0,format:"html"}),n.startContent=n.getContent({format:"raw"}),n.initialized=!0,n.bindPendingEventDelegates(),n.fire("init"),n.focus(!0),n.nodeChanged({initial:!0}),n.execCallback("init_instance_callback",n), +n.on("compositionstart compositionend",function(e){n.composing="compositionstart"===e.type}),n.contentStyles.length>0&&(m="",I(n.contentStyles,function(e){m+=e+"\r\n"}),n.dom.addStyle(m)),I(n.contentCSS,function(e){n.loadedCSS[e]||(n.dom.loadCSS(e),n.loadedCSS[e]=!0)}),r.auto_focus&&N.setEditorTimeout(n,function(){var e;e=r.auto_focus===!0?n:n.editorManager.get(r.auto_focus),e.destroyed||e.focus()},100),s=p=h=null},focus:function(e){function t(e){return n.dom.getParent(e,function(e){return"true"===n.dom.getContentEditable(e)})}var n=this,r=n.selection,i=n.settings.content_editable,o,a,s=n.getDoc(),l=n.getBody(),u;if(!e){if(o=r.getRng(),o.item&&(a=o.item(0)),n.quirks.refreshContentEditable(),u=t(r.getNode()),n.$.contains(l,u))return u.focus(),r.normalize(),void n.editorManager.setActive(n);if(i||(w.opera||n.getBody().focus(),n.getWin().focus()),$||i){if(l.setActive)try{l.setActive()}catch(c){l.focus()}else l.focus();i&&r.normalize()}a&&a.ownerDocument==s&&(o=s.body.createControlRange(),o.addElement(a),o.select())}n.editorManager.setActive(n)},execCallback:function(e){var t=this,n=t.settings[e],r;if(n)return t.callbackLookup&&(r=t.callbackLookup[e])&&(n=r.func,r=r.scope),"string"==typeof n&&(r=n.replace(/\.\w+$/,""),r=r?W(r):0,n=W(n),t.callbackLookup=t.callbackLookup||{},t.callbackLookup[e]={func:n,scope:r}),n.apply(r||t,Array.prototype.slice.call(arguments,1))},translate:function(e){var t=this.settings.language||"en",n=this.editorManager.i18n;return e?(e=n.data[t+"."+e]||e.replace(/\{\#([^\}]+)\}/g,function(e,r){return n.data[t+"."+r]||"{#"+r+"}"}),this.editorManager.translate(e)):""},getLang:function(e,n){return this.editorManager.i18n.data[(this.settings.language||"en")+"."+e]||(n!==t?n:"{#"+e+"}")},getParam:function(e,t,n){var r=e in this.settings?this.settings[e]:t,i;return"hash"===n?(i={},"string"==typeof r?I(r.indexOf("=")>0?r.split(/[;,](?![^=;,]*(?:[;,]|$))/):r.split(","),function(e){e=e.split("="),e.length>1?i[U(e[0])]=U(e[1]):i[U(e[0])]=U(e)}):i=r,i):r},nodeChanged:function(e){this._nodeChangeDispatcher.nodeChanged(e)},addButton:function(e,t){var n=this;t.cmd&&(t.onclick=function(){n.execCommand(t.cmd)}),t.text||t.icon||(t.icon=e),n.buttons=n.buttons||{},t.tooltip=t.tooltip||t.title,n.buttons[e]=t},addSidebar:function(e,t){return B.add(this,e,t)},addMenuItem:function(e,t){var n=this;t.cmd&&(t.onclick=function(){n.execCommand(t.cmd)}),n.menuItems=n.menuItems||{},n.menuItems[e]=t},addContextToolbar:function(e,t){var n=this,r;n.contextToolbars=n.contextToolbars||[],"string"==typeof e&&(r=e,e=function(e){return n.dom.is(e,r)}),n.contextToolbars.push({id:A.uuid("mcet"),predicate:e,items:t})},addCommand:function(e,t,n){this.editorCommands.addCommand(e,t,n)},addQueryStateHandler:function(e,t,n){this.editorCommands.addQueryStateHandler(e,t,n)},addQueryValueHandler:function(e,t,n){this.editorCommands.addQueryValueHandler(e,t,n)},addShortcut:function(e,t,n,r){this.shortcuts.add(e,t,n,r)},execCommand:function(e,t,n,r){return this.editorCommands.execCommand(e,t,n,r)},queryCommandState:function(e){return this.editorCommands.queryCommandState(e)},queryCommandValue:function(e){return this.editorCommands.queryCommandValue(e)},queryCommandSupported:function(e){return this.editorCommands.queryCommandSupported(e)},show:function(){var e=this;e.hidden&&(e.hidden=!1,e.inline?e.getBody().contentEditable=!0:(M.show(e.getContainer()),M.hide(e.id)),e.load(),e.fire("show"))},hide:function(){var e=this,t=e.getDoc();e.hidden||(q&&t&&!e.inline&&t.execCommand("SelectAll"),e.save(),e.inline?(e.getBody().contentEditable=!1,e==e.editorManager.focusedEditor&&(e.editorManager.focusedEditor=null)):(M.hide(e.getContainer()),M.setStyle(e.id,"display",e.orgDisplay)),e.hidden=!0,e.fire("hide"))},isHidden:function(){return!!this.hidden},setProgressState:function(e,t){this.fire("ProgressState",{state:e,time:t})},load:function(e){var n=this,r=n.getElement(),i;if(r)return e=e||{},e.load=!0,i=n.setContent(r.value!==t?r.value:r.innerHTML,e),e.element=r,e.no_events||n.fire("LoadContent",e),e.element=r=null,i},save:function(e){var t=this,n=t.getElement(),r,i;if(n&&t.initialized)return e=e||{},e.save=!0,e.element=n,r=e.content=t.getContent(e),e.no_events||t.fire("SaveContent",e),"raw"==e.format&&t.fire("RawSaveContent",e),r=e.content,/TEXTAREA|INPUT/i.test(n.nodeName)?n.value=r:(t.inline||(n.innerHTML=r),(i=M.getParent(t.id,"form"))&&I(i.elements,function(e){if(e.name==t.id)return e.value=r,!1})),e.element=n=null,e.set_dirty!==!1&&t.setDirty(!1),r},setContent:function(e,t){var n=this,r=n.getBody(),i,o;return t=t||{},t.format=t.format||"html",t.set=!0,t.content=e,t.no_events||n.fire("BeforeSetContent",t),e=t.content,0===e.length||/^\s+$/.test(e)?(o=q&&q<11?"":'
      ',"TABLE"==r.nodeName?e=""+o+"":/^(UL|OL)$/.test(r.nodeName)&&(e="
    • "+o+"
    • "),i=n.settings.forced_root_block,i&&n.schema.isValidChild(r.nodeName.toLowerCase(),i.toLowerCase())?(e=o,e=n.dom.createHTML(i,n.settings.forced_root_block_attrs,e)):q||e||(e='
      '),n.dom.setHTML(r,e),n.fire("SetContent",t)):("raw"!==t.format&&(e=new s({validate:n.validate},n.schema).serialize(n.parser.parse(e,{isRootContent:!0}))),t.content=U(e),n.dom.setHTML(r,t.content),t.no_events||n.fire("SetContent",t)),t.content},getContent:function(e){var t=this,n,r=t.getBody();return e=e||{},e.format=e.format||"html",e.get=!0,e.getInner=!0,e.no_events||t.fire("BeforeGetContent",e),n="raw"==e.format?t.serializer.getTrimmedContent():"text"==e.format?r.innerText||r.textContent:t.serializer.serialize(r,e),"text"!=e.format?e.content=U(n):e.content=n,e.no_events||t.fire("GetContent",e),e.content},insertContent:function(e,t){t&&(e=H({content:e},t)),this.execCommand("mceInsertContent",!1,e)},isDirty:function(){return!this.isNotDirty},setDirty:function(e){var t=!this.isNotDirty;this.isNotDirty=!e,e&&e!=t&&this.fire("dirty")},setMode:function(e){S.setMode(this,e)},getContainer:function(){var e=this;return e.container||(e.container=M.get(e.editorContainer||e.id+"_parent")),e.container},getContentAreaContainer:function(){return this.contentAreaContainer},getElement:function(){return this.targetElm||(this.targetElm=M.get(this.id)),this.targetElm},getWin:function(){var e=this,t;return e.contentWindow||(t=e.iframeElement,t&&(e.contentWindow=t.contentWindow)),e.contentWindow},getDoc:function(){var e=this,t;return e.contentDocument||(t=e.getWin(),t&&(e.contentDocument=t.document)),e.contentDocument},getBody:function(){var e=this.getDoc();return this.bodyElement||(e?e.body:null)},convertURL:function(e,t,n){var r=this,i=r.settings;return i.urlconverter_callback?r.execCallback("urlconverter_callback",e,n,!0,t):!i.convert_urls||n&&"LINK"==n.nodeName||0===e.indexOf("file:")||0===e.length?e:i.relative_urls?r.documentBaseURI.toRelative(e):e=r.documentBaseURI.toAbsolute(e,i.remove_script_host)},addVisual:function(e){var n=this,r=n.settings,i=n.dom,o;e=e||n.getBody(),n.hasVisual===t&&(n.hasVisual=r.visual),I(i.select("table,a",e),function(e){var t;switch(e.nodeName){case"TABLE":return o=r.visual_table_class||"mce-item-table",t=i.getAttrib(e,"border"),void(t&&"0"!=t||!n.hasVisual?i.removeClass(e,o):i.addClass(e,o));case"A":return void(i.getAttrib(e,"href",!1)||(t=i.getAttrib(e,"name")||e.id,o=r.visual_anchor_class||"mce-item-anchor",t&&n.hasVisual?i.addClass(e,o):i.removeClass(e,o)))}}),n.fire("VisualAid",{element:e,hasVisual:n.hasVisual})},remove:function(){var e=this;e.removed||(e.save(),e.removed=1,e.unbindAllNativeEvents(),e.hasHiddenInput&&M.remove(e.getElement().nextSibling),e.inline||(q&&q<10&&e.getDoc().execCommand("SelectAll",!1,null),M.setStyle(e.id,"display",e.orgDisplay),e.getBody().onload=null),e.fire("remove"),e.editorManager.remove(e),M.remove(e.getContainer()),e._selectionOverrides.destroy(),e.editorUpload.destroy(),e.destroy())},destroy:function(e){var t=this,n;if(!t.destroyed){if(!e&&!t.removed)return void t.remove();e||(t.editorManager.off("beforeunload",t._beforeUnload),t.theme&&t.theme.destroy&&t.theme.destroy(),t.selection.destroy(),t.dom.destroy()),n=t.formElement,n&&(n._mceOldSubmit&&(n.submit=n._mceOldSubmit,n._mceOldSubmit=null),M.unbind(n,"submit reset",t.formEventDelegate)),t.contentAreaContainer=t.formElement=t.container=t.editorContainer=null,t.bodyElement=t.contentDocument=t.contentWindow=null,t.iframeElement=t.targetElm=null,t.selection&&(t.selection=t.selection.win=t.selection.dom=t.selection.dom.doc=null),t.destroyed=1}},uploadImages:function(e){return this.editorUpload.uploadImages(e)},_scanForImages:function(){return this.editorUpload.scanForImages()}},H(L.prototype,_),L}),r(st,[m],function(e){var t={},n="en";return{setCode:function(e){e&&(n=e,this.rtl=!!this.data[e]&&"rtl"===this.data[e]._dir)},getCode:function(){return n},rtl:!1,add:function(e,n){var r=t[e];r||(t[e]=r={});for(var i in n)r[i]=n[i];this.setCode(e)},translate:function(r){function i(t){return e.is(t,"function")?Object.prototype.toString.call(t):o(t)?"":""+t}function o(t){return""===t||null===t||e.is(t,"undefined")}function a(t){return t=i(t),e.hasOwn(s,t)?i(s[t]):t}var s=t[n]||{};if(o(r))return"";if(e.is(r,"object")&&e.hasOwn(r,"raw"))return i(r.raw);if(e.is(r,"array")){var l=r.slice(1);r=a(r[0]).replace(/\{([0-9]+)\}/g,function(t,n){return e.hasOwn(l,n)?i(l[n]):t})}return a(r).replace(/{context:\w+}$/,"")},data:t}}),r(lt,[w,c,d],function(e,t,n){function r(e){function l(){try{return document.activeElement}catch(e){return document.body}}function u(e,t){if(t&&t.startContainer){if(!e.isChildOf(t.startContainer,e.getRoot())||!e.isChildOf(t.endContainer,e.getRoot()))return;return{startContainer:t.startContainer,startOffset:t.startOffset,endContainer:t.endContainer,endOffset:t.endOffset}}return t}function c(e,t){var n;return t.startContainer?(n=e.getDoc().createRange(),n.setStart(t.startContainer,t.startOffset),n.setEnd(t.endContainer,t.endOffset)):n=t,n}function d(e){return!!s.getParent(e,r.isEditorUIElement)}function f(r){var f=r.editor;f.on("init",function(){(f.inline||n.ie)&&("onbeforedeactivate"in document&&n.ie<9?f.dom.bind(f.getBody(),"beforedeactivate",function(e){if(e.target==f.getBody())try{f.lastRng=f.selection.getRng()}catch(t){}}):f.on("nodechange mouseup keyup",function(e){var t=l();"nodechange"==e.type&&e.selectionChange||(t&&t.id==f.id+"_ifr"&&(t=f.getBody()),f.dom.isChildOf(t,f.getBody())&&(f.lastRng=f.selection.getRng()))}),n.webkit&&!i&&(i=function(){var t=e.activeEditor;if(t&&t.selection){var n=t.selection.getRng();n&&!n.collapsed&&(f.lastRng=n)}},s.bind(document,"selectionchange",i)))}),f.on("setcontent",function(){f.lastRng=null}),f.on("mousedown",function(){f.selection.lastFocusBookmark=null}),f.on("focusin",function(){var t=e.focusedEditor,n;f.selection.lastFocusBookmark&&(n=c(f,f.selection.lastFocusBookmark),f.selection.lastFocusBookmark=null,f.selection.setRng(n)),t!=f&&(t&&t.fire("blur",{focusedEditor:f}),e.setActive(f),e.focusedEditor=f,f.fire("focus",{blurredEditor:t}),f.focus(!0)),f.lastRng=null}),f.on("focusout",function(){t.setEditorTimeout(f,function(){var t=e.focusedEditor;d(l())||t!=f||(f.fire("blur",{focusedEditor:null}),e.focusedEditor=null,f.selection&&(f.selection.lastFocusBookmark=null))})}),o||(o=function(t){var n=e.activeEditor,r;r=t.target,n&&r.ownerDocument==document&&(n.selection&&r!=n.getBody()&&(n.selection.lastFocusBookmark=u(n.dom,n.lastRng)),r==document.body||d(r)||e.focusedEditor!=n||(n.fire("blur",{focusedEditor:null}),e.focusedEditor=null))},s.bind(document,"focusin",o)),f.inline&&!a&&(a=function(t){var n=e.activeEditor,r=n.dom;if(n.inline&&r&&!r.isChildOf(t.target,n.getBody())){var i=n.selection.getRng();i.collapsed||(n.lastRng=i)}},s.bind(document,"mouseup",a))}function p(t){e.focusedEditor==t.editor&&(e.focusedEditor=null),e.activeEditor||(s.unbind(document,"selectionchange",i),s.unbind(document,"focusin",o),s.unbind(document,"mouseup",a),i=o=a=null)}e.on("AddEditor",f),e.on("RemoveEditor",p)}var i,o,a,s=e.DOM;return r.isEditorUIElement=function(e){return e.className.toString().indexOf("mce-")!==-1},r}),r(ut,[at,g,w,ue,d,m,u,pe,st,lt,N],function(e,t,n,r,i,o,a,s,l,u,c){function d(e){v(x.editors,function(t){"scroll"===e.type?t.fire("ScrollWindow",e):t.fire("ResizeWindow",e)})}function f(e,n){n!==w&&(n?t(window).on("resize scroll",d):t(window).off("resize scroll",d),w=n)}function p(e){var t=x.editors,n;delete t[e.id];for(var r=0;r0&&v(g(t),function(e){var t;(t=m.get(e))?n.push(t):v(document.forms,function(t){v(t.elements,function(t){t.name===e&&(e="mce_editor_"+b++,m.setAttrib(t,"id",e),n.push(t))})})});break;case"textareas":case"specific_textareas":v(m.select("textarea"),function(t){e.editor_deselector&&u(t,e.editor_deselector)||e.editor_selector&&!u(t,e.editor_selector)||n.push(t)})}return n}function d(){function a(t,n,r){var i=new e(t,n,f);p.push(i),i.on("init",function(){++u===g.length&&x(p)}),i.targetElm=i.targetElm||r,i.render()}var u=0,p=[],g;return m.unbind(window,"ready",d),l("onpageload"),g=t.unique(c(n)),n.types?void v(n.types,function(e){o.each(g,function(t){return!m.is(t,e.selector)||(a(s(t),y({},n,e),t),!1)})}):(o.each(g,function(e){h(f.get(e.id))}),g=o.grep(g,function(e){return!f.get(e.id)}),void v(g,function(e){r(n,e)?i("Could not initialize inline editor on invalid inline target element",e):a(s(e),n,e)}))}var f=this,p,C;C=o.makeMap("area base basefont br col frame hr img input isindex link meta param embed source wbr track colgroup option tbody tfoot thead tr script noscript style textarea video audio iframe object menu"," ");var x=function(e){p=e};return f.settings=n,m.bind(window,"ready",d),new a(function(e){p?e(p):x=function(t){e(t)}})},get:function(e){return arguments.length?e in this.editors?this.editors[e]:null:this.editors},add:function(e){var t=this,n=t.editors;return n[e.id]=e,n.push(e),f(n,!0),t.activeEditor=e,t.fire("AddEditor",{editor:e}),C||(C=function(){t.fire("BeforeUnload")},m.bind(window,"beforeunload",C)),e},createEditor:function(t,n){return this.add(new e(t,n,this))},remove:function(e){var t=this,n,r=t.editors,i;{if(e)return"string"==typeof e?(e=e.selector||e,void v(m.select(e),function(e){i=r[e.id],i&&t.remove(i)})):(i=e,r[i.id]?(p(i)&&t.fire("RemoveEditor",{editor:i}),r.length||m.unbind(window,"beforeunload",C),i.remove(),f(r,r.length>0),i):null);for(n=r.length-1;n>=0;n--)t.remove(r[n])}},execCommand:function(t,n,r){var i=this,o=i.get(r);switch(t){case"mceAddEditor":return i.get(r)||new e(r,i.settings,i).render(),!0;case"mceRemoveEditor":return o&&o.remove(),!0;case"mceToggleEditor":return o?(o.isHidden()?o.show():o.hide(),!0):(i.execCommand("mceAddEditor",0,r),!0)}return!!i.activeEditor&&i.activeEditor.execCommand(t,n,r)},triggerSave:function(){v(this.editors,function(e){e.save()})},addI18n:function(e,t){l.add(e,t)},translate:function(e){return l.translate(e)},setActive:function(e){var t=this.activeEditor;this.activeEditor!=e&&(t&&t.fire("deactivate",{relatedTarget:e}),e.fire("activate",{relatedTarget:t})),this.activeEditor=e}},y(x,s),x.setup(),window.tinymce=window.tinyMCE=x,x}),r(ct,[ut,m],function(e,t){var n=t.each,r=t.explode;e.on("AddEditor",function(e){var t=e.editor;t.on("preInit",function(){function e(e,t){n(t,function(t,n){t&&s.setStyle(e,n,t)}),s.rename(e,"span")}function i(e){s=t.dom,l.convert_fonts_to_spans&&n(s.select("font,u,strike",e.node),function(e){o[e.nodeName.toLowerCase()](s,e)})}var o,a,s,l=t.settings;l.inline_styles&&(a=r(l.font_size_legacy_values),o={font:function(t,n){e(n,{backgroundColor:n.style.backgroundColor,color:n.color,fontFamily:n.face,fontSize:a[parseInt(n.size,10)-1]})},u:function(n,r){"html4"===t.settings.schema&&e(r,{textDecoration:"underline"})},strike:function(t,n){e(n,{textDecoration:"line-through"})}},t.on("PreProcess SetContent",i))})})}),r(dt,[pe,m],function(e,t){var n={send:function(e){function r(){!e.async||4==i.readyState||o++>1e4?(e.success&&o<1e4&&200==i.status?e.success.call(e.success_scope,""+i.responseText,i,e):e.error&&e.error.call(e.error_scope,o>1e4?"TIMED_OUT":"GENERAL",i,e),i=null):setTimeout(r,10)}var i,o=0;if(e.scope=e.scope||this,e.success_scope=e.success_scope||e.scope,e.error_scope=e.error_scope||e.scope,e.async=e.async!==!1,e.data=e.data||"",n.fire("beforeInitialize",{settings:e}),i=new XMLHttpRequest){if(i.overrideMimeType&&i.overrideMimeType(e.content_type),i.open(e.type||(e.data?"POST":"GET"),e.url,e.async),e.crossDomain&&(i.withCredentials=!0),e.content_type&&i.setRequestHeader("Content-Type",e.content_type),e.requestheaders&&t.each(e.requestheaders,function(e){i.setRequestHeader(e.key,e.value)}),i.setRequestHeader("X-Requested-With","XMLHttpRequest"),i=n.fire("beforeSend",{xhr:i,settings:e}).xhr,i.send(e.data),!e.async)return r();setTimeout(r,10)}}};return t.extend(n,e),n}),r(ft,[],function(){function e(t,n){var r,i,o,a;if(n=n||'"',null===t)return"null";if(o=typeof t,"string"==o)return i="\bb\tt\nn\ff\rr\"\"''\\\\",n+t.replace(/([\u0080-\uFFFF\x00-\x1f\"\'\\])/g,function(e,t){return'"'===n&&"'"===e?e:(r=i.indexOf(t),r+1?"\\"+i.charAt(r+1):(e=t.charCodeAt().toString(16),"\\u"+"0000".substring(e.length)+e))})+n;if("object"==o){if(t.hasOwnProperty&&"[object Array]"===Object.prototype.toString.call(t)){for(r=0,i="[";r0?",":"")+e(t[r],n);return i+"]"}i="{";for(a in t)t.hasOwnProperty(a)&&(i+="function"!=typeof t[a]?(i.length>1?","+n:n)+a+n+":"+e(t[a],n):"");return i+"}"}return""+t}return{serialize:e,parse:function(e){try{return window[String.fromCharCode(101)+"val"]("("+e+")")}catch(t){}}}}),r(pt,[ft,dt,m],function(e,t,n){function r(e){this.settings=i({},e),this.count=0}var i=n.extend;return r.sendRPC=function(e){return(new r).send(e)},r.prototype={send:function(n){var r=n.error,o=n.success;n=i(this.settings,n),n.success=function(t,i){t=e.parse(t),"undefined"==typeof t&&(t={error:"JSON Parse error."}),t.error?r.call(n.error_scope||n.scope,t.error,i):o.call(n.success_scope||n.scope,t.result)},n.error=function(e,t){r&&r.call(n.error_scope||n.scope,e,t)},n.data=e.serialize({id:n.id||"c"+this.count++,method:n.method,params:n.params}),n.content_type="application/json",t.send(n)}},r}),r(ht,[w],function(e){return{callbacks:{},count:0,send:function(n){var r=this,i=e.DOM,o=n.count!==t?n.count:r.count,a="tinymce_jsonp_"+o;r.callbacks[o]=function(e){i.remove(a),delete r.callbacks[o],n.callback(e)},i.add(i.doc.body,"script",{id:a,src:n.url,type:"text/javascript"}),r.count++}}}),r(mt,[],function(){function e(){s=[];for(var e in a)s.push(e);i.length=s.length}function n(){function n(e){var n,r;return r=e!==t?c+e:i.indexOf(",",c),r===-1||r>i.length?null:(n=i.substring(c,r),c=r+1,n)}var r,i,s,c=0;if(a={},u){o.load(l),i=o.getAttribute(l)||"";do{var d=n();if(null===d)break;if(r=n(parseInt(d,32)||0),null!==r){if(d=n(),null===d)break;s=n(parseInt(d,32)||0),r&&(a[r]=s)}}while(null!==r);e()}}function r(){var t,n="";if(u){for(var r in a)t=a[r],n+=(n?",":"")+r.length.toString(32)+","+r+","+t.length.toString(32)+","+t;o.setAttribute(l,n);try{o.save(l)}catch(i){}e()}}var i,o,a,s,l,u;try{if(window.localStorage)return localStorage}catch(c){}return l="tinymce",o=document.documentElement,u=!!o.addBehavior,u&&o.addBehavior("#default#userData"),i={key:function(e){return s[e]},getItem:function(e){return e in a?a[e]:null},setItem:function(e,t){a[e]=""+t,r()},removeItem:function(e){delete a[e],r()},clear:function(){a={},r()}},n(),i}),r(gt,[w,f,E,N,m,d],function(e,t,n,r,i,o){var a=window.tinymce;return a.DOM=e.DOM,a.ScriptLoader=n.ScriptLoader,a.PluginManager=r.PluginManager,a.ThemeManager=r.ThemeManager,a.dom=a.dom||{},a.dom.Event=t.Event,i.each("trim isArray is toArray makeMap each map grep inArray extend create walk createNS resolve explode _addCacheSuffix".split(" "),function(e){a[e]=i[e]}),i.each("isOpera isWebKit isIE isGecko isMac".split(" "),function(e){a[e]=o[e.substr(2).toLowerCase()]}),{}}),r(vt,[ce,m],function(e,t){return e.extend({Defaults:{firstControlClass:"first",lastControlClass:"last"},init:function(e){this.settings=t.extend({},this.Defaults,e)},preRender:function(e){e.bodyClasses.add(this.settings.containerClass)},applyClasses:function(e){var t=this,n=t.settings,r,i,o,a;r=n.firstControlClass,i=n.lastControlClass,e.each(function(e){e.classes.remove(r).remove(i).add(n.controlClass),e.visible()&&(o||(o=e),a=e)}),o&&o.classes.add(r),a&&a.classes.add(i)},renderHtml:function(e){var t=this,n="";return t.applyClasses(e.items()),e.items().each(function(e){n+=e.renderHtml()}),n},recalc:function(){},postRender:function(){},isNative:function(){return!1}})}),r(yt,[vt],function(e){return e.extend({Defaults:{containerClass:"abs-layout",controlClass:"abs-layout-item"},recalc:function(e){e.items().filter(":visible").each(function(e){var t=e.settings;e.layoutRect({x:t.x,y:t.y,w:t.w,h:t.h}),e.recalc&&e.recalc()})},renderHtml:function(e){return'
      '+this._super(e)}})}),r(bt,[Pe],function(e){return e.extend({Defaults:{classes:"widget btn",role:"button"},init:function(e){var t=this,n;t._super(e),e=t.settings,n=t.settings.size,t.on("click mousedown",function(e){e.preventDefault()}),t.on("touchstart",function(e){t.fire("click",e),e.preventDefault()}),e.subtype&&t.classes.add(e.subtype),n&&t.classes.add("btn-"+n),e.icon&&t.icon(e.icon)},icon:function(e){return arguments.length?(this.state.set("icon",e),this):this.state.get("icon")},repaint:function(){var e=this.getEl().firstChild,t;e&&(t=e.style,t.width=t.height="100%"),this._super()},renderHtml:function(){var e=this,t=e._id,n=e.classPrefix,r=e.state.get("icon"),i,o=e.state.get("text"),a="";return i=e.settings.image,i?(r="none","string"!=typeof i&&(i=window.getSelection?i[0]:i[1]),i=" style=\"background-image: url('"+i+"')\""):i="",o&&(e.classes.add("btn-has-text"),a=''+e.encode(o)+""),r=r?n+"ico "+n+"i-"+r:"",'
      "},bindStates:function(){function e(e){var i=n("span."+r,t.getEl());e?(i[0]||(n("button:first",t.getEl()).append(''),i=n("span."+r,t.getEl())),i.html(t.encode(e))):i.remove(),t.classes.toggle("btn-has-text",!!e)}var t=this,n=t.$,r=t.classPrefix+"txt";return t.state.on("change:text",function(t){e(t.value)}),t.state.on("change:icon",function(n){var r=n.value,i=t.classPrefix;t.settings.icon=r,r=r?i+"ico "+i+"i-"+t.settings.icon:"";var o=t.getEl().firstChild,a=o.getElementsByTagName("i")[0];r?(a&&a==o.firstChild||(a=document.createElement("i"),o.insertBefore(a,o.firstChild)),a.className=r):a&&o.removeChild(a),e(t.state.get("text"))}),t._super()}})}),r(Ct,[Ne],function(e){return e.extend({Defaults:{defaultType:"button",role:"group"},renderHtml:function(){var e=this,t=e._layout;return e.classes.add("btn-group"),e.preRender(),t.preRender(e),'
      '+(e.settings.html||"")+t.renderHtml(e)+"
      "}})}),r(xt,[Pe],function(e){return e.extend({Defaults:{classes:"checkbox",role:"checkbox",checked:!1},init:function(e){var t=this;t._super(e),t.on("click mousedown",function(e){e.preventDefault()}),t.on("click",function(e){e.preventDefault(),t.disabled()||t.checked(!t.checked())}),t.checked(t.settings.checked)},checked:function(e){return arguments.length?(this.state.set("checked",e),this):this.state.get("checked")},value:function(e){return arguments.length?this.checked(e):this.checked()},renderHtml:function(){var e=this,t=e._id,n=e.classPrefix;return'
      '+e.encode(e.state.get("text"))+"
      "},bindStates:function(){function e(e){t.classes.toggle("checked",e),t.aria("checked",e)}var t=this;return t.state.on("change:text",function(e){t.getEl("al").firstChild.data=t.translate(e.value)}),t.state.on("change:checked change:value",function(n){t.fire("change"),e(n.value)}),t.state.on("change:icon",function(e){var n=e.value,r=t.classPrefix;if("undefined"==typeof n)return t.settings.icon;t.settings.icon=n,n=n?r+"ico "+r+"i-"+t.settings.icon:"";var i=t.getEl().firstChild,o=i.getElementsByTagName("i")[0];n?(o&&o==i.firstChild||(o=document.createElement("i"),i.insertBefore(o,i.firstChild)),o.className=n):o&&i.removeChild(o)}),t.state.get("checked")&&e(!0),t._super()}})}),r(wt,[Pe,we,ve,g,I,m],function(e,t,n,r,i,o){return e.extend({init:function(e){var t=this;t._super(e),e=t.settings,t.classes.add("combobox"),t.subinput=!0,t.ariaTarget="inp",e.menu=e.menu||e.values,e.menu&&(e.icon="caret"),t.on("click",function(n){var i=n.target,o=t.getEl();if(r.contains(o,i)||i==o)for(;i&&i!=o;)i.id&&i.id.indexOf("-open")!=-1&&(t.fire("action"),e.menu&&(t.showMenu(),n.aria&&t.menu.items()[0].focus())),i=i.parentNode}),t.on("keydown",function(e){var n;13==e.keyCode&&"INPUT"===e.target.nodeName&&(e.preventDefault(),t.parents().reverse().each(function(e){if(e.toJSON)return n=e,!1}),t.fire("submit",{data:n.toJSON()}))}),t.on("keyup",function(e){if("INPUT"==e.target.nodeName){var n=t.state.get("value"),r=e.target.value;r!==n&&(t.state.set("value",r),t.fire("autocomplete",e))}}),t.on("mouseover",function(e){var n=t.tooltip().moveTo(-65535);if(t.statusLevel()&&e.target.className.indexOf(t.classPrefix+"status")!==-1){var r=t.statusMessage()||"Ok",i=n.text(r).show().testMoveRel(e.target,["bc-tc","bc-tl","bc-tr"]);n.classes.toggle("tooltip-n","bc-tc"==i),n.classes.toggle("tooltip-nw","bc-tl"==i),n.classes.toggle("tooltip-ne","bc-tr"==i),n.moveRel(e.target,i)}})},statusLevel:function(e){return arguments.length>0&&this.state.set("statusLevel",e),this.state.get("statusLevel")},statusMessage:function(e){return arguments.length>0&&this.state.set("statusMessage",e),this.state.get("statusMessage")},showMenu:function(){var e=this,n=e.settings,r;e.menu||(r=n.menu||[],r.length?r={type:"menu",items:r}:r.type=r.type||"menu",e.menu=t.create(r).parent(e).renderTo(e.getContainerElm()),e.fire("createmenu"),e.menu.reflow(),e.menu.on("cancel",function(t){t.control===e.menu&&e.focus()}),e.menu.on("show hide",function(t){t.control.items().each(function(t){t.active(t.value()==e.value())})}).fire("show"),e.menu.on("select",function(t){e.value(t.control.value())}),e.on("focusin",function(t){"INPUT"==t.target.tagName.toUpperCase()&&e.menu.hide()}),e.aria("expanded",!0)),e.menu.show(),e.menu.layoutRect({w:e.layoutRect().w}),e.menu.moveRel(e.getEl(),e.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"])},focus:function(){this.getEl("inp").focus()},repaint:function(){var e=this,t=e.getEl(),i=e.getEl("open"),o=e.layoutRect(),a,s,l=0,u=t.firstChild;e.statusLevel()&&"none"!==e.statusLevel()&&(l=parseInt(n.getRuntimeStyle(u,"padding-right"),10)-parseInt(n.getRuntimeStyle(u,"padding-left"),10)),a=i?o.w-n.getSize(i).width-10:o.w-10;var c=document;return c.all&&(!c.documentMode||c.documentMode<=8)&&(s=e.layoutRect().h-2+"px"),r(u).css({width:a-l,lineHeight:s}),e._super(),e},postRender:function(){var e=this;return r(this.getEl("inp")).on("change",function(t){e.state.set("value",t.target.value),e.fire("change",t)}),e._super()},renderHtml:function(){var e=this,t=e._id,n=e.settings,r=e.classPrefix,i=e.state.get("value")||"",o,a,s="",l="",u="";return"spellcheck"in n&&(l+=' spellcheck="'+n.spellcheck+'"'),n.maxLength&&(l+=' maxlength="'+n.maxLength+'"'),n.size&&(l+=' size="'+n.size+'"'),n.subtype&&(l+=' type="'+n.subtype+'"'),u='',e.disabled()&&(l+=' disabled="disabled"'),o=n.icon,o&&"caret"!=o&&(o=r+"ico "+r+"i-"+n.icon),a=e.state.get("text"),(o||a)&&(s='
      ",e.classes.add("has-open")),'
      '+u+s+"
      "},value:function(e){return arguments.length?(this.state.set("value",e),this):(this.state.get("rendered")&&this.state.set("value",this.getEl("inp").value),this.state.get("value"))},showAutoComplete:function(e,n){var r=this;if(0===e.length)return void r.hideMenu();var i=function(e,t){return function(){r.fire("selectitem",{title:t,value:e})}};r.menu?r.menu.items().remove():r.menu=t.create({type:"menu",classes:"combobox-menu",layout:"flow"}).parent(r).renderTo(),o.each(e,function(e){r.menu.add({text:e.title,url:e.previewUrl,match:n,classes:"menu-item-ellipsis",onclick:i(e.value,e.title)})}),r.menu.renderNew(),r.hideMenu(),r.menu.on("cancel",function(e){e.control.parent()===r.menu&&(e.stopPropagation(),r.focus(),r.hideMenu())}),r.menu.on("select",function(){r.focus()});var a=r.layoutRect().w;r.menu.layoutRect({w:a,minW:0,maxW:a}),r.menu.reflow(),r.menu.show(),r.menu.moveRel(r.getEl(),r.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"])},hideMenu:function(){this.menu&&this.menu.hide()},bindStates:function(){var e=this;e.state.on("change:value",function(t){e.getEl("inp").value!=t.value&&(e.getEl("inp").value=t.value)}),e.state.on("change:disabled",function(t){e.getEl("inp").disabled=t.value}),e.state.on("change:statusLevel",function(t){var r=e.getEl("status"),i=e.classPrefix,o=t.value;n.css(r,"display","none"===o?"none":""),n.toggleClass(r,i+"i-checkmark","ok"===o),n.toggleClass(r,i+"i-warning","warn"===o),n.toggleClass(r,i+"i-error","error"===o),e.classes.toggle("has-status","none"!==o),e.repaint()}),n.on(e.getEl("status"),"mouseleave",function(){e.tooltip().hide()}),e.on("cancel",function(t){e.menu&&e.menu.visible()&&(t.stopPropagation(),e.hideMenu())});var t=function(e,t){t&&t.items().length>0&&t.items().eq(e)[0].focus()};return e.on("keydown",function(n){ +var r=n.keyCode;"INPUT"===n.target.nodeName&&(r===i.DOWN?(n.preventDefault(),e.fire("autocomplete"),t(0,e.menu)):r===i.UP&&(n.preventDefault(),t(-1,e.menu)))}),e._super()},remove:function(){r(this.getEl("inp")).off(),this.menu&&this.menu.remove(),this._super()}})}),r(Et,[wt],function(e){return e.extend({init:function(e){var t=this;e.spellcheck=!1,e.onaction&&(e.icon="none"),t._super(e),t.classes.add("colorbox"),t.on("change keyup postrender",function(){t.repaintColor(t.value())})},repaintColor:function(e){var t=this.getEl("open"),n=t?t.getElementsByTagName("i")[0]:null;if(n)try{n.style.background=e}catch(r){}},bindStates:function(){var e=this;return e.state.on("change:value",function(t){e.state.get("rendered")&&e.repaintColor(t.value)}),e._super()}})}),r(Nt,[bt,Ae],function(e,t){return e.extend({showPanel:function(){var e=this,n=e.settings;if(e.active(!0),e.panel)e.panel.show();else{var r=n.panel;r.type&&(r={layout:"grid",items:r}),r.role=r.role||"dialog",r.popover=!0,r.autohide=!0,r.ariaRoot=!0,e.panel=new t(r).on("hide",function(){e.active(!1)}).on("cancel",function(t){t.stopPropagation(),e.focus(),e.hidePanel()}).parent(e).renderTo(e.getContainerElm()),e.panel.fire("show"),e.panel.reflow()}e.panel.moveRel(e.getEl(),n.popoverAlign||(e.isRtl()?["bc-tr","bc-tc"]:["bc-tl","bc-tc"]))},hidePanel:function(){var e=this;e.panel&&e.panel.hide()},postRender:function(){var e=this;return e.aria("haspopup",!0),e.on("click",function(t){t.control===e&&(e.panel&&e.panel.visible()?e.hidePanel():(e.showPanel(),e.panel.focus(!!t.aria)))}),e._super()},remove:function(){return this.panel&&(this.panel.remove(),this.panel=null),this._super()}})}),r(_t,[Nt,w],function(e,t){var n=t.DOM;return e.extend({init:function(e){this._super(e),this.classes.add("colorbutton")},color:function(e){return e?(this._color=e,this.getEl("preview").style.backgroundColor=e,this):this._color},resetColor:function(){return this._color=null,this.getEl("preview").style.backgroundColor=null,this},renderHtml:function(){var e=this,t=e._id,n=e.classPrefix,r=e.state.get("text"),i=e.settings.icon?n+"ico "+n+"i-"+e.settings.icon:"",o=e.settings.image?" style=\"background-image: url('"+e.settings.image+"')\"":"",a="";return r&&(e.classes.add("btn-has-text"),a=''+e.encode(r)+""),'
      '},postRender:function(){var e=this,t=e.settings.onclick;return e.on("click",function(r){r.aria&&"down"==r.aria.key||r.control!=e||n.getParent(r.target,"."+e.classPrefix+"open")||(r.stopImmediatePropagation(),t.call(e,r))}),delete e.settings.onclick,e._super()}})}),r(St,[],function(){function e(e){function i(e,i,o){var a,s,l,u,c,d;return a=0,s=0,l=0,e/=255,i/=255,o/=255,c=t(e,t(i,o)),d=n(e,n(i,o)),c==d?(l=c,{h:0,s:0,v:100*l}):(u=e==c?i-o:o==c?e-i:o-e,a=e==c?3:o==c?1:5,a=60*(a-u/(d-c)),s=(d-c)/d,l=d,{h:r(a),s:r(100*s),v:r(100*l)})}function o(e,i,o){var a,s,l,u;if(e=(parseInt(e,10)||0)%360,i=parseInt(i,10)/100,o=parseInt(o,10)/100,i=n(0,t(i,1)),o=n(0,t(o,1)),0===i)return void(d=f=p=r(255*o));switch(a=e/60,s=o*i,l=s*(1-Math.abs(a%2-1)),u=o-s,Math.floor(a)){case 0:d=s,f=l,p=0;break;case 1:d=l,f=s,p=0;break;case 2:d=0,f=s,p=l;break;case 3:d=0,f=l,p=s;break;case 4:d=l,f=0,p=s;break;case 5:d=s,f=0,p=l;break;default:d=f=p=0}d=r(255*(d+u)),f=r(255*(f+u)),p=r(255*(p+u))}function a(){function e(e){return e=parseInt(e,10).toString(16),e.length>1?e:"0"+e}return"#"+e(d)+e(f)+e(p)}function s(){return{r:d,g:f,b:p}}function l(){return i(d,f,p)}function u(e){var t;return"object"==typeof e?"r"in e?(d=e.r,f=e.g,p=e.b):"v"in e&&o(e.h,e.s,e.v):(t=/rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)[^\)]*\)/gi.exec(e))?(d=parseInt(t[1],10),f=parseInt(t[2],10),p=parseInt(t[3],10)):(t=/#([0-F]{2})([0-F]{2})([0-F]{2})/gi.exec(e))?(d=parseInt(t[1],16),f=parseInt(t[2],16),p=parseInt(t[3],16)):(t=/#([0-F])([0-F])([0-F])/gi.exec(e))&&(d=parseInt(t[1]+t[1],16),f=parseInt(t[2]+t[2],16),p=parseInt(t[3]+t[3],16)),d=d<0?0:d>255?255:d,f=f<0?0:f>255?255:f,p=p<0?0:p>255?255:p,c}var c=this,d=0,f=0,p=0;e&&u(e),c.toRgb=s,c.toHsv=l,c.toHex=a,c.parse=u}var t=Math.min,n=Math.max,r=Math.round;return e}),r(kt,[Pe,_e,ve,St],function(e,t,n,r){return e.extend({Defaults:{classes:"widget colorpicker"},init:function(e){this._super(e)},postRender:function(){function e(e,t){var r=n.getPos(e),i,o;return i=t.pageX-r.x,o=t.pageY-r.y,i=Math.max(0,Math.min(i/e.clientWidth,1)),o=Math.max(0,Math.min(o/e.clientHeight,1)),{x:i,y:o}}function i(e,t){var i=(360-e.h)/360;n.css(d,{top:100*i+"%"}),t||n.css(p,{left:e.s+"%",top:100-e.v+"%"}),f.style.background=new r({s:100,v:100,h:e.h}).toHex(),s.color().parse({s:e.s,v:e.v,h:e.h})}function o(t){var n;n=e(f,t),u.s=100*n.x,u.v=100*(1-n.y),i(u),s.fire("change")}function a(t){var n;n=e(c,t),u=l.toHsv(),u.h=360*(1-n.y),i(u,!0),s.fire("change")}var s=this,l=s.color(),u,c,d,f,p;c=s.getEl("h"),d=s.getEl("hp"),f=s.getEl("sv"),p=s.getEl("svp"),s._repaint=function(){u=l.toHsv(),i(u)},s._super(),s._svdraghelper=new t(s._id+"-sv",{start:o,drag:o}),s._hdraghelper=new t(s._id+"-h",{start:a,drag:a}),s._repaint()},rgb:function(){return this.color().toRgb()},value:function(e){var t=this;return arguments.length?(t.color().parse(e),void(t._rendered&&t._repaint())):t.color().toHex()},color:function(){return this._color||(this._color=new r),this._color},renderHtml:function(){function e(){var e,t,n="",i,a;for(i="filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=",a=o.split(","),e=0,t=a.length-1;e
    ';return n}var t=this,n=t._id,r=t.classPrefix,i,o="#ff0000,#ff0080,#ff00ff,#8000ff,#0000ff,#0080ff,#00ffff,#00ff80,#00ff00,#80ff00,#ffff00,#ff8000,#ff0000",a="background: -ms-linear-gradient(top,"+o+");background: linear-gradient(to bottom,"+o+");";return i='
    '+e()+'
    ','
    '+i+"
    "}})}),r(Tt,[Pe],function(e){return e.extend({init:function(e){var t=this;e.delimiter||(e.delimiter="\xbb"),t._super(e),t.classes.add("path"),t.canFocus=!0,t.on("click",function(e){var n,r=e.target;(n=r.getAttribute("data-index"))&&t.fire("select",{value:t.row()[n],index:n})}),t.row(t.settings.row)},focus:function(){var e=this;return e.getEl().firstChild.focus(),e},row:function(e){return arguments.length?(this.state.set("row",e),this):this.state.get("row")},renderHtml:function(){var e=this;return'
    '+e._getDataPathHtml(e.state.get("row"))+"
    "},bindStates:function(){var e=this;return e.state.on("change:row",function(t){e.innerHtml(e._getDataPathHtml(t.value))}),e._super()},_getDataPathHtml:function(e){var t=this,n=e||[],r,i,o="",a=t.classPrefix;for(r=0,i=n.length;r0?'":"")+'
    '+n[r].name+"
    ";return o||(o='
    \xa0
    '),o}})}),r(Rt,[Tt],function(e){return e.extend({postRender:function(){function e(e){if(1===e.nodeType){if("BR"==e.nodeName||e.getAttribute("data-mce-bogus"))return!0;if("bookmark"===e.getAttribute("data-mce-type"))return!0}return!1}var t=this,n=t.settings.editor;return n.settings.elementpath!==!1&&(t.on("select",function(e){n.focus(),n.selection.select(this.row()[e.index].element),n.nodeChanged()}),n.on("nodeChange",function(r){for(var i=[],o=r.parents,a=o.length;a--;)if(1==o[a].nodeType&&!e(o[a])){var s=n.fire("ResolveName",{name:o[a].nodeName.toLowerCase(),target:o[a]});if(s.isDefaultPrevented()||i.push({name:s.name,element:o[a]}),s.isPropagationStopped())break}t.row(i)})),t._super()}})}),r(At,[Ne],function(e){return e.extend({Defaults:{layout:"flex",align:"center",defaults:{flex:1}},renderHtml:function(){var e=this,t=e._layout,n=e.classPrefix;return e.classes.add("formitem"),t.preRender(e),'
    '+(e.settings.title?'
    '+e.settings.title+"
    ":"")+'
    '+(e.settings.html||"")+t.renderHtml(e)+"
    "}})}),r(Bt,[Ne,At,m],function(e,t,n){return e.extend({Defaults:{containerCls:"form",layout:"flex",direction:"column",align:"stretch",flex:1,padding:20,labelGap:30,spacing:10,callbacks:{submit:function(){this.submit()}}},preRender:function(){var e=this,r=e.items();e.settings.formItemDefaults||(e.settings.formItemDefaults={layout:"flex",autoResize:"overflow",defaults:{flex:1}}),r.each(function(r){var i,o=r.settings.label;o&&(i=new t(n.extend({items:{type:"label",id:r._id+"-l",text:o,flex:0,forId:r._id,disabled:r.disabled()}},e.settings.formItemDefaults)),i.type="formitem",r.aria("labelledby",r._id+"-l"),"undefined"==typeof r.settings.flex&&(r.settings.flex=1),e.replace(r,i),i.add(r))})},submit:function(){return this.fire("submit",{data:this.toJSON()})},postRender:function(){var e=this;e._super(),e.fromJSON(e.settings.data)},bindStates:function(){function e(){var e=0,n=[],r,i,o;if(t.settings.labelGapCalc!==!1)for(o="children"==t.settings.labelGapCalc?t.find("formitem"):t.items(),o.filter("formitem").each(function(t){var r=t.items()[0],i=r.getEl().clientWidth;e=i>e?i:e,n.push(r)}),i=t.settings.labelGap||0,r=n.length;r--;)n[r].settings.minWidth=e+i}var t=this;t._super(),t.on("show",e),e()}})}),r(Dt,[Bt],function(e){return e.extend({Defaults:{containerCls:"fieldset",layout:"flex",direction:"column",align:"stretch",flex:1,padding:"25 15 5 15",labelGap:30,spacing:10,border:1},renderHtml:function(){var e=this,t=e._layout,n=e.classPrefix;return e.preRender(),t.preRender(e),'
    '+(e.settings.title?''+e.settings.title+"":"")+'
    '+(e.settings.html||"")+t.renderHtml(e)+"
    "}})}),r(Lt,[w,z,h,it,m,_],function(e,t,n,r,i,o){var a=i.trim,s=function(e,t,n,r,i){return{type:e,title:t,url:n,level:r,attach:i}},l=function(e){for(;e=e.parentNode;){var t=e.contentEditable;if(t&&"inherit"!==t)return o.isContentEditableTrue(e)}return!1},u=function(t,n){return e.DOM.select(t,n)},c=function(e){return e.innerText||e.textContent},d=function(e){return e.id?e.id:r.uuid("h")},f=function(e){return e&&"A"===e.nodeName&&(e.id||e.name)},p=function(e){return f(e)&&m(e)},h=function(e){return e&&/^(H[1-6])$/.test(e.nodeName)},m=function(e){return l(e)&&!o.isContentEditableFalse(e)},g=function(e){return h(e)&&m(e)},v=function(e){return h(e)?parseInt(e.nodeName.substr(1),10):0},y=function(e){var t=d(e),n=function(){e.id=t};return s("header",c(e),"#"+t,v(e),n)},b=function(e){var n=e.id||e.name,r=c(e);return s("anchor",r?r:"#"+n,"#"+n,0,t.noop)},C=function(e){return n.map(n.filter(e,g),y)},x=function(e){return n.map(n.filter(e,p),b)},w=function(e){var t=u("h1,h2,h3,h4,h5,h6,a:not([href])",e);return t},E=function(e){return a(e.title).length>0},N=function(e){var t=w(e);return n.filter(C(t).concat(x(t)),E)};return{find:N}}),r(Mt,[wt,m,h,z,I,Lt],function(e,t,n,r,i,o){var a={},s=5,l=function(e){return{title:e.title,value:{title:{raw:e.title},url:e.url,attach:e.attach}}},u=function(e){return t.map(e,l)},c=function(e,t){return{title:e,value:{title:e,url:t,attach:r.noop}}},d=function(e,t){var r=n.find(t,function(t){return t.url===e});return!r},f=function(e,t,n){var r=t in e?e[t]:n;return r===!1?null:r},p=function(e,i,o,s){var l={title:"-"},p=function(e){var a=n.filter(e[o],function(e){return d(e,i)});return t.map(a,function(e){return{title:e,value:{title:e,url:e,attach:r.noop}}})},h=function(e){var t=n.filter(i,function(t){return t.type==e});return u(t)},g=function(){var e=h("anchor"),t=f(s,"anchor_top","#top"),n=f(s,"anchor_bottom","#bottom");return null!==t&&e.unshift(c("",t)),null!==n&&e.push(c("",n)),e},v=function(e){return n.reduce(e,function(e,t){var n=0===e.length||0===t.length;return n?e.concat(t):e.concat(l,t)},[])};return s.typeahead_urls===!1?[]:"file"===o?v([m(e,p(a)),m(e,h("header")),m(e,g())]):m(e,p(a))},h=function(e,t){var r=a[t];/^https?/.test(e)&&(r?n.indexOf(r,e)===-1&&(a[t]=r.slice(0,s).concat(e)):a[t]=[e])},m=function(e,n){var r=e.toLowerCase(),i=t.grep(n,function(e){return e.title.toLowerCase().indexOf(r)!==-1});return 1===i.length&&i[0].title===e?[]:i},g=function(e){var t=e.title;return t.raw?t.raw:t},v=function(e,t,n,r){var i=function(i){var a=o.find(n),s=p(i,a,r,t);e.showAutoComplete(s,i)};e.on("autocomplete",function(){i(e.value())}),e.on("selectitem",function(t){var n=t.value;e.value(n.url);var i=g(n);"image"===r?e.fire("change",{meta:{alt:i,attach:n.attach}}):e.fire("change",{meta:{text:i,attach:n.attach}}),e.focus()}),e.on("click",function(t){0===e.value().length&&"INPUT"===t.target.nodeName&&i("")}),e.on("PostRender",function(){e.getRoot().on("submit",function(t){t.isDefaultPrevented()||h(e.value(),r)})})},y=function(e){var t=e.status,n=e.message;return"valid"===t?{status:"ok",message:n}:"unknown"===t?{status:"warn",message:n}:"invalid"===t?{status:"warn",message:n}:{status:"none",message:""}},b=function(e,t,n){var r=t.filepicker_validator_handler;if(r){var i=function(t){return 0===t.length?void e.statusLevel("none"):void r({url:t,type:n},function(t){var n=y(t);e.statusMessage(n.message),e.statusLevel(n.status)})};e.state.on("change:value",function(e){i(e.value)})}};return e.extend({init:function(e){var n=this,r=tinymce.activeEditor,i=r.settings,o,a,s,l=e.filetype;e.spellcheck=!1,s=i.file_picker_types||i.file_browser_callback_types,s&&(s=t.makeMap(s,/[, ]/)),s&&!s[l]||(a=i.file_picker_callback,!a||s&&!s[l]?(a=i.file_browser_callback,!a||s&&!s[l]||(o=function(){a(n.getEl("inp").id,n.value(),l,window)})):o=function(){var e=n.fire("beforecall").meta;e=t.extend({filetype:l},e),a.call(r,function(e,t){n.value(e).fire("change",{meta:t})},n.value(),e)}),o&&(e.icon="browse",e.onaction=o),n._super(e),v(n,i,r.getBody(),l),b(n,i,l)}})}),r(Pt,[yt],function(e){return e.extend({recalc:function(e){var t=e.layoutRect(),n=e.paddingBox;e.items().filter(":visible").each(function(e){e.layoutRect({x:n.left,y:n.top,w:t.innerW-n.right-n.left,h:t.innerH-n.top-n.bottom}),e.recalc&&e.recalc()})}})}),r(Ot,[yt],function(e){return e.extend({recalc:function(e){var t,n,r,i,o,a,s,l,u,c,d,f,p,h,m,g,v=[],y,b,C,x,w,E,N,_,S,k,T,R,A,B,D,L,M,P,O,H,I,F,z=Math.max,U=Math.min;for(r=e.items().filter(":visible"),i=e.layoutRect(),o=e.paddingBox,a=e.settings,f=e.isRtl()?a.direction||"row-reversed":a.direction,s=a.align,l=e.isRtl()?a.pack||"end":a.pack,u=a.spacing||0,"row-reversed"!=f&&"column-reverse"!=f||(r=r.set(r.toArray().reverse()),f=f.split("-")[0]),"column"==f?(S="y",N="h",_="minH",k="maxH",R="innerH",T="top",A="deltaH",B="contentH",O="left",M="w",D="x",L="innerW",P="minW",H="right",I="deltaW",F="contentW"):(S="x",N="w",_="minW",k="maxW",R="innerW",T="left",A="deltaW",B="contentW",O="top",M="h",D="y",L="innerH",P="minH",H="bottom",I="deltaH",F="contentH"),d=i[R]-o[T]-o[T],E=c=0,t=0,n=r.length;t0&&(c+=g,h[k]&&v.push(p),h.flex=g),d-=h[_],y=o[O]+h[P]+o[H],y>E&&(E=y);if(x={},d<0?x[_]=i[_]-d+i[A]:x[_]=i[R]-d+i[A],x[P]=E+i[I],x[B]=i[R]-d,x[F]=E,x.minW=U(x.minW,i.maxW),x.minH=U(x.minH,i.maxH),x.minW=z(x.minW,i.startMinWidth),x.minH=z(x.minH,i.startMinHeight),!i.autoResize||x.minW==i.minW&&x.minH==i.minH){for(C=d/c,t=0,n=v.length;tb?(d-=h[k]-h[_],c-=h.flex,h.flex=0,h.maxFlexSize=b):h.maxFlexSize=0;for(C=d/c,w=o[T],x={},0===c&&("end"==l?w=d+o[T]:"center"==l?(w=Math.round(i[R]/2-(i[R]-d)/2)+o[T],w<0&&(w=o[T])):"justify"==l&&(w=o[T],u=Math.floor(d/(r.length-1)))),x[D]=o[O],t=0,n=r.length;t0&&(y+=h.flex*C),x[N]=y,x[S]=w,p.layoutRect(x),p.recalc&&p.recalc(),w+=y+u}else if(x.w=x.minW,x.h=x.minH,e.layoutRect(x),this.recalc(e),null===e._lastRect){var W=e.parent();W&&(W._lastRect=null,W.recalc())}}})}),r(Ht,[vt],function(e){return e.extend({Defaults:{containerClass:"flow-layout",controlClass:"flow-layout-item",endClass:"break"},recalc:function(e){e.items().filter(":visible").each(function(e){e.recalc&&e.recalc()})},isNative:function(){return!0}})}),r(It,[w],function(e){var t=function(e,t,n){for(;n!==t;){if(n.style[e])return n.style[e];n=n.parentNode}return 0},n=function(e){return/[0-9.]+px$/.test(e)?Math.round(72*parseInt(e,10)/96)+"pt":e},r=function(e){return e.replace(/[\'\"]/g,"").replace(/,\s+/g,",")},i=function(t,n){return e.DOM.getStyle(n,t,!0)},o=function(e,n){var r=t("fontSize",e,n);return r?r:i("fontSize",n)},a=function(e,n){var o=t("fontFamily",e,n);return r(o?o:i("fontFamily",n))};return{getFontSize:o,getFontFamily:a,toPt:n}}),r(Ft,[xe,Pe,Ae,m,h,w,ut,d,It],function(e,t,n,r,i,o,a,s,l){function u(e){e.settings.ui_container&&(s.container=o.DOM.select(e.settings.ui_container)[0])}function c(t){t.on("ScriptsLoaded",function(){t.rtl&&(e.rtl=!0)})}function d(e){function t(t,n){return function(){var r=this;e.on("nodeChange",function(i){var o=e.formatter,a=null;f(i.parents,function(e){if(f(t,function(t){if(n?o.matchNode(e,n,{value:t.value})&&(a=t.value):o.matchNode(e,t.value)&&(a=t.value),a)return!1}),a)return!1}),r.value(a)})}}function i(t){return function(){var n=this,r=function(e){return e?e.split(",")[0]:""};e.on("nodeChange",function(i){var o,a=null;o=l.getFontFamily(e.getBody(),i.element),f(t,function(e){e.value.toLowerCase()===o.toLowerCase()&&(a=e.value)}),f(t,function(e){a||r(e.value).toLowerCase()!==r(o).toLowerCase()||(a=e.value)}),n.value(a),!a&&o&&n.text(r(o))})}}function o(t){return function(){var n=this;e.on("nodeChange",function(r){var i,o,a=null;i=l.getFontSize(e.getBody(),r.element),o=l.toPt(i),f(t,function(e){e.value===i?a=i:e.value===o&&(a=o)}),n.value(a),a||n.text(o)})}}function a(e){e=e.replace(/;$/,"").split(";");for(var t=e.length;t--;)e[t]=e[t].split("=");return e}function s(){function t(e){var n=[];if(e)return f(e,function(e){var o={text:e.title,icon:e.icon};if(e.items)o.menu=t(e.items);else{var a=e.format||"custom"+r++;e.format||(e.name=a,i.push(e)),o.format=a,o.cmd=e.cmd}n.push(o)}),n}function n(){var n;return n=t(e.settings.style_formats_merge?e.settings.style_formats?o.concat(e.settings.style_formats):o:e.settings.style_formats||o)}var r=0,i=[],o=[{title:"Headings",items:[{title:"Heading 1",format:"h1"},{title:"Heading 2",format:"h2"},{title:"Heading 3",format:"h3"},{title:"Heading 4",format:"h4"},{title:"Heading 5",format:"h5"},{title:"Heading 6",format:"h6"}]},{title:"Inline",items:[{title:"Bold",icon:"bold",format:"bold"},{title:"Italic",icon:"italic",format:"italic"},{title:"Underline",icon:"underline",format:"underline"},{title:"Strikethrough",icon:"strikethrough",format:"strikethrough"},{title:"Superscript",icon:"superscript",format:"superscript"},{title:"Subscript",icon:"subscript",format:"subscript"},{title:"Code",icon:"code",format:"code"}]},{title:"Blocks",items:[{title:"Paragraph",format:"p"},{title:"Blockquote",format:"blockquote"},{title:"Div",format:"div"},{title:"Pre",format:"pre"}]},{title:"Alignment",items:[{title:"Left",icon:"alignleft",format:"alignleft"},{title:"Center",icon:"aligncenter",format:"aligncenter"},{title:"Right",icon:"alignright",format:"alignright"},{title:"Justify",icon:"alignjustify",format:"alignjustify"}]}];return e.on("init",function(){f(i,function(t){e.formatter.register(t.name,t)})}),{type:"menu",items:n(),onPostRender:function(t){e.fire("renderFormatsMenu",{control:t.control})},itemDefaults:{preview:!0,textStyle:function(){if(this.settings.format)return e.formatter.getCssText(this.settings.format)},onPostRender:function(){var t=this;t.parent().on("show",function(){var n,r;n=t.settings.format,n&&(t.disabled(!e.formatter.canApply(n)),t.active(e.formatter.match(n))),r=t.settings.cmd,r&&t.active(e.queryCommandState(r))})},onclick:function(){this.settings.format&&h(this.settings.format),this.settings.cmd&&e.execCommand(this.settings.cmd)}}}}function u(t){return function(){var n=this;e.formatter?e.formatter.formatChanged(t,function(e){n.active(e)}):e.on("init",function(){e.formatter.formatChanged(t,function(e){n.active(e)})})}}function c(t){return function(){function n(){return!!e.undoManager&&e.undoManager[t]()}var r=this;t="redo"==t?"hasRedo":"hasUndo",r.disabled(!n()),e.on("Undo Redo AddUndo TypingUndo ClearUndos SwitchMode",function(){r.disabled(e.readonly||!n())})}}function d(){var t=this;e.on("VisualAid",function(e){t.active(e.hasVisual)}),t.active(e.hasVisual)}function h(t){t.control&&(t=t.control.value()),t&&e.execCommand("mceToggleFormat",!1,t)}function m(t){var n=t.length;return r.each(t,function(t){t.menu&&(t.hidden=0===m(t.menu));var r=t.format;r&&(t.hidden=!e.formatter.canApply(r)),t.hidden&&n--}),n}function g(t){var n=t.items().length;return t.items().each(function(t){t.menu&&t.visible(g(t.menu)>0),!t.menu&&t.settings.menu&&t.visible(m(t.settings.menu)>0);var r=t.settings.format;r&&t.visible(e.formatter.canApply(r)),t.visible()||n--}),n}var v;v=s(),f({bold:"Bold",italic:"Italic",underline:"Underline",strikethrough:"Strikethrough",subscript:"Subscript",superscript:"Superscript"},function(t,n){e.addButton(n,{tooltip:t,onPostRender:u(n),onclick:function(){h(n)}})}),f({outdent:["Decrease indent","Outdent"],indent:["Increase indent","Indent"],cut:["Cut","Cut"],copy:["Copy","Copy"],paste:["Paste","Paste"],help:["Help","mceHelp"],selectall:["Select all","SelectAll"],removeformat:["Clear formatting","RemoveFormat"],visualaid:["Visual aids","mceToggleVisualAid"],newdocument:["New document","mceNewDocument"]},function(t,n){e.addButton(n,{tooltip:t[0],cmd:t[1]})}),f({blockquote:["Blockquote","mceBlockQuote"],subscript:["Subscript","Subscript"],superscript:["Superscript","Superscript"],alignleft:["Align left","JustifyLeft"],aligncenter:["Align center","JustifyCenter"],alignright:["Align right","JustifyRight"],alignjustify:["Justify","JustifyFull"],alignnone:["No alignment","JustifyNone"]},function(t,n){e.addButton(n,{tooltip:t[0],cmd:t[1],onPostRender:u(n)})});var y=function(e){var t=e;return t.length>0&&"-"===t[0].text&&(t=t.slice(1)),t.length>0&&"-"===t[t.length-1].text&&(t=t.slice(0,t.length-1)),t},b=function(t){var n,i;if("string"==typeof t)i=t.split(" ");else if(r.isArray(t))return p(r.map(t,b));return n=r.grep(i,function(t){return"|"===t||t in e.menuItems}),r.map(n,function(t){return"|"===t?{text:"-"}:e.menuItems[t]})},C=function(t){var n=[{text:"-"}],i=r.grep(e.menuItems,function(e){return e.context===t});return r.each(i,function(e){"before"==e.separator&&n.push({text:"|"}),e.prependToContext?n.unshift(e):n.push(e),"after"==e.separator&&n.push({text:"|"})}),n},x=function(e){return y(e.insert_button_items?b(e.insert_button_items):C("insert"))};e.addButton("undo",{tooltip:"Undo",onPostRender:c("undo"),cmd:"undo"}),e.addButton("redo",{tooltip:"Redo",onPostRender:c("redo"),cmd:"redo"}),e.addMenuItem("newdocument",{text:"New document",icon:"newdocument",cmd:"mceNewDocument"}),e.addMenuItem("undo",{text:"Undo",icon:"undo",shortcut:"Meta+Z",onPostRender:c("undo"),cmd:"undo"}),e.addMenuItem("redo",{text:"Redo",icon:"redo",shortcut:"Meta+Y",onPostRender:c("redo"),cmd:"redo"}),e.addMenuItem("visualaid",{text:"Visual aids",selectable:!0,onPostRender:d,cmd:"mceToggleVisualAid"}),e.addButton("remove",{tooltip:"Remove",icon:"remove",cmd:"Delete"}),e.addButton("insert",{type:"menubutton",icon:"insert",menu:[],oncreatemenu:function(){this.menu.add(x(e.settings)),this.menu.renderNew()}}),f({cut:["Cut","Cut","Meta+X"],copy:["Copy","Copy","Meta+C"],paste:["Paste","Paste","Meta+V"],selectall:["Select all","SelectAll","Meta+A"],bold:["Bold","Bold","Meta+B"],italic:["Italic","Italic","Meta+I"],underline:["Underline","Underline","Meta+U"],strikethrough:["Strikethrough","Strikethrough"],subscript:["Subscript","Subscript"],superscript:["Superscript","Superscript"],removeformat:["Clear formatting","RemoveFormat"]},function(t,n){e.addMenuItem(n,{text:t[0],icon:n,shortcut:t[2],cmd:t[1]})}),e.on("mousedown",function(){n.hideAll()}),e.addButton("styleselect",{type:"menubutton",text:"Formats",menu:v,onShowMenu:function(){e.settings.style_formats_autohide&&g(this.menu)}}),e.addButton("formatselect",function(){var n=[],r=a(e.settings.block_formats||"Paragraph=p;Heading 1=h1;Heading 2=h2;Heading 3=h3;Heading 4=h4;Heading 5=h5;Heading 6=h6;Preformatted=pre");return f(r,function(t){n.push({text:t[0],value:t[1],textStyle:function(){return e.formatter.getCssText(t[1])}})}),{type:"listbox",text:r[0][0],values:n,fixedWidth:!0,onselect:h,onPostRender:t(n)}}),e.addButton("fontselect",function(){var t="Andale Mono=andale mono,monospace;Arial=arial,helvetica,sans-serif;Arial Black=arial black,sans-serif;Book Antiqua=book antiqua,palatino,serif;Comic Sans MS=comic sans ms,sans-serif;Courier New=courier new,courier,monospace;Georgia=georgia,palatino,serif;Helvetica=helvetica,arial,sans-serif;Impact=impact,sans-serif;Symbol=symbol;Tahoma=tahoma,arial,helvetica,sans-serif;Terminal=terminal,monaco,monospace;Times New Roman=times new roman,times,serif;Trebuchet MS=trebuchet ms,geneva,sans-serif;Verdana=verdana,geneva,sans-serif;Webdings=webdings;Wingdings=wingdings,zapf dingbats",n=[],r=a(e.settings.font_formats||t);return f(r,function(e){n.push({text:{raw:e[0]},value:e[1],textStyle:e[1].indexOf("dings")==-1?"font-family:"+e[1]:""})}),{type:"listbox",text:"Font Family",tooltip:"Font Family",values:n,fixedWidth:!0,onPostRender:i(n),onselect:function(t){t.control.settings.value&&e.execCommand("FontName",!1,t.control.settings.value)}}}),e.addButton("fontsizeselect",function(){var t=[],n="8pt 10pt 12pt 14pt 18pt 24pt 36pt",r=e.settings.fontsize_formats||n;return f(r.split(" "),function(e){var n=e,r=e,i=e.split("=");i.length>1&&(n=i[0],r=i[1]),t.push({text:n,value:r})}),{type:"listbox",text:"Font Sizes",tooltip:"Font Sizes",values:t,fixedWidth:!0,onPostRender:o(t),onclick:function(t){t.control.settings.value&&e.execCommand("FontSize",!1,t.control.settings.value)}}}),e.addMenuItem("formats",{text:"Formats",menu:v})}var f=r.each,p=function(e){return i.reduce(e,function(e,t){return e.concat(t)},[])};a.on("AddEditor",function(e){var t=e.editor;c(t),d(t),u(t)}),e.translate=function(e){return a.translate(e)},t.tooltips=!s.iOS}),r(zt,[yt],function(e){return e.extend({recalc:function(e){var t,n,r,i,o,a,s,l,u,c,d,f,p,h,m,g,v,y,b,C,x,w,E,N=[],_=[],S,k,T,R,A,B;t=e.settings,i=e.items().filter(":visible"),o=e.layoutRect(),r=t.columns||Math.ceil(Math.sqrt(i.length)),n=Math.ceil(i.length/r),y=t.spacingH||t.spacing||0,b=t.spacingV||t.spacing||0,C=t.alignH||t.align,x=t.alignV||t.align,g=e.paddingBox,A="reverseRows"in t?t.reverseRows:e.isRtl(),C&&"string"==typeof C&&(C=[C]),x&&"string"==typeof x&&(x=[x]);for(d=0;dN[d]?S:N[d],_[f]=k>_[f]?k:_[f];for(T=o.innerW-g.left-g.right,w=0,d=0;d0?y:0),T-=(d>0?y:0)+N[d];for(R=o.innerH-g.top-g.bottom,E=0,f=0;f0?b:0),R-=(f>0?b:0)+_[f];if(w+=g.left+g.right,E+=g.top+g.bottom,l={},l.minW=w+(o.w-o.innerW),l.minH=E+(o.h-o.innerH),l.contentW=l.minW-o.deltaW,l.contentH=l.minH-o.deltaH,l.minW=Math.min(l.minW,o.maxW),l.minH=Math.min(l.minH,o.maxH),l.minW=Math.max(l.minW,o.startMinWidth),l.minH=Math.max(l.minH,o.startMinHeight),!o.autoResize||l.minW==o.minW&&l.minH==o.minH){o.autoResize&&(l=e.layoutRect(l),l.contentW=l.minW-o.deltaW,l.contentH=l.minH-o.deltaH);var D;D="start"==t.packV?0:R>0?Math.floor(R/n):0;var L=0,M=t.flexWidths;if(M)for(d=0;d'},src:function(e){this.getEl().src=e},html:function(e,n){var r=this,i=this.getEl().contentWindow.document.body;return i?(i.innerHTML=e,n&&n()):t.setTimeout(function(){r.html(e)}),this}})}),r(Wt,[Pe],function(e){return e.extend({init:function(e){var t=this;t._super(e),t.classes.add("widget").add("infobox"),t.canFocus=!1},severity:function(e){this.classes.remove("error"),this.classes.remove("warning"),this.classes.remove("success"),this.classes.add(e)},help:function(e){this.state.set("help",e)},renderHtml:function(){var e=this,t=e.classPrefix;return'
    '+e.encode(e.state.get("text"))+'
    '},bindStates:function(){var e=this;return e.state.on("change:text",function(t){e.getEl("body").firstChild.data=e.encode(t.value),e.state.get("rendered")&&e.updateLayoutRect()}),e.state.on("change:help",function(t){e.classes.toggle("has-help",t.value),e.state.get("rendered")&&e.updateLayoutRect()}),e._super()}})}),r(Vt,[Pe,ve],function(e,t){return e.extend({init:function(e){var t=this;t._super(e),t.classes.add("widget").add("label"),t.canFocus=!1,e.multiline&&t.classes.add("autoscroll"),e.strong&&t.classes.add("strong")},initLayoutRect:function(){var e=this,n=e._super();if(e.settings.multiline){var r=t.getSize(e.getEl());r.width>n.maxW&&(n.minW=n.maxW,e.classes.add("multiline")),e.getEl().style.width=n.minW+"px",n.startMinH=n.h=n.minH=Math.min(n.maxH,t.getSize(e.getEl()).height)}return n},repaint:function(){var e=this;return e.settings.multiline||(e.getEl().style.lineHeight=e.layoutRect().h+"px"),e._super()},severity:function(e){this.classes.remove("error"),this.classes.remove("warning"),this.classes.remove("success"),this.classes.add(e)},renderHtml:function(){var e=this,t,n,r=e.settings.forId;return!r&&(n=e.settings.forName)&&(t=e.getRoot().find("#"+n)[0],t&&(r=t._id)),r?'":''+e.encode(e.state.get("text"))+""},bindStates:function(){var e=this;return e.state.on("change:text",function(t){e.innerHtml(e.encode(t.value)),e.state.get("rendered")&&e.updateLayoutRect()}),e._super()}})}),r($t,[Ne],function(e){return e.extend({Defaults:{role:"toolbar",layout:"flow"},init:function(e){var t=this;t._super(e),t.classes.add("toolbar")},postRender:function(){var e=this;return e.items().each(function(e){e.classes.add("toolbar-item")}),e._super()}})}),r(qt,[$t],function(e){ +return e.extend({Defaults:{role:"menubar",containerCls:"menubar",ariaRoot:!0,defaults:{type:"menubutton"}}})}),r(jt,[bt,we,qt],function(e,t,n){function r(e,t){for(;e;){if(t===e)return!0;e=e.parentNode}return!1}var i=e.extend({init:function(e){var t=this;t._renderOpen=!0,t._super(e),e=t.settings,t.classes.add("menubtn"),e.fixedWidth&&t.classes.add("fixed-width"),t.aria("haspopup",!0),t.state.set("menu",e.menu||t.render())},showMenu:function(){var e=this,n;return e.menu&&e.menu.visible()?e.hideMenu():(e.menu||(n=e.state.get("menu")||[],n.length?n={type:"menu",items:n}:n.type=n.type||"menu",n.renderTo?e.menu=n.parent(e).show().renderTo():e.menu=t.create(n).parent(e).renderTo(),e.fire("createmenu"),e.menu.reflow(),e.menu.on("cancel",function(t){t.control.parent()===e.menu&&(t.stopPropagation(),e.focus(),e.hideMenu())}),e.menu.on("select",function(){e.focus()}),e.menu.on("show hide",function(t){t.control==e.menu&&e.activeMenu("show"==t.type),e.aria("expanded","show"==t.type)}).fire("show")),e.menu.show(),e.menu.layoutRect({w:e.layoutRect().w}),e.menu.moveRel(e.getEl(),e.isRtl()?["br-tr","tr-br"]:["bl-tl","tl-bl"]),void e.fire("showmenu"))},hideMenu:function(){var e=this;e.menu&&(e.menu.items().each(function(e){e.hideMenu&&e.hideMenu()}),e.menu.hide())},activeMenu:function(e){this.classes.toggle("active",e)},renderHtml:function(){var e=this,t=e._id,r=e.classPrefix,i=e.settings.icon,o,a=e.state.get("text"),s="";return o=e.settings.image,o?(i="none","string"!=typeof o&&(o=window.getSelection?o[0]:o[1]),o=" style=\"background-image: url('"+o+"')\""):o="",a&&(e.classes.add("btn-has-text"),s=''+e.encode(a)+""),i=e.settings.icon?r+"ico "+r+"i-"+i:"",e.aria("role",e.parent()instanceof n?"menuitem":"button"),'
    '},postRender:function(){var e=this;return e.on("click",function(t){t.control===e&&r(t.target,e.getEl())&&(e.showMenu(),t.aria&&e.menu.items().filter(":visible")[0].focus())}),e.on("mouseenter",function(t){var n=t.control,r=e.parent(),o;n&&r&&n instanceof i&&n.parent()==r&&(r.items().filter("MenuButton").each(function(e){e.hideMenu&&e!=n&&(e.menu&&e.menu.visible()&&(o=!0),e.hideMenu())}),o&&(n.focus(),n.showMenu()))}),e._super()},bindStates:function(){var e=this;return e.state.on("change:menu",function(){e.menu&&e.menu.remove(),e.menu=null}),e._super()},remove:function(){this._super(),this.menu&&this.menu.remove()}});return i}),r(Yt,[Pe,we,d,c],function(e,t,n,r){return e.extend({Defaults:{border:0,role:"menuitem"},init:function(e){var t=this,n;t._super(e),e=t.settings,t.classes.add("menu-item"),e.menu&&t.classes.add("menu-item-expand"),e.preview&&t.classes.add("menu-item-preview"),n=t.state.get("text"),"-"!==n&&"|"!==n||(t.classes.add("menu-item-sep"),t.aria("role","separator"),t.state.set("text","-")),e.selectable&&(t.aria("role","menuitemcheckbox"),t.classes.add("menu-item-checkbox"),e.icon="selected"),e.preview||e.selectable||t.classes.add("menu-item-normal"),t.on("mousedown",function(e){e.preventDefault()}),e.menu&&!e.ariaHideMenu&&t.aria("haspopup",!0)},hasMenus:function(){return!!this.settings.menu},showMenu:function(){var e=this,n=e.settings,r,i=e.parent();if(i.items().each(function(t){t!==e&&t.hideMenu()}),n.menu){r=e.menu,r?r.show():(r=n.menu,r.length?r={type:"menu",items:r}:r.type=r.type||"menu",i.settings.itemDefaults&&(r.itemDefaults=i.settings.itemDefaults),r=e.menu=t.create(r).parent(e).renderTo(),r.reflow(),r.on("cancel",function(t){t.stopPropagation(),e.focus(),r.hide()}),r.on("show hide",function(e){e.control.items&&e.control.items().each(function(e){e.active(e.settings.selected)})}).fire("show"),r.on("hide",function(t){t.control===r&&e.classes.remove("selected")}),r.submenu=!0),r._parentMenu=i,r.classes.add("menu-sub");var o=r.testMoveRel(e.getEl(),e.isRtl()?["tl-tr","bl-br","tr-tl","br-bl"]:["tr-tl","br-bl","tl-tr","bl-br"]);r.moveRel(e.getEl(),o),r.rel=o,o="menu-sub-"+o,r.classes.remove(r._lastRel).add(o),r._lastRel=o,e.classes.add("selected"),e.aria("expanded",!0)}},hideMenu:function(){var e=this;return e.menu&&(e.menu.items().each(function(e){e.hideMenu&&e.hideMenu()}),e.menu.hide(),e.aria("expanded",!1)),e},renderHtml:function(){function e(e){var t,r,i={};for(i=n.mac?{alt:"⌥",ctrl:"⌘",shift:"⇧",meta:"⌘"}:{meta:"Ctrl"},e=e.split("+"),t=0;t").replace(new RegExp(t("]mce~match!"),"g"),"")}var o=this,a=o._id,s=o.settings,l=o.classPrefix,u=o.state.get("text"),c=o.settings.icon,d="",f=s.shortcut,p=o.encode(s.url),h="";return c&&o.parent().classes.add("menu-has-icons"),s.image&&(d=" style=\"background-image: url('"+s.image+"')\""),f&&(f=e(f)),c=l+"ico "+l+"i-"+(o.settings.icon||"none"),h="-"!==u?'\xa0":"",u=i(o.encode(r(u))),p=i(o.encode(r(p))),'
    '+h+("-"!==u?''+u+"":"")+(f?'
    '+f+"
    ":"")+(s.menu?'
    ':"")+(p?'":"")+"
    "},postRender:function(){var e=this,t=e.settings,n=t.textStyle;if("function"==typeof n&&(n=n.call(this)),n){var i=e.getEl("text");i&&i.setAttribute("style",n)}return e.on("mouseenter click",function(n){n.control===e&&(t.menu||"click"!==n.type?(e.showMenu(),n.aria&&e.menu.focus(!0)):(e.fire("select"),r.requestAnimationFrame(function(){e.parent().hideAll()})))}),e._super(),e},hover:function(){var e=this;return e.parent().items().each(function(e){e.classes.remove("selected")}),e.classes.toggle("selected",!0),e},active:function(e){return"undefined"!=typeof e&&this.aria("checked",e),this._super(e)},remove:function(){this._super(),this.menu&&this.menu.remove()}})}),r(Xt,[g,xe,c],function(e,t,n){return function(r,i){var o=this,a,s=t.classPrefix,l;o.show=function(t,u){function c(){a&&(e(r).append('
    '),u&&u())}return o.hide(),a=!0,t?l=n.setTimeout(c,t):c(),o},o.hide=function(){var e=r.lastChild;return n.clearTimeout(l),e&&e.className.indexOf("throbber")!=-1&&e.parentNode.removeChild(e),a=!1,o}}}),r(Kt,[Ae,Yt,Xt,m],function(e,t,n,r){return e.extend({Defaults:{defaultType:"menuitem",border:1,layout:"stack",role:"application",bodyRole:"menu",ariaRoot:!0},init:function(e){var t=this;if(e.autohide=!0,e.constrainToViewport=!0,"function"==typeof e.items&&(e.itemsFactory=e.items,e.items=[]),e.itemDefaults)for(var n=e.items,i=n.length;i--;)n[i]=r.extend({},e.itemDefaults,n[i]);t._super(e),t.classes.add("menu")},repaint:function(){return this.classes.toggle("menu-align",!0),this._super(),this.getEl().style.height="",this.getEl("body").style.height="",this},cancel:function(){var e=this;e.hideAll(),e.fire("select")},load:function(){function e(){t.throbber&&(t.throbber.hide(),t.throbber=null)}var t=this,r,i;i=t.settings.itemsFactory,i&&(t.throbber||(t.throbber=new n(t.getEl("body"),!0),0===t.items().length?(t.throbber.show(),t.fire("loading")):t.throbber.show(100,function(){t.items().remove(),t.fire("loading")}),t.on("hide close",e)),t.requestTime=r=(new Date).getTime(),t.settings.itemsFactory(function(n){return 0===n.length?void t.hide():void(t.requestTime===r&&(t.getEl().style.width="",t.getEl("body").style.width="",e(),t.items().remove(),t.getEl("body").innerHTML="",t.add(n),t.renderNew(),t.fire("loaded")))}))},hideAll:function(){var e=this;return this.find("menuitem").exec("hideMenu"),e._super()},preRender:function(){var e=this;return e.items().each(function(t){var n=t.settings;if(n.icon||n.image||n.selectable)return e._hasIcons=!0,!1}),e.settings.itemsFactory&&e.on("postrender",function(){e.settings.itemsFactory&&e.load()}),e._super()}})}),r(Gt,[jt,Kt],function(e,t){return e.extend({init:function(e){function t(r){for(var a=0;a0&&(o=r[0].text,n.state.set("value",r[0].value)),n.state.set("menu",r)),n.state.set("text",e.text||o),n.classes.add("listbox"),n.on("select",function(t){var r=t.control;a&&(t.lastControl=a),e.multiple?r.active(!r.active()):n.value(t.control.value()),a=r})},bindStates:function(){function e(e,n){e instanceof t&&e.items().each(function(e){e.hasMenus()||e.active(e.value()===n)})}function n(e,t){var r;if(e)for(var i=0;i
    '},postRender:function(){var e=this;e._super(),e.resizeDragHelper=new t(this._id,{start:function(){e.fire("ResizeStart")},drag:function(t){"both"!=e.settings.direction&&(t.deltaX=0),e.fire("Resize",t)},stop:function(){e.fire("ResizeEnd")}})},remove:function(){return this.resizeDragHelper&&this.resizeDragHelper.destroy(),this._super()}})}),r(Zt,[Pe],function(e){function t(e){var t="";if(e)for(var n=0;n'+e[n]+"";return t}return e.extend({Defaults:{classes:"selectbox",role:"selectbox",options:[]},init:function(e){var t=this;t._super(e),t.settings.size&&(t.size=t.settings.size),t.settings.options&&(t._options=t.settings.options),t.on("keydown",function(e){var n;13==e.keyCode&&(e.preventDefault(),t.parents().reverse().each(function(e){if(e.toJSON)return n=e,!1}),t.fire("submit",{data:n.toJSON()}))})},options:function(e){return arguments.length?(this.state.set("options",e),this):this.state.get("options")},renderHtml:function(){var e=this,n,r="";return n=t(e._options),e.size&&(r=' size = "'+e.size+'"'),'"},bindStates:function(){var e=this;return e.state.on("change:options",function(n){e.getEl().innerHTML=t(n.value)}),e._super()}})}),r(en,[Pe,_e,ve],function(e,t,n){function r(e,t,n){return en&&(e=n),e}function i(e,t,n){e.setAttribute("aria-"+t,n)}function o(e,t){var r,o,a,s,l,u;"v"==e.settings.orientation?(s="top",a="height",o="h"):(s="left",a="width",o="w"),u=e.getEl("handle"),r=(e.layoutRect()[o]||100)-n.getSize(u)[a],l=r*((t-e._minValue)/(e._maxValue-e._minValue))+"px",u.style[s]=l,u.style.height=e.layoutRect().h+"px",i(u,"valuenow",t),i(u,"valuetext",""+e.settings.previewFilter(t)),i(u,"valuemin",e._minValue),i(u,"valuemax",e._maxValue)}return e.extend({init:function(e){var t=this;e.previewFilter||(e.previewFilter=function(e){return Math.round(100*e)/100}),t._super(e),t.classes.add("slider"),"v"==e.orientation&&t.classes.add("vertical"),t._minValue=e.minValue||0,t._maxValue=e.maxValue||100,t._initValue=t.state.get("value")},renderHtml:function(){var e=this,t=e._id,n=e.classPrefix;return'
    '},reset:function(){this.value(this._initValue).repaint()},postRender:function(){function e(e,t,n){return(n+e)/(t-e)}function i(e,t,n){return n*(t-e)-e}function o(t,n){function o(o){var a;a=s.value(),a=i(t,n,e(t,n,a)+.05*o),a=r(a,t,n),s.value(a),s.fire("dragstart",{value:a}),s.fire("drag",{value:a}),s.fire("dragend",{value:a})}s.on("keydown",function(e){switch(e.keyCode){case 37:case 38:o(-1);break;case 39:case 40:o(1)}})}function a(e,i,o){var a,l,u,h,m;s._dragHelper=new t(s._id,{handle:s._id+"-handle",start:function(e){a=e[c],l=parseInt(s.getEl("handle").style[d],10),u=(s.layoutRect()[p]||100)-n.getSize(o)[f],s.fire("dragstart",{value:m})},drag:function(t){var n=t[c]-a;h=r(l+n,0,u),o.style[d]=h+"px",m=e+h/u*(i-e),s.value(m),s.tooltip().text(""+s.settings.previewFilter(m)).show().moveRel(o,"bc tc"),s.fire("drag",{value:m})},stop:function(){s.tooltip().hide(),s.fire("dragend",{value:m})}})}var s=this,l,u,c,d,f,p;l=s._minValue,u=s._maxValue,"v"==s.settings.orientation?(c="screenY",d="top",f="height",p="h"):(c="screenX",d="left",f="width",p="w"),s._super(),o(l,u,s.getEl("handle")),a(l,u,s.getEl("handle"))},repaint:function(){this._super(),o(this,this.value())},bindStates:function(){var e=this;return e.state.on("change:value",function(t){o(e,t.value)}),e._super()}})}),r(tn,[Pe],function(e){return e.extend({renderHtml:function(){var e=this;return e.classes.add("spacer"),e.canFocus=!1,'
    '}})}),r(nn,[jt,ve,g],function(e,t,n){return e.extend({Defaults:{classes:"widget btn splitbtn",role:"button"},repaint:function(){var e=this,r=e.getEl(),i=e.layoutRect(),o,a;return e._super(),o=r.firstChild,a=r.lastChild,n(o).css({width:i.w-t.getSize(a).width,height:i.h-2}),n(a).css({height:i.h-2}),e},activeMenu:function(e){var t=this;n(t.getEl().lastChild).toggleClass(t.classPrefix+"active",e)},renderHtml:function(){var e=this,t=e._id,n=e.classPrefix,r,i=e.state.get("icon"),o=e.state.get("text"),a="";return r=e.settings.image,r?(i="none","string"!=typeof r&&(r=window.getSelection?r[0]:r[1]),r=" style=\"background-image: url('"+r+"')\""):r="",i=e.settings.icon?n+"ico "+n+"i-"+i:"",o&&(e.classes.add("btn-has-text"),a=''+e.encode(o)+""),'
    '},postRender:function(){var e=this,t=e.settings.onclick;return e.on("click",function(e){var n=e.target;if(e.control==this)for(;n;){if(e.aria&&"down"!=e.aria.key||"BUTTON"==n.nodeName&&n.className.indexOf("open")==-1)return e.stopImmediatePropagation(),void(t&&t.call(this,e));n=n.parentNode}}),delete e.settings.onclick,e._super()}})}),r(rn,[Ht],function(e){return e.extend({Defaults:{containerClass:"stack-layout",controlClass:"stack-layout-item",endClass:"break"},isNative:function(){return!0}})}),r(on,[ke,g,ve],function(e,t,n){return e.extend({Defaults:{layout:"absolute",defaults:{type:"panel"}},activateTab:function(e){var n;this.activeTabId&&(n=this.getEl(this.activeTabId),t(n).removeClass(this.classPrefix+"active"),n.setAttribute("aria-selected","false")),this.activeTabId="t"+e,n=this.getEl("t"+e),n.setAttribute("aria-selected","true"),t(n).addClass(this.classPrefix+"active"),this.items()[e].show().fire("showtab"),this.reflow(),this.items().each(function(t,n){e!=n&&t.hide()})},renderHtml:function(){var e=this,t=e._layout,n="",r=e.classPrefix;return e.preRender(),t.preRender(e),e.items().each(function(t,i){var o=e._id+"-t"+i;t.aria("role","tabpanel"),t.aria("labelledby",o),n+='"}),'
    '+n+'
    '+t.renderHtml(e)+"
    "},postRender:function(){var e=this;e._super(),e.settings.activeTab=e.settings.activeTab||0,e.activateTab(e.settings.activeTab),this.on("click",function(t){var n=t.target.parentNode;if(n&&n.id==e._id+"-head")for(var r=n.childNodes.length;r--;)n.childNodes[r]==t.target&&e.activateTab(r)})},initLayoutRect:function(){var e=this,t,r,i;r=n.getSize(e.getEl("head")).width,r=r<0?0:r,i=0,e.items().each(function(e){r=Math.max(r,e.layoutRect().minW),i=Math.max(i,e.layoutRect().minH)}),e.items().each(function(e){e.settings.x=0,e.settings.y=0,e.settings.w=r,e.settings.h=i,e.layoutRect({x:0,y:0,w:r,h:i})});var o=n.getSize(e.getEl("head")).height;return e.settings.minWidth=r,e.settings.minHeight=i+o,t=e._super(),t.deltaH+=o,t.innerH=t.h-t.deltaH,t}})}),r(an,[Pe,m,ve],function(e,t,n){return e.extend({init:function(e){var t=this;t._super(e),t.classes.add("textbox"),e.multiline?t.classes.add("multiline"):(t.on("keydown",function(e){var n;13==e.keyCode&&(e.preventDefault(),t.parents().reverse().each(function(e){if(e.toJSON)return n=e,!1}),t.fire("submit",{data:n.toJSON()}))}),t.on("keyup",function(e){t.state.set("value",e.target.value)}))},repaint:function(){var e=this,t,n,r,i,o=0,a;t=e.getEl().style,n=e._layoutRect,a=e._lastRepaintRect||{};var s=document;return!e.settings.multiline&&s.all&&(!s.documentMode||s.documentMode<=8)&&(t.lineHeight=n.h-o+"px"),r=e.borderBox,i=r.left+r.right+8,o=r.top+r.bottom+(e.settings.multiline?8:0),n.x!==a.x&&(t.left=n.x+"px",a.x=n.x),n.y!==a.y&&(t.top=n.y+"px",a.y=n.y),n.w!==a.w&&(t.width=n.w-i+"px",a.w=n.w),n.h!==a.h&&(t.height=n.h-o+"px",a.h=n.h),e._lastRepaintRect=a,e.fire("repaint",{},!1),e},renderHtml:function(){var e=this,r=e.settings,i,o;return i={id:e._id,hidefocus:"1"},t.each(["rows","spellcheck","maxLength","size","readonly","min","max","step","list","pattern","placeholder","required","multiple"],function(e){i[e]=r[e]}),e.disabled()&&(i.disabled="disabled"),r.subtype&&(i.type=r.subtype),o=n.create(r.multiline?"textarea":"input",i),o.value=e.state.get("value"),o.className=e.classes,o.outerHTML},value:function(e){return arguments.length?(this.state.set("value",e),this):(this.state.get("rendered")&&this.state.set("value",this.getEl().value),this.state.get("value"))},postRender:function(){var e=this;e.getEl().value=e.state.get("value"),e._super(),e.$el.on("change",function(t){e.state.set("value",t.target.value),e.fire("change",t)})},bindStates:function(){var e=this;return e.state.on("change:value",function(t){e.getEl().value!=t.value&&(e.getEl().value=t.value)}),e.state.on("change:disabled",function(t){e.getEl().disabled=t.value}),e._super()},remove:function(){this.$el.off(),this._super()}})}),r(sn,[],function(){var e=this||window,t=function(){return e.tinymce};return"function"==typeof e.define&&(e.define.amd||e.define("ephox/tinymce",[],t)),"object"==typeof module&&(module.exports=window.tinymce),{}}),a([l,u,c,d,f,p,m,g,v,y,C,w,E,N,T,A,B,D,L,M,P,O,I,F,j,Y,J,te,le,ue,ce,de,pe,me,ge,Ce,xe,we,Ee,Ne,_e,Se,ke,Te,Re,Ae,Be,De,Le,Me,Pe,Oe,He,Ie,Ue,Ve,at,st,lt,ut,dt,ft,pt,ht,mt,gt,vt,yt,bt,Ct,xt,wt,Et,Nt,_t,St,kt,Tt,Rt,At,Bt,Dt,Mt,Pt,Ot,Ht,Ft,zt,Ut,Wt,Vt,$t,qt,jt,Yt,Xt,Kt,Gt,Jt,Qt,Zt,en,tn,nn,rn,on,an])}(window); \ No newline at end of file diff --git a/src/Public/js/vendor/typeahead.bundle.min.js b/src/Public/js/vendor/typeahead.bundle.min.js new file mode 100644 index 000000000..11dcbf42e --- /dev/null +++ b/src/Public/js/vendor/typeahead.bundle.min.js @@ -0,0 +1,7 @@ +/*! + * typeahead.js 0.10.5 + * https://github.com/twitter/typeahead.js + * Copyright 2013-2014 Twitter, Inc. and other contributors; Licensed MIT + */ + +!function(a){var b=function(){"use strict";return{isMsie:function(){return/(msie|trident)/i.test(navigator.userAgent)?navigator.userAgent.match(/(msie |rv:)(\d+(.\d+)?)/i)[2]:!1},isBlankString:function(a){return!a||/^\s*$/.test(a)},escapeRegExChars:function(a){return a.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g,"\\$&")},isString:function(a){return"string"==typeof a},isNumber:function(a){return"number"==typeof a},isArray:a.isArray,isFunction:a.isFunction,isObject:a.isPlainObject,isUndefined:function(a){return"undefined"==typeof a},toStr:function(a){return b.isUndefined(a)||null===a?"":a+""},bind:a.proxy,each:function(b,c){function d(a,b){return c(b,a)}a.each(b,d)},map:a.map,filter:a.grep,every:function(b,c){var d=!0;return b?(a.each(b,function(a,e){return(d=c.call(null,e,a,b))?void 0:!1}),!!d):d},some:function(b,c){var d=!1;return b?(a.each(b,function(a,e){return(d=c.call(null,e,a,b))?!1:void 0}),!!d):d},mixin:a.extend,getUniqueId:function(){var a=0;return function(){return a++}}(),templatify:function(b){function c(){return String(b)}return a.isFunction(b)?b:c},defer:function(a){setTimeout(a,0)},debounce:function(a,b,c){var d,e;return function(){var f,g,h=this,i=arguments;return f=function(){d=null,c||(e=a.apply(h,i))},g=c&&!d,clearTimeout(d),d=setTimeout(f,b),g&&(e=a.apply(h,i)),e}},throttle:function(a,b){var c,d,e,f,g,h;return g=0,h=function(){g=new Date,e=null,f=a.apply(c,d)},function(){var i=new Date,j=b-(i-g);return c=this,d=arguments,0>=j?(clearTimeout(e),e=null,g=i,f=a.apply(c,d)):e||(e=setTimeout(h,j)),f}},noop:function(){}}}(),c="0.10.5",d=function(){"use strict";function a(a){return a=b.toStr(a),a?a.split(/\s+/):[]}function c(a){return a=b.toStr(a),a?a.split(/\W+/):[]}function d(a){return function(){var c=[].slice.call(arguments,0);return function(d){var e=[];return b.each(c,function(c){e=e.concat(a(b.toStr(d[c])))}),e}}}return{nonword:c,whitespace:a,obj:{nonword:d(c),whitespace:d(a)}}}(),e=function(){"use strict";function c(c){this.maxSize=b.isNumber(c)?c:100,this.reset(),this.maxSize<=0&&(this.set=this.get=a.noop)}function d(){this.head=this.tail=null}function e(a,b){this.key=a,this.val=b,this.prev=this.next=null}return b.mixin(c.prototype,{set:function(a,b){var c,d=this.list.tail;this.size>=this.maxSize&&(this.list.remove(d),delete this.hash[d.key]),(c=this.hash[a])?(c.val=b,this.list.moveToFront(c)):(c=new e(a,b),this.list.add(c),this.hash[a]=c,this.size++)},get:function(a){var b=this.hash[a];return b?(this.list.moveToFront(b),b.val):void 0},reset:function(){this.size=0,this.hash={},this.list=new d}}),b.mixin(d.prototype,{add:function(a){this.head&&(a.next=this.head,this.head.prev=a),this.head=a,this.tail=this.tail||a},remove:function(a){a.prev?a.prev.next=a.next:this.head=a.next,a.next?a.next.prev=a.prev:this.tail=a.prev},moveToFront:function(a){this.remove(a),this.add(a)}}),c}(),f=function(){"use strict";function a(a){this.prefix=["__",a,"__"].join(""),this.ttlKey="__ttl__",this.keyMatcher=new RegExp("^"+b.escapeRegExChars(this.prefix))}function c(){return(new Date).getTime()}function d(a){return JSON.stringify(b.isUndefined(a)?null:a)}function e(a){return JSON.parse(a)}var f,g;try{f=window.localStorage,f.setItem("~~~","!"),f.removeItem("~~~")}catch(h){f=null}return g=f&&window.JSON?{_prefix:function(a){return this.prefix+a},_ttlKey:function(a){return this._prefix(a)+this.ttlKey},get:function(a){return this.isExpired(a)&&this.remove(a),e(f.getItem(this._prefix(a)))},set:function(a,e,g){return b.isNumber(g)?f.setItem(this._ttlKey(a),d(c()+g)):f.removeItem(this._ttlKey(a)),f.setItem(this._prefix(a),d(e))},remove:function(a){return f.removeItem(this._ttlKey(a)),f.removeItem(this._prefix(a)),this},clear:function(){var a,b,c=[],d=f.length;for(a=0;d>a;a++)(b=f.key(a)).match(this.keyMatcher)&&c.push(b.replace(this.keyMatcher,""));for(a=c.length;a--;)this.remove(c[a]);return this},isExpired:function(a){var d=e(f.getItem(this._ttlKey(a)));return b.isNumber(d)&&c()>d?!0:!1}}:{get:b.noop,set:b.noop,remove:b.noop,clear:b.noop,isExpired:b.noop},b.mixin(a.prototype,g),a}(),g=function(){"use strict";function c(b){b=b||{},this.cancelled=!1,this.lastUrl=null,this._send=b.transport?d(b.transport):a.ajax,this._get=b.rateLimiter?b.rateLimiter(this._get):this._get,this._cache=b.cache===!1?new e(0):i}function d(c){return function(d,e){function f(a){b.defer(function(){h.resolve(a)})}function g(a){b.defer(function(){h.reject(a)})}var h=a.Deferred();return c(d,e,f,g),h}}var f=0,g={},h=6,i=new e(10);return c.setMaxPendingRequests=function(a){h=a},c.resetCache=function(){i.reset()},b.mixin(c.prototype,{_get:function(a,b,c){function d(b){c&&c(null,b),k._cache.set(a,b)}function e(){c&&c(!0)}function i(){f--,delete g[a],k.onDeckRequestArgs&&(k._get.apply(k,k.onDeckRequestArgs),k.onDeckRequestArgs=null)}var j,k=this;this.cancelled||a!==this.lastUrl||((j=g[a])?j.done(d).fail(e):h>f?(f++,g[a]=this._send(a,b).done(d).fail(e).always(i)):this.onDeckRequestArgs=[].slice.call(arguments,0))},get:function(a,c,d){var e;return b.isFunction(c)&&(d=c,c={}),this.cancelled=!1,this.lastUrl=a,(e=this._cache.get(a))?b.defer(function(){d&&d(null,e)}):this._get(a,c,d),!!e},cancel:function(){this.cancelled=!0}}),c}(),h=function(){"use strict";function c(b){b=b||{},b.datumTokenizer&&b.queryTokenizer||a.error("datumTokenizer and queryTokenizer are both required"),this.datumTokenizer=b.datumTokenizer,this.queryTokenizer=b.queryTokenizer,this.reset()}function d(a){return a=b.filter(a,function(a){return!!a}),a=b.map(a,function(a){return a.toLowerCase()})}function e(){return{ids:[],children:{}}}function f(a){for(var b={},c=[],d=0,e=a.length;e>d;d++)b[a[d]]||(b[a[d]]=!0,c.push(a[d]));return c}function g(a,b){function c(a,b){return a-b}var d=0,e=0,f=[];a=a.sort(c),b=b.sort(c);for(var g=a.length,h=b.length;g>d&&h>e;)a[d]b[e]?e++:(f.push(a[d]),d++,e++);return f}return b.mixin(c.prototype,{bootstrap:function(a){this.datums=a.datums,this.trie=a.trie},add:function(a){var c=this;a=b.isArray(a)?a:[a],b.each(a,function(a){var f,g;f=c.datums.push(a)-1,g=d(c.datumTokenizer(a)),b.each(g,function(a){var b,d,g;for(b=c.trie,d=a.split("");g=d.shift();)b=b.children[g]||(b.children[g]=e()),b.ids.push(f)})})},get:function(a){var c,e,h=this;return c=d(this.queryTokenizer(a)),b.each(c,function(a){var b,c,d,f;if(e&&0===e.length)return!1;for(b=h.trie,c=a.split("");b&&(d=c.shift());)b=b.children[d];return b&&0===c.length?(f=b.ids.slice(0),void(e=e?g(e,f):f)):(e=[],!1)}),e?b.map(f(e),function(a){return h.datums[a]}):[]},reset:function(){this.datums=[],this.trie=e()},serialize:function(){return{datums:this.datums,trie:this.trie}}}),c}(),i=function(){"use strict";function d(a){return a.local||null}function e(d){var e,f;return f={url:null,thumbprint:"",ttl:864e5,filter:null,ajax:{}},(e=d.prefetch||null)&&(e=b.isString(e)?{url:e}:e,e=b.mixin(f,e),e.thumbprint=c+e.thumbprint,e.ajax.type=e.ajax.type||"GET",e.ajax.dataType=e.ajax.dataType||"json",!e.url&&a.error("prefetch requires url to be set")),e}function f(c){function d(a){return function(c){return b.debounce(c,a)}}function e(a){return function(c){return b.throttle(c,a)}}var f,g;return g={url:null,cache:!0,wildcard:"%QUERY",replace:null,rateLimitBy:"debounce",rateLimitWait:300,send:null,filter:null,ajax:{}},(f=c.remote||null)&&(f=b.isString(f)?{url:f}:f,f=b.mixin(g,f),f.rateLimiter=/^throttle$/i.test(f.rateLimitBy)?e(f.rateLimitWait):d(f.rateLimitWait),f.ajax.type=f.ajax.type||"GET",f.ajax.dataType=f.ajax.dataType||"json",delete f.rateLimitBy,delete f.rateLimitWait,!f.url&&a.error("remote requires url to be set")),f}return{local:d,prefetch:e,remote:f}}();!function(c){"use strict";function e(b){b&&(b.local||b.prefetch||b.remote)||a.error("one of local, prefetch, or remote is required"),this.limit=b.limit||5,this.sorter=j(b.sorter),this.dupDetector=b.dupDetector||k,this.local=i.local(b),this.prefetch=i.prefetch(b),this.remote=i.remote(b),this.cacheKey=this.prefetch?this.prefetch.cacheKey||this.prefetch.url:null,this.index=new h({datumTokenizer:b.datumTokenizer,queryTokenizer:b.queryTokenizer}),this.storage=this.cacheKey?new f(this.cacheKey):null}function j(a){function c(b){return b.sort(a)}function d(a){return a}return b.isFunction(a)?c:d}function k(){return!1}var l,m;return l=c.Bloodhound,m={data:"data",protocol:"protocol",thumbprint:"thumbprint"},c.Bloodhound=e,e.noConflict=function(){return c.Bloodhound=l,e},e.tokenizers=d,b.mixin(e.prototype,{_loadPrefetch:function(b){function c(a){f.clear(),f.add(b.filter?b.filter(a):a),f._saveToStorage(f.index.serialize(),b.thumbprint,b.ttl)}var d,e,f=this;return(d=this._readFromStorage(b.thumbprint))?(this.index.bootstrap(d),e=a.Deferred().resolve()):e=a.ajax(b.url,b.ajax).done(c),e},_getFromRemote:function(a,b){function c(a,c){b(a?[]:f.remote.filter?f.remote.filter(c):c)}var d,e,f=this;if(this.transport)return a=a||"",e=encodeURIComponent(a),d=this.remote.replace?this.remote.replace(this.remote.url,a):this.remote.url.replace(this.remote.wildcard,e),this.transport.get(d,this.remote.ajax,c)},_cancelLastRemoteRequest:function(){this.transport&&this.transport.cancel()},_saveToStorage:function(a,b,c){this.storage&&(this.storage.set(m.data,a,c),this.storage.set(m.protocol,location.protocol,c),this.storage.set(m.thumbprint,b,c))},_readFromStorage:function(a){var b,c={};return this.storage&&(c.data=this.storage.get(m.data),c.protocol=this.storage.get(m.protocol),c.thumbprint=this.storage.get(m.thumbprint)),b=c.thumbprint!==a||c.protocol!==location.protocol,c.data&&!b?c.data:null},_initialize:function(){function c(){e.add(b.isFunction(f)?f():f)}var d,e=this,f=this.local;return d=this.prefetch?this._loadPrefetch(this.prefetch):a.Deferred().resolve(),f&&d.done(c),this.transport=this.remote?new g(this.remote):null,this.initPromise=d.promise()},initialize:function(a){return!this.initPromise||a?this._initialize():this.initPromise},add:function(a){this.index.add(a)},get:function(a,c){function d(a){var d=f.slice(0);b.each(a,function(a){var c;return c=b.some(d,function(b){return e.dupDetector(a,b)}),!c&&d.push(a),d.length0||!this.transport)&&c&&c(f)},clear:function(){this.index.reset()},clearPrefetchCache:function(){this.storage&&this.storage.clear()},clearRemoteCache:function(){this.transport&&g.resetCache()},ttAdapter:function(){return b.bind(this.get,this)}}),e}(this);var j=function(){return{wrapper:'',dropdown:'',dataset:'
    ',suggestions:'',suggestion:'
    '}}(),k=function(){"use strict";var a={wrapper:{position:"relative",display:"inline-block"},hint:{position:"absolute",top:"0",left:"0",borderColor:"transparent",boxShadow:"none",opacity:"1"},input:{position:"relative",verticalAlign:"top",backgroundColor:"transparent"},inputWithNoHint:{position:"relative",verticalAlign:"top"},dropdown:{position:"absolute",top:"100%",left:"0",zIndex:"100",display:"none"},suggestions:{display:"block"},suggestion:{whiteSpace:"nowrap",cursor:"pointer"},suggestionChild:{whiteSpace:"normal"},ltr:{left:"0",right:"auto"},rtl:{left:"auto",right:" 0"}};return b.isMsie()&&b.mixin(a.input,{backgroundImage:"url(data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7)"}),b.isMsie()&&b.isMsie()<=7&&b.mixin(a.input,{marginTop:"-1px"}),a}(),l=function(){"use strict";function c(b){b&&b.el||a.error("EventBus initialized without el"),this.$el=a(b.el)}var d="typeahead:";return b.mixin(c.prototype,{trigger:function(a){var b=[].slice.call(arguments,1);this.$el.trigger(d+a,b)}}),c}(),m=function(){"use strict";function a(a,b,c,d){var e;if(!c)return this;for(b=b.split(i),c=d?h(c,d):c,this._callbacks=this._callbacks||{};e=b.shift();)this._callbacks[e]=this._callbacks[e]||{sync:[],async:[]},this._callbacks[e][a].push(c);return this}function b(b,c,d){return a.call(this,"async",b,c,d)}function c(b,c,d){return a.call(this,"sync",b,c,d)}function d(a){var b;if(!this._callbacks)return this;for(a=a.split(i);b=a.shift();)delete this._callbacks[b];return this}function e(a){var b,c,d,e,g;if(!this._callbacks)return this;for(a=a.split(i),d=[].slice.call(arguments,1);(b=a.shift())&&(c=this._callbacks[b]);)e=f(c.sync,this,[b].concat(d)),g=f(c.async,this,[b].concat(d)),e()&&j(g);return this}function f(a,b,c){function d(){for(var d,e=0,f=a.length;!d&&f>e;e+=1)d=a[e].apply(b,c)===!1;return!d}return d}function g(){var a;return a=window.setImmediate?function(a){setImmediate(function(){a()})}:function(a){setTimeout(function(){a()},0)}}function h(a,b){return a.bind?a.bind(b):function(){a.apply(b,[].slice.call(arguments,0))}}var i=/\s+/,j=g();return{onSync:c,onAsync:b,off:d,trigger:e}}(),n=function(a){"use strict";function c(a,c,d){for(var e,f=[],g=0,h=a.length;h>g;g++)f.push(b.escapeRegExChars(a[g]));return e=d?"\\b("+f.join("|")+")\\b":"("+f.join("|")+")",c?new RegExp(e):new RegExp(e,"i")}var d={node:null,pattern:null,tagName:"strong",className:null,wordsOnly:!1,caseSensitive:!1};return function(e){function f(b){var c,d,f;return(c=h.exec(b.data))&&(f=a.createElement(e.tagName),e.className&&(f.className=e.className),d=b.splitText(c.index),d.splitText(c[0].length),f.appendChild(d.cloneNode(!0)),b.parentNode.replaceChild(f,d)),!!c}function g(a,b){for(var c,d=3,e=0;e
    diff --git a/src/Templates/SIS/tabs/piss.twig b/src/Templates/SIS/tabs/piss.twig deleted file mode 100755 index 8fc68755d..000000000 --- a/src/Templates/SIS/tabs/piss.twig +++ /dev/null @@ -1,4 +0,0 @@ -
    - {{tab.vars.piss.content | raw}} -
    {{tab.vars.piss.author}} on {{tab.vars.piss.posted}}
    -
    diff --git a/src/Templates/SIS/tabs/schedule.twig b/src/Templates/SIS/tabs/schedule.twig deleted file mode 100755 index b9d074454..000000000 --- a/src/Templates/SIS/tabs/schedule.twig +++ /dev/null @@ -1,14 +0,0 @@ -
    -
    -

    On Air:

    -
    -

    -

    -
    -
    -
    -
    -

    Coming Up:

    -
    - -
    \ No newline at end of file diff --git a/src/Templates/SIS/tabs/tracklist.twig b/src/Templates/SIS/tabs/tracklist.twig deleted file mode 100755 index d5ccffea3..000000000 --- a/src/Templates/SIS/tabs/tracklist.twig +++ /dev/null @@ -1,22 +0,0 @@ -
    - -
    -
    - -
    -
    -
    - - - - - - -
    -
    -
    - SIS could not find --- by --- in the library. Continue?
    -
    -
    -
    -
    \ No newline at end of file diff --git a/src/Templates/Scheduler/allocate.twig b/src/Templates/Scheduler/allocate.twig index 9c447470d..3f0e09e1e 100644 --- a/src/Templates/Scheduler/allocate.twig +++ b/src/Templates/Scheduler/allocate.twig @@ -1,20 +1,29 @@ {% extends 'form.twig' %} {% block stripecontent %} -

    You are allocating Timeslots for a Season of {{frm_custom.name}} in the current term.

    -
    +

    You are allocating Timeslots for a Season of {{frm_custom.title}} in the current term.

    +
    {{frm_custom.description|raw}}
      {% for credit in frm_custom.credits %} -
    • {{credit.type_name}}: {{credit.name}}
    • +
    • {{credit.type_name}}: {{credit.User.fname}} {{credit.User.sname}}
    • {% endfor %}
    + +{% for credit in frm_custom.credits %} +{% if credit.User.contract_signed != 1 %} +
    {{credit.User.name}} has not signed the Presenter's Contract
    +{% endif %} +{% endfor %} + {{ parent() }} - +
    {% endblock %} {% block foot %} {{ parent() }} - -{% endblock %} \ No newline at end of file + + + +{% endblock %} diff --git a/src/Templates/Scheduler/createDemo.twig b/src/Templates/Scheduler/createDemo.twig deleted file mode 100644 index cf7fa5f02..000000000 --- a/src/Templates/Scheduler/createDemo.twig +++ /dev/null @@ -1,9 +0,0 @@ -{% extends 'form.twig' %} -{% block head %} -{{ parent() }} -{% endblock %} -{% block stripecontent %} -

    All demos will last one hour, take a maximum of two participants and happen in Studio 1. You will be registered as the Trainer for this session.

    -{{ parent() }} - -{% endblock %} \ No newline at end of file diff --git a/src/Templates/Scheduler/createSeason.twig b/src/Templates/Scheduler/createSeason.twig index 90afe83c3..1b9579396 100644 --- a/src/Templates/Scheduler/createSeason.twig +++ b/src/Templates/Scheduler/createSeason.twig @@ -2,10 +2,10 @@ {% block stripecontent %} {% if frm_custom.current_term is null %} -
    Season applications are currently closed. You can apply for a season 28 days before the start of term.
    +
    Season applications are currently closed. You can apply for a season 28 days before the start of term.
    {% else %} -

    Apply for a New Season

    -
    You are applying for a season for the Term {{frm_custom.current_term}}
    +

    Apply for a New Season!

    +
    You are applying for a season in Term {{frm_custom.current_term}}

    Fill out the form below to apply for a new season of {{frm_custom.show_title}}. Please provide at least three possible times for your show, and don't tick a week if you know you can't do it!

    {{ parent() }} {% endif %} @@ -13,5 +13,5 @@ {% block foot %} {{ parent() }} - -{% endblock %} \ No newline at end of file + +{% endblock %} diff --git a/src/Templates/Scheduler/createShow.twig b/src/Templates/Scheduler/createShow.twig index fd13d4e90..f725c8864 100644 --- a/src/Templates/Scheduler/createShow.twig +++ b/src/Templates/Scheduler/createShow.twig @@ -9,5 +9,5 @@ {% block foot %} {{ parent() }} - -{% endblock %} \ No newline at end of file + +{% endblock %} diff --git a/src/Templates/Scheduler/listTerms.twig b/src/Templates/Scheduler/listTerms.twig new file mode 100644 index 000000000..d7c0bf5f7 --- /dev/null +++ b/src/Templates/Scheduler/listTerms.twig @@ -0,0 +1,6 @@ +{% extends 'table.twig' %} +{% import 'macros.twig' as MyRadio %} +{% block stripecontent %} +Add a new Term +{{ parent() }} +{% endblock %} diff --git a/src/Templates/Scheduler/myShows.twig b/src/Templates/Scheduler/myShows.twig index 4f9bf91f9..a816aac30 100644 --- a/src/Templates/Scheduler/myShows.twig +++ b/src/Templates/Scheduler/myShows.twig @@ -2,4 +2,4 @@ {% block stripecontent %} {{ parent() }} Help me get started -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/Templates/Scheduler/stop.twig b/src/Templates/Scheduler/stop.twig index efcbd54c7..22d62ea81 100644 --- a/src/Templates/Scheduler/stop.twig +++ b/src/Templates/Scheduler/stop.twig @@ -15,7 +15,7 @@

    You should push this button if:

      -
    • The Queen or another named person has died, or you believe they have died
    • +
    • The King or another named person has died, or you believe they have died
    • Another major destructive event has occured (e.g. a terrorist attack, nuclear war)
    @@ -27,41 +27,40 @@
  • You have run out of bacon
  • {% endif %} -{% if stage != 0 %}

    You are on step {{stage}} of 3.

    {% endif %} +{% if stage != 0 %}

    You are on step {{stage}} of 3.

    {% endif %} {% if stage == 1 %}
    - +
    {% endif %} {% if stage == 2 %} {% if not result %} -

    That wasn't you first show. If you've never done a show, you can't use this.

    +

    That wasn't you first show. If you've never done a show, you can't use this.

    {% endif %}

    Okay. Please enter the title of your first ever show on the station:

    -
    - +
    +
    {% endif %} {% if stage == 3 %}
    -

    Enter your University username (without @york.ac.uk) below to cease broadcasting.

    +

    Enter your username (the part before the @) below to cease broadcasting.

    -
    - +
    +
    {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/Templates/Setup/dbdata.twig b/src/Templates/Setup/dbdata.twig new file mode 100644 index 000000000..780e8b6e1 --- /dev/null +++ b/src/Templates/Setup/dbdata.twig @@ -0,0 +1,25 @@ +{% extends 'minimal.twig' %} +{% block content %} +

    Since you're just getting started with MyRadio, we'd like to know how much you want us to set up for you. There's options from a complete, ready-to-go set of roles and permissions to a completely blank slate.

    +

    When MyRadio is updated, new options will be made available according to the choice you make here.

    +

    +

    + Start me off with everything I need to get going. +
    +

    +

    +

    + Start me off with permissions and mappings to actions, but don't create any roles for me. +
    +

    +

    +

    + Make all actions available to all logged-in users. Managing actions and permissions is disabled. +
    +

    +

    +

    + Don't do anything except give me access to the pages I need to set things up myself. +
    +

    +{% endblock %} diff --git a/src/Templates/Setup/dbdata_warning.twig b/src/Templates/Setup/dbdata_warning.twig new file mode 100644 index 000000000..3fabd352e --- /dev/null +++ b/src/Templates/Setup/dbdata_warning.twig @@ -0,0 +1,9 @@ +{% extends 'minimal.twig' %} +{% block content %} +

    Some warnings occured whilst performing that action. Please review the information below, make necessary adjustments, then click here to continue.

    +
      +{% for warning in warnings %} +
    1. {{warning}}
    2. +{% endfor %} +
    +{% endblock %} diff --git a/src/Templates/Setup/dbschema.twig b/src/Templates/Setup/dbschema.twig new file mode 100644 index 000000000..0db7d974a --- /dev/null +++ b/src/Templates/Setup/dbschema.twig @@ -0,0 +1,22 @@ +{% extends 'minimal.twig' %} +{% block content %} +

    Your database settings are correct, and MyRadio has now looked at the database and decided what to do next. Review the information below before continuing.

    +{% if operation == 'ERROR' %} +
    MyRadio has failed to autodetect what to do with your current database schema. Manual work is likely required.
    +{% elseif operation == 'NEW' %} +

    It looks like you don't have a current or previous MyRadio schema in the target database. MyRadio will set up a complete new schema.

    +{% elseif operation == 'UPGRADE' %} +

    There is an existing MyRadio schema in the target database, but it is not the latest version. MyRadio will upgrade this schema. Once the upgrade is complete, older versions of MyRadio may no longer work.

    +{% elseif operation == 'NEWER_WARN' %} +
    There is already a MyRadio schema in the target database, and it is running a new version of MyRadio than the one you are currently trying to set up. MyRadio will not alter the schema, however it may not work properly. Please consider upgrading to the latest version of MyRadio.
    +{% elseif operation == 'CURRENT' %} +

    The target database already contains the latest version of the MyRadio schema. MyRadio setup will not do anything to the database at this stage.

    +{% else %} +

    I'm confused and don't entirely remember waht to do with "{{operation}}"

    +{% endif %} +{% if operation != 'ERROR' %} +
    + (this may take several minutes, depending on the operation) +
    +{% endif %} +{% endblock %} diff --git a/src/Templates/Setup/dbschema_error.twig b/src/Templates/Setup/dbschema_error.twig new file mode 100644 index 000000000..fb554530c --- /dev/null +++ b/src/Templates/Setup/dbschema_error.twig @@ -0,0 +1,8 @@ +{% extends 'minimal.twig' %} +{% block content %} +

    An error occurred attempting to configure the MyRadio schema. Review the error below, make necessary changes, then retry.

    +

    {{error}}

    +
    + (this may take several minutes, depending on the operation) +
    +{% endblock %} diff --git a/src/Templates/Setup/dbserver.twig b/src/Templates/Setup/dbserver.twig new file mode 100644 index 000000000..0736c35cf --- /dev/null +++ b/src/Templates/Setup/dbserver.twig @@ -0,0 +1,17 @@ +{% extends 'minimal.twig' %} +{% block content %} +{% if db_error %} +
    Failed to connect to database server. Please check your settings and try again.
    +{% endif %} +

    Currently, MyRadio only supports PostgreSQL as a database server.

    +

    Please fill in the details of your server below, with a login that has full control over the database it will be working in.

    +
    +
    +
    +
    +
    +
    + +
    +
    +{% endblock %} diff --git a/src/Templates/Setup/strings.twig b/src/Templates/Setup/strings.twig new file mode 100644 index 000000000..947615247 --- /dev/null +++ b/src/Templates/Setup/strings.twig @@ -0,0 +1,23 @@ +{% extends 'minimal.twig' %} +{% block content %} +

    This page gives you lots of Configurables! Set these up how you want them to be. You can always change these later.

    +
    + +{% for item in short %} + + + + + +{% endfor %} +{% for item in long %} + + + + + +{% endfor %} +
    {{item[2]|raw}}
    {{item[2]|raw}}
    + +
    +{% endblock %} diff --git a/src/Templates/Setup/user.twig b/src/Templates/Setup/user.twig new file mode 100644 index 000000000..6503a7529 --- /dev/null +++ b/src/Templates/Setup/user.twig @@ -0,0 +1,26 @@ +{% extends 'minimal.twig' %} +{% block content %} +

    To finish up, tell us a little about yourself so we can create you an account.

    +
    + +
    + +
    +
    + +
    + +
    +
    + +
    + +
    {{pass_error}} +
    + +
    +{% endblock %} diff --git a/src/Templates/Timelord/main.twig b/src/Templates/Timelord/main.twig deleted file mode 100644 index e21388ba1..000000000 --- a/src/Templates/Timelord/main.twig +++ /dev/null @@ -1,32 +0,0 @@ -{% spaceless %} -{% import 'macros.twig' as myury %} - - - - - {% include 'parts/base_head.twig' %} - - - -
    Studio X is On Air
    -
    00:00:00
    -
    1st January 1970
    -
    -
    - (( - Jukebox - ))
    -
    Up Next: Jukebox @ 00:00am
    Jukebox @ 00:00am
    -
    -
    Studio 1
    -
    Studio 2
    -
    OB 1
    -
    OB 2
    -
    OBIT
    -
    DEAD AIR
    -
    - {% include 'parts/base_foot.twig' %} - - - -{% endspaceless %} \ No newline at end of file diff --git a/src/Templates/Training/createDemo.twig b/src/Templates/Training/createDemo.twig new file mode 100644 index 000000000..a653067fe --- /dev/null +++ b/src/Templates/Training/createDemo.twig @@ -0,0 +1,9 @@ +{% extends 'form.twig' %} +{% block head %} +{{ parent() }} +{% endblock %} +{% block stripecontent %} +

    All sessions will last one hour, take a maximum of two participants and happen in a free studio. You will be registered as the Trainer for this session.

    +{{ parent() }} + +{% endblock %} diff --git a/src/Templates/Webcam/focus.twig b/src/Templates/Webcam/focus.twig index f4b9d90fe..a7f744bfd 100644 --- a/src/Templates/Webcam/focus.twig +++ b/src/Templates/Webcam/focus.twig @@ -1,24 +1,43 @@ {% extends 'stripe.twig' %} +{% block head %} +{{ parent() }} + +{% endblock %} + {% block stripecontent %} -
    - -
    {{live.streamname}}
    -
    -
    - {% for stream in streams %} -
    - -
    {{stream.streamname}}
    -
    - {% endfor %} -
    -
    - You've been watching the webcams for not very long -
    + +{% if live %} +
    + +
    {{live.streamname}}
    +
    +
    + {% for stream in streams %} +
    + +
    {{stream.streamname}}
    + +
    + {% endfor %} +
    +
    + You've been watching the webcams for (loading). +
    +{% else %} +
    + No webcams have been setup yet. Please check the database. +
    +{% endif %} {% endblock %} {% block foot %} {{ parent() }} {% include 'Webcam/tracker.twig' %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/Templates/Webcam/grid.twig b/src/Templates/Webcam/grid.twig index 4a3765d1b..e66d08bf7 100644 --- a/src/Templates/Webcam/grid.twig +++ b/src/Templates/Webcam/grid.twig @@ -1,20 +1,26 @@ {% extends 'stripe.twig' %} {% block stripecontent %} +{% if streams %}
    {% for stream in streams %}
    - +
    {{stream.streamname}}
    {% endfor %}
    -
    - You've been watching the webcams for not very long +
    + You've been watching the webcams for (loading).
    +{% else %} +
    + No webcams have been setup yet. Please check the database. +
    +{% endif %} {% endblock %} {% block foot %} {{ parent() }} {% include 'Webcam/tracker.twig' %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/Templates/Webcam/tracker.twig b/src/Templates/Webcam/tracker.twig index ab4598445..5e57831b8 100644 --- a/src/Templates/Webcam/tracker.twig +++ b/src/Templates/Webcam/tracker.twig @@ -1 +1 @@ - \ No newline at end of file + diff --git a/src/Templates/Website/bannerfrm.twig b/src/Templates/Website/bannerfrm.twig index a430f4cee..0588a8618 100644 --- a/src/Templates/Website/bannerfrm.twig +++ b/src/Templates/Website/bannerfrm.twig @@ -1,9 +1,12 @@ {% extends 'form.twig' %} {% block stripecontent %} {% if frm_custom.bannerName %} -

    You are editing a the {{frm_custom.bannerName}} Banner.

    +

    + {{frm_custom.bannerName}} Banner Preview + You are editing the {{frm_custom.bannerName}} Banner. +

    {% else %}

    You are creating a new Banner.

    {% endif %} {{ parent() }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/Templates/Website/banners.twig b/src/Templates/Website/banners.twig index 79ce32883..19819d6f9 100644 --- a/src/Templates/Website/banners.twig +++ b/src/Templates/Website/banners.twig @@ -4,4 +4,4 @@ This section enables you to create new Banners, and configure their Campaigns - when and where they appear.

    Create a new Banner | Help Me Get Started {{ parent() }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/Templates/Website/campaignfrm.twig b/src/Templates/Website/campaignfrm.twig index 8f599bfb5..bdc772f36 100644 --- a/src/Templates/Website/campaignfrm.twig +++ b/src/Templates/Website/campaignfrm.twig @@ -6,4 +6,4 @@

    You are creating a new Campaign for the Banner {{frm_custom.bannerName}}.

    {% endif %} {{ parent() }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/Templates/Website/campaigns.twig b/src/Templates/Website/campaigns.twig index 0df0cef8b..9646d563b 100644 --- a/src/Templates/Website/campaigns.twig +++ b/src/Templates/Website/campaigns.twig @@ -6,4 +6,4 @@ This section enables you to create new Banners, and configure their Campaign

    You are viewing Campaigns for the {{bannerName}} Banner.

    Create a new Campaign {{ parent() }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/Templates/Website/shortUrls.twig b/src/Templates/Website/shortUrls.twig new file mode 100644 index 000000000..909712308 --- /dev/null +++ b/src/Templates/Website/shortUrls.twig @@ -0,0 +1,6 @@ +{% extends 'table.twig' %} +{% block stripecontent %} +

    Short URLs let you create shorter versions of long, unwieldy URLs.

    +Create a new short URL +{{ parent() }} +{% endblock %} diff --git a/src/Templates/bargraph.twig b/src/Templates/bargraph.twig index 670b8ec05..23cc04358 100644 --- a/src/Templates/bargraph.twig +++ b/src/Templates/bargraph.twig @@ -20,4 +20,4 @@ chart.draw(data, options); } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/Templates/base.twig b/src/Templates/base.twig old mode 100755 new mode 100644 index bb5687259..85051771b --- a/src/Templates/base.twig +++ b/src/Templates/base.twig @@ -1,56 +1,52 @@ {% spaceless %} -{% import 'macros.twig' as myury %} +{% import 'macros.twig' as MyRadio %} {% block head %} {% include 'parts/base_head.twig' %} {% endblock %} - - - - {% if phperrors is not null %} -
      - {% for phperror in phperrors %} -
    • {{ phperror.name }} : {{ phperror.string|raw }} - In {{ phperror.file }} on line {{ phperror.line }}
    • - {% endfor %} -
    - {% endif %} -{% block content %}{% endblock %} -{% block footer %} -
    -
    - -
    - - -{% endblock %} + {% endif %} + {% for notice in notices %} +
     {{notice.message | raw}}
    + {% endfor %} + {% block content %}{% endblock %} +
    + {% block footer %} + + {% endblock %} -{% block foot %} -{% include 'parts/base_foot.twig' %} -{% endblock %} + {% block foot %} + {% include 'parts/base_foot.twig' %} + {% endblock %} -{% if joyride %} -{% include 'joyrides/base.twig' %} -{% include 'joyrides/' ~ joyride ~ '.twig' %} -{% endif %} - - -{% endspaceless %} \ No newline at end of file + {% if joyride %} + {% include 'joyrides/base.twig' %} + {% include 'joyrides/' ~ joyride ~ '.twig' %} + {% endif %} + + +{% endspaceless %} diff --git a/src/Templates/csv.twig b/src/Templates/csv.twig index df2b306cb..f500619ac 100644 --- a/src/Templates/csv.twig +++ b/src/Templates/csv.twig @@ -1,9 +1,9 @@ -{# -# Transforms an array of data into a CSV file. Input format: -# data[0]['column header']: content -# The first row of output will contain column headers. -#}{% for title, column in data[0] %}"{{ title|raw }}"{% if not loop.last %},{% endif %}{% endfor %} - -{% for row in data %}{% for title, column in row %}"{{column|replace({'"': '\\"'})|raw}}"{% if not loop.last %},{% endif %}{% endfor %} - -{% endfor %} \ No newline at end of file +{# +# Transforms an array of data into a CSV file. Input format: +# data[0]['column header']: content +# The first row of output will contain column headers. +#}{% for title, column in data[0] %}"{{ title|raw }}"{% if not loop.last %},{% endif %}{% endfor %} + +{% for row in data %}{% for title, column in row %}"{{column|replace({'"': '\\"'})|raw}}"{% if not loop.last %},{% endif %}{% endfor %} + +{% endfor %} diff --git a/src/Templates/error.twig b/src/Templates/error.twig index 26a2abaf2..c418b0dc5 100644 --- a/src/Templates/error.twig +++ b/src/Templates/error.twig @@ -1,5 +1,5 @@ {% extends 'stripe.twig' %} {% block stripecontent %} -
    {{ body | raw}}
    +
    {{ body | raw}}
    Go back -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/Templates/form.twig b/src/Templates/form.twig index 9dc2a87b5..624ccecd6 100644 --- a/src/Templates/form.twig +++ b/src/Templates/form.twig @@ -2,10 +2,11 @@ {% block head %} {{ parent() }} {% include 'parts/table-header.twig' %} + {% endblock %} {% block stripecontent %} -{% set myury_form_section_exists = false %} -{% set myury_form_file_progress = false %} +{% set myradio_form_section_exists = false %} +{% set myradio_form_file_progress = false %}
    {% for field in frm_fields %} @@ -25,18 +26,20 @@ {% if captcha %}
    {{ captcha | raw }}
    {% endif %} - +
    -
    Starting Upload...
    - +
    Starting Upload...
    +
    {% endblock %} {% block foot %} {{ parent() }} {% include 'parts/table-footer.twig' %} - - - - -{% endblock %} \ No newline at end of file + + + + + + +{% endblock %} diff --git a/src/Templates/iTones/configurePlaylist.twig b/src/Templates/iTones/configurePlaylist.twig new file mode 100644 index 000000000..ee4402627 --- /dev/null +++ b/src/Templates/iTones/configurePlaylist.twig @@ -0,0 +1,15 @@ +{% extends 'form.twig' %} +{% import 'macros.twig' as MyRadio %} +{% set tabledata = frm_custom.tabledata %} +{% set tablescript = 'myradio.iTones.configurePlaylist' %} +{% block stripecontent %} +{{ parent() }} +

    Availabilities

    +{% if frm_custom.playlistid %} +Add an Availability +

    This playlist is active on the following schedules:

    +{% include 'parts/table-content.twig' %} +{% else %} +

    You need to create this playlist before you can edit its Availability.

    +{% endif %} +{% endblock %} diff --git a/src/Templates/iTones/default.twig b/src/Templates/iTones/default.twig index cf6420bdb..d46e795da 100644 --- a/src/Templates/iTones/default.twig +++ b/src/Templates/iTones/default.twig @@ -3,4 +3,4 @@

    Welcome the the Campus Jukebox Manager. These pages allow you to control what the Campus Jukebox plays out on air. This includes editing the playlists, requesting tracks, or scheduling the playout of pre-recorded material.

    {{ parent() }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/Templates/iTones/editPlaylist.twig b/src/Templates/iTones/editPlaylist.twig index 2ea90377e..f75594c5d 100644 --- a/src/Templates/iTones/editPlaylist.twig +++ b/src/Templates/iTones/editPlaylist.twig @@ -7,5 +7,5 @@ {% block foot %} {{ parent() }} - -{% endblock %} \ No newline at end of file + +{% endblock %} diff --git a/src/Templates/iTones/listPlaylists.twig b/src/Templates/iTones/listPlaylists.twig new file mode 100644 index 000000000..529426eb3 --- /dev/null +++ b/src/Templates/iTones/listPlaylists.twig @@ -0,0 +1,16 @@ +{% extends 'table.twig' %} +{% import 'macros.twig' as MyRadio %} +{% block stripecontent %} +Add a new Playlist +
    + + +
    +{{ parent() }} +{% endblock %} diff --git a/src/Templates/joyrides/banner_intro.twig b/src/Templates/joyrides/banner_intro.twig index ff4160da7..f89b96d5f 100644 --- a/src/Templates/joyrides/banner_intro.twig +++ b/src/Templates/joyrides/banner_intro.twig @@ -1,4 +1,4 @@ -
      + - \ No newline at end of file + diff --git a/src/Templates/joyrides/base.twig b/src/Templates/joyrides/base.twig index bdc46522a..6d2f286fa 100644 --- a/src/Templates/joyrides/base.twig +++ b/src/Templates/joyrides/base.twig @@ -1,2 +1,2 @@ - - \ No newline at end of file + + diff --git a/src/Templates/joyrides/first_show.twig b/src/Templates/joyrides/first_show.twig index b4443b281..3e9df0f50 100644 --- a/src/Templates/joyrides/first_show.twig +++ b/src/Templates/joyrides/first_show.twig @@ -1,4 +1,4 @@ -
        + - \ No newline at end of file + diff --git a/src/Templates/linegraph.twig b/src/Templates/linegraph.twig index 1faa03378..45310e169 100644 --- a/src/Templates/linegraph.twig +++ b/src/Templates/linegraph.twig @@ -22,4 +22,4 @@ chart.draw(data, options); } -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/Templates/macros.twig b/src/Templates/macros.twig index 0b3a6354f..fbaf0ab22 100644 --- a/src/Templates/macros.twig +++ b/src/Templates/macros.twig @@ -1,7 +1,7 @@ {% macro makeURL(config, module, action, options) %} {{config.base_url}} {% if config.rewrite_url -%}{{module|default('default_module')}}/{{action|default('default_action')}}/{% +%}{{module|default(config.default_module)}}/{{action|default(config.default_action)}}/{% if options %}?{% endif %}{% else %}?module={{module|default(config.default_module)}}&action={{action|default(config.default_action)}}{% @@ -9,4 +9,34 @@ if options %}&{% endif %}{% endif %}{% for i in options|keys %}{% if not loop.first %}&{% endif %}{{i}}={{options[i]}}{% endfor %} -{% endmacro %} \ No newline at end of file +{% endmacro %} + +{% macro makeAPI(config, module, action, options, id) %} +{{config.api_url}}/{{module}}{% if id %}/{{id}}{% endif %}/{{action}}/{% +if options %}?{% endif %}{% +for i in options|keys %}{% +if not loop.first %}&{% endif %}{{i}}={{options[i]}}{% +endfor %} +{% endmacro %} + +{% macro linkAPI(name, config, method, module, action, options, id) -%} + +{%- endmacro %} diff --git a/src/Templates/minimal.twig b/src/Templates/minimal.twig new file mode 100644 index 000000000..89a094d54 --- /dev/null +++ b/src/Templates/minimal.twig @@ -0,0 +1,47 @@ +{# + This minimal template is mainly used for the setup wizard + full templates may not work for many stages of an install. +#} + + + Welcome to MyRadio + + + + + + + + + + + +
        +
        +

        {{ title }}

        + {% block content %} + {{content|nl2br}} + {% endblock %} + {% block rawcontent %} + {{rawcontent|raw}} + {% endblock %} +
        +
        +
        +
        +
        + MyRadio by University Radio York +
        +
        +
        +
        + + diff --git a/src/Templates/parts/base_foot.twig b/src/Templates/parts/base_foot.twig old mode 100755 new mode 100644 index f3d9559db..e7da2ba38 --- a/src/Templates/parts/base_foot.twig +++ b/src/Templates/parts/base_foot.twig @@ -1,5 +1,9 @@ - - - - - \ No newline at end of file + + + + diff --git a/src/Templates/parts/base_head.twig b/src/Templates/parts/base_head.twig index d01f12bf6..3310e9ea8 100644 --- a/src/Templates/parts/base_head.twig +++ b/src/Templates/parts/base_head.twig @@ -1,9 +1,10 @@ {{ title }} | {{config.long_name}} + + - - - + - + + diff --git a/src/Templates/parts/nav.twig b/src/Templates/parts/nav.twig new file mode 100644 index 000000000..2008daf1f --- /dev/null +++ b/src/Templates/parts/nav.twig @@ -0,0 +1,56 @@ +{% import 'macros.twig' as MyRadio %} +{% if nonav == false %} + {% if name == 'Alex Towells' %} +