diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 43c65f365..40e219551 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,6 +1,6 @@ # Select image from https://hub.docker.com/_/php/ #image: php:7.2 -# Use a prepared Hubzilla image to optimise pipeline run +# Use a prepared Hubzilla image to optimise pipeline duration image: registry.gitlab.com/dawnbreak/hubzilla/core:php7.2 @@ -32,55 +32,28 @@ variables: before_script: +# pecl and composer do not work with PHP production restrictions (from Hubzilla Docker image) +- if [ -f /usr/local/etc/php/conf.d/z_prod.ini ]; then mv /usr/local/etc/php/conf.d/z_prod.ini /usr/local/etc/php/conf.d/z_prod.ini.off; fi # Install & enable Xdebug for code coverage reports - pecl install xdebug - docker-php-ext-enable xdebug # Install composer - curl -sS https://getcomposer.org/installer | php # Install dev libraries from composer -- php composer.phar install --no-progress +- php ./composer.phar install --no-progress -# test PHP7 with MySQL 5.7 -php7.2_mysql 1/2: +# hidden job definition with template for MySQL/MariaDB +.job_template_mysql: &job_definition_mysql stage: test - services: - - mysql:5.7 script: - echo "USE $MYSQL_DATABASE; $(cat ./install/schema_mysql.sql)" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mysql "$MYSQL_DATABASE" - echo "SHOW DATABASES;" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mysql "$MYSQL_DATABASE" - echo "USE $MYSQL_DATABASE; SHOW TABLES;" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mysql "$MYSQL_DATABASE" - vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-text - -# test PHP7 with MySQL latest (8) -php7.2_mysql 2/2: - stage: test - services: - - name: mysql:latest - command: ["--default-authentication-plugin=mysql_native_password"] - script: - - echo "USE $MYSQL_DATABASE; $(cat ./install/schema_mysql.sql)" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mysql "$MYSQL_DATABASE" - - echo "SHOW DATABASES;" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mysql "$MYSQL_DATABASE" - - echo "USE $MYSQL_DATABASE; SHOW TABLES;" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mysql "$MYSQL_DATABASE" - - vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-text - - -# test PHP7 with MariaDB latest (10.3) -php7.2_mariadb: - stage: test - services: - - name: mariadb:latest - alias: mysql - script: - - echo "USE $MYSQL_DATABASE; $(cat ./install/schema_mysql.sql)" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mysql "$MYSQL_DATABASE" - - echo "SHOW DATABASES;" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mysql "$MYSQL_DATABASE" - - echo "USE $MYSQL_DATABASE; SHOW TABLES;" | mysql --user=root --password="$MYSQL_ROOT_PASSWORD" --host=mysql "$MYSQL_DATABASE" - - vendor/bin/phpunit --configuration tests/phpunit.xml --coverage-text - - -# test PHP7 with PostgreSQL latest -php7.2_postgres: +# hidden job definition with template for PostgreSQL +.job_template_postgres: &job_definition_postgres stage: test services: - postgres:latest @@ -95,7 +68,10 @@ php7.2_postgres: #- psql -h "postgres" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -c "\dt;" # Run the actual tests - vendor/bin/phpunit --configuration tests/phpunit-pgsql.xml --testdox - artifacts: + +# hidden job definition with artifacts config template +.artifacts_template: + artifacts: &artifacts_template expire_in: 1 week # Gitlab should show the results, but has problems parsing PHPUnit's junit file. reports: @@ -106,7 +82,52 @@ php7.2_postgres: - tests/results/ -# Generate Doxygen API Documentation and deploy it at GitLab pages +# PHP7.2 with MySQL 5.7 +php7.2_mysql5.7: + <<: *job_definition_mysql + services: + - mysql:5.7 + + +# PHP7.2 with MySQL 8 (latest) +php7.2_mysql8: + <<: *job_definition_mysql + services: + - name: mysql:8 + command: ["--default-authentication-plugin=mysql_native_password"] + + +# PHP7.2 with MariaDB 10.2 +php7.2_mariadb10.2: + <<: *job_definition_mysql + services: + - name: mariadb:10.2 + alias: mysql + + +# PHP7.3 with MariaDB 10.3 (latest) +php7.3_mariadb10.3: + <<: *job_definition_mysql + image: registry.gitlab.com/dawnbreak/hubzilla/core:php7.3 + services: + - name: mariadb:10.3 + alias: mysql + + +# PHP7.2 with PostgreSQL latest (11) +php7.2_postgres11: + <<: *job_definition_postgres + artifacts: *artifacts_template + + +# PHP7.3 with PostgreSQL latest (11) +php7.3_postgres11: + <<: *job_definition_postgres + image: registry.gitlab.com/dawnbreak/hubzilla/core:php7.3 + artifacts: *artifacts_template + + +# Generate Doxygen API Documentation and deploy it as GitLab pages pages: stage: deploy cache: {} diff --git a/.homeinstall/README.md b/.homeinstall/README.md index d63931a84..45e1ba0e6 100644 --- a/.homeinstall/README.md +++ b/.homeinstall/README.md @@ -1,16 +1,43 @@ # Hubzilla at Home next to your Router -Run hubzilla-setup.sh for an unattended installation of hubzilla. +This readme will show you how to install and run Hubzilla or Zap at home. + +The installation is done by a script. + +What the script will do for you... + ++ install everything required by Zap/Hubzilla, basically a web server (Apache), PHP, a database (MySQL), certbot,... ++ create a database ++ run certbot to have everything for a secure connection (httpS) ++ create a script for daily maintenance + - backup to external disk (certificates, database, /var/www/) + - renew certfificate (letsencrypt) + - update of Zap/Hubzilla + - update of Debian + - restart ++ create cron jobs for + - DynDNS (selfHOST.de or freedns.afraid.org) every 5 minutes + - Master.php for Zap/Hubzilla every 10 minutes + - daily maintenance script every day at 05:30 The script is known to work without adjustments with + Hardware - - Mini-PC with Debian-9.5-amd64, or - - Rapberry 3 with Raspbian, Debian-9.5 + - Mini-PC with Debian 9 (stretch), or + - Rapberry 3 with Raspbian, Debian 9 + DynDNS - selfHOST.de - freedns.afraid.org +The script can install both [Hubzilla](https://zotlabs.org/page/hubzilla/hubzilla-project) and [Zap](https://zotlabs.com/zap/). Make sure to use the correct GIT repositories. + ++ Hubzilla + - core: git clone https://framagit.org/hubzilla/core.git html (in this readme) + - addons: util/add_addon_repo https://framagit.org/hubzilla/addons.git hzaddons (in hubzilla-setup.sh) ++ Zap + - core: git clone https://framagit.org/zot/zap.git html (in this readme) + - addons: util/add_addon_repo https://framagit.org/zot/zap-addons.git zaddons (in hubzilla-setup.sh) + ## Disclaimers - This script does work with Debian 9 only. @@ -29,7 +56,7 @@ Hardware Software + Fresh installation of Debian 9 (Stretch) -+ Router with open ports 80 and 443 for your Hub ++ Router with open ports 80 and 443 for your web server ## The basic steps (quick overview) @@ -44,10 +71,9 @@ Software - nano hubzilla-config.txt - Read the comments carefully - Enter your values: db pass, domain, values for dyn DNS - - Make sure your external drive (for backups) is mounted + - Prepare your external disk for backups - hubzilla-setup.sh as root - ... wait, wait, wait until the script is finised - - reboot + Open your domain with a browser and step throught the initial configuration of hubzilla. ## Troubleshooting @@ -66,58 +92,28 @@ In Admin settings of hubzilla or via terminal # Step-by-Step in Detail -## Preparations Hardware - -### Mini-PC - -### Recommended: USB Drive for Backups - -The installation will create a daily backup written to an external drive. - -The USB drive must be compatible with the filesystems - -- ext4 (if you do not want to encrypt the USB) -- LUKS + ext4 (if you want to encrypt the USB) - -The backup includes - -- Hubzilla DB -- Hubzilla installation /var/www/html -- Certificates for letsencrypt - ## Preparations Software -### Install Debian Linux on the Mini-PC +## Install Debian 9 -Download the stable Debian at https://www.debian.org/ -(Debian 8 is no longer supported.) +Provided you use a Raspberry Pi 3... -Create bootable USB drive with Debian on it.You could use +Download the OS Raspbian from https://www.raspberrypi.org/downloads/raspbian/ -- unetbootin, https://en.wikipedia.org/wiki/UNetbootin -- or simply the linux command "dd" +Follow the installation instruction there. -Example for command dd... +## Configure your Router - su - - dd if=2018-10-09-raspbian-stretch.img of=/dev/mmcblk0 +Your web has to be visible in the internet. -Do not forget to unmount the SD card before and check if unmounted like in this example... - - su - - umount /dev/mmcblk0* - df -h - - -Switch off your mini pc, plug in your USB drive and start the mini pc from the -stick. Install Debian. Follow the instructions of the installation. - -### Configure your Router - -Open the ports 80 and 443 on your router for your Debian +Open the ports 80 and 443 on your router for your Debian. Make sure your web server is marked as "exposed host". ## Preparations Dynamic IP Address +Follow the instructions in .homeinstall/hubzilla-config.txt. + +In short... + Your Hubzilla must be reachable by a domain that you can type in your browser cooldomain.org @@ -132,105 +128,15 @@ There are two ways to get a domain... ...for example buy at selfHOST.de -The cost are around 10,- € once and 1,50 € per month (2017). +The cost is 1,50 € per month (2019). ### Method 2: Register a free subdomain ...for example register at freedns.afraid.org -Follow the instructions in .homeinstall/hubzilla-config.txt. +## Note on Rasperry - -## Install Hubzilla on your Debian - -Login to your debian -(Provided your username is "you" and the name of the mini pc is "debian". You -could take the IP address instead of "debian") - - ssh -X you@debian - -Change to root user - - su -l - -Install git - - apt-get install git - -Make the directory for apache and change diretory to it - - mkdir /var/www - cd /var/www/ - -Clone hubzilla from git ("git pull" will update it later) - - git clone https://framagit.org/hubzilla/core.git html - -Change to the install script - - cd html/.homeinstall/ - -Copy the template file - - cp hubzilla-config.txt.template hubzilla-config.txt - -Modify the file "hubzilla-config.txt". Read the instructions there carefully and enter your values. - - nano hubzilla-config.txt - -Make sure your external drive (for backups) is plugged in and can be mounted as configured in "hubzilla-config.txt". Otherwise the daily backups will not work. - -Run the script - - ./hubzilla-setup.sh - -Wait... The script should not finish with an error message. - -In a webbrowser open your domain. -Expected: A test page of hubzilla is shown. All checks there should be -successfull. Go on... -Expected: A page for the Hubzilla server configuration shows up. - -Leave db server name "127.0.0.1" and port "0" untouched. - -Enter - -- DB user name = hubzilla -- DB pass word = This is the password you entered in "hubzilla-config.txt" -- DB name = hubzilla - -Leave db type "MySQL" untouched. - -Follow the instructions in the next pages. - -Recommended: Set path to imagemagick - -- in admin settings of hubzilla or -- via terminal - - util/config system.imagick_convert_path /usr/bin/convert - -After the daily script was executed at 05:30 (am) - -- look at /var/www/html/hubzilla-daily.log -- check your backup on the external drive -- optionally view the daily log under yourdomain.org/admin/logs/ - - set the logfile to var/www/html/hubzilla-daily.log - - -## Install Hubzilla in a Virtual Machine for Test Purposes - -Modify the file "hubzilla-config.txt". - - nano hubzilla-config.txt - -There use - - le_domain=localhost - -## Note for the Rasperry - -The script was tested with an Raspberry 3 under Raspian (Debian 9.5, 2018-10-09-raspbian-stretch.img). +The script was tested with an Raspberry 3 under Raspian, Debian 9. It is recommended to run the Raspi without graphical frontend (X-Server). Use... @@ -240,7 +146,7 @@ to boot the Rapsi to the client console. DO NOT FORGET TO CHANGE THE DEFAULT PASSWORD FOR USER PI! -If the validation of the mail address fails for the very first registered user... +On a Raspian Stretch (Debian 9) the validation of the mail address fails for the very first user. This used to happen on some *bsd distros but there was some work to fix that a year ago (2017). So if your system isn't registered in DNS or DNS isn't active do diff --git a/.homeinstall/hubzilla-config.txt.template b/.homeinstall/hubzilla-config.txt.template index e42da0e4e..f0bf6121c 100644 --- a/.homeinstall/hubzilla-config.txt.template +++ b/.homeinstall/hubzilla-config.txt.template @@ -2,8 +2,8 @@ ### MANDATORY - database password ############# # # Please give your database password +# It is better to not use blanks inside the password. # Example: db_pass=pass_word_with_no_blanks_in_it -# Example: db_pass="this password has blanks in it" db_pass= ############################################### @@ -18,9 +18,12 @@ db_pass= # Example: my.cooldomain.org # Example: cooldomain.org # -# Example: localhost (test installation without certificates for httpS) +# You might use "localhost" for a LOCAL TEST installation. +# This is usefull if you want to debug the server inside a VM. # -# Email is optional +# Example: localhost +# +# Email is optional if you use "localhost". # # le_domain= @@ -30,7 +33,7 @@ le_email= ### OPTIONAL - selfHOST - dynamic IP address ## # # 1. Register a domain at selfhost.de -# - choose offer "DOMAIN dynamisch" 1,50€/mon at 08.01.2016 +# - choose offer "DOMAIN dynamisch" 1,50€/mon at 04/2019 # 2. Get your configuration for dynamic IP update # - Log in at selfhost.de # - go to "DynDNS Accounte" diff --git a/.homeinstall/hubzilla-setup.sh b/.homeinstall/hubzilla-setup.sh old mode 100755 new mode 100644 index 1f3ad5db5..023ef7afc --- a/.homeinstall/hubzilla-setup.sh +++ b/.homeinstall/hubzilla-setup.sh @@ -3,7 +3,10 @@ # How to use # ---------- # -# This file automates the installation of hubzilla under Debian Linux +# This file automates the installation of +# - hubzilla: https://zotlabs.org/page/hubzilla/hubzilla-project and +# - zap: https://zotlabs.com/zap/ +# under Debian Linux # # 1) Copy the file "hubzilla-config.txt.template" to "hubzilla-config.txt" # Follow the instuctions there @@ -25,16 +28,14 @@ # * php, # * mysql - the database for hubzilla, # * phpmyadmin, -# * git to download and update hubzilla itself +# * git to download and update hubzilla addon # - download hubzilla core and addons # - configure cron -# * "poller.php" for regular background prozesses of hubzilla -# * to_do "apt-get update" and "apt-get dist-upgrade" to keep linux -# up-to-date -# * to_do backup hubzillas database and files (rsnapshot) -# - configure dynamic ip with cron -# - to_do letsencrypt -# - to_do redirection to https +# * "Master.php" for regular background prozesses of hubzilla +# * "apt-get update" and "apt-get dist-upgrade" and "apt-get autoremove" to keep linux up-to-date +# * run command to keep the IP up-to-date > DynDNS provided by selfHOST.de or freedns.afraid.org +# * backup hubzillas database and files (rsync) +# - letsencrypt # # # Discussion @@ -43,26 +44,11 @@ # Security - password is the same for mysql-server, phpmyadmin and hubzilla db # - The script runs into installation errors for phpmyadmin if it uses # different passwords. For the sake of simplicity one singel password. -# -# Security - suhosin for PHP -# - The script does not install suhosin. -# - Is the security package suhosin usefull or not usefull? # # Hubzilla - email verification # - The script switches off email verification off in all htconfig.tpl. # Example: /var/www/html/view/en/htconfig.tpl # - Is this a silly idea or not? -# -# -# Remove Hubzilla (for a fresh start using the script) -# ---------------------------------------------------- -# -# You could use /var/www/hubzilla-remove.sh -# that is created by hubzilla-setup.sh. -# -# The script will remove (almost everything) what was installed by the script. -# After the removal you could run the script again to have a fresh install -# of all applications including hubzilla and its database. # # How to restore from backup # -------------------------- @@ -76,18 +62,10 @@ # # hubzilla-daily.sh makes a (daily) backup of all relevant files # - /var/lib/mysql/ > hubzilla database -# - /var/www/html/ > hubzilla from github -# - /var/www/letsencrypt/ > certificates +# - /var/www/ > hubzilla/zap from github +# - /etc/letsencrypt/ > certificates # -# hubzilla-daily.sh writes the backup -# - either to an external disk compatible to LUKS+ext4 (see hubzilla-config.txt) -# - or to /var/cache/rsnapshot in case the external disk is not plugged in -# -# Restore backup -# - - - - - - - -# -# This was not tested yet. -# Bacically you can copy the files from the backup to the server. +# hubzilla-daily.sh writes the backup to an external disk compatible to LUKS+ext4 (see hubzilla-config.txt) # # Credits # ------- @@ -136,11 +114,11 @@ function check_config { # backup is important and should be checked if [ -n "$backup_device_name" ] then - if [ ! -d "$backup_mount_point" ] - then - mkdir "$backup_mount_point" - fi - device_mounted=0 + if [ ! -d "$backup_mount_point" ] + then + mkdir "$backup_mount_point" + fi + device_mounted=0 if fdisk -l | grep -i "$backup_device_name.*linux" then print_info "ok - filesystem of external device is linux" @@ -264,7 +242,7 @@ function install_sendmail { function install_php { # openssl and mbstring are included in libapache2-mod-php print_info "installing php..." - nocheck_install "libapache2-mod-php php php-pear php-curl php-mcrypt php-gd" + nocheck_install "libapache2-mod-php php php-pear php-curl php-mcrypt php-gd php-mysqli php-mbstring php-xml" sed -i "s/^upload_max_filesize =.*/upload_max_filesize = 100M/g" /etc/php/7.0/apache2/php.ini sed -i "s/^post_max_size =.*/post_max_size = 100M/g" /etc/php/7.0/apache2/php.ini } @@ -449,11 +427,11 @@ function configure_cron_selfhost { print_info "configure cron for selfhost..." if [ -z "$selfhost_user" ] then - print_info "freedns is not configured because freedns_key is empty in $configfile" + print_info "selfhost is not configured because selfhost_key is empty in $configfile" else # Use cron for dynamich ip update # - at reboot - # - every 30 minutes + # - every 5 minutes if [ -z "`grep 'selfhost-updater.sh' /etc/crontab`" ] then echo "@reboot root bash /etc/selfhost/selfhost-updater.sh update > /dev/null 2>&1" >> /etc/crontab @@ -471,89 +449,24 @@ function install_letsencrypt { then die "Failed to install let's encrypt: 'le_domain' is empty in $configfile" fi - # configure apache - apache_le_conf=/etc/apache2/sites-available/le-default.conf - if [ -f $apache_le_conf ] + # check if user gave mail address + if [ -z "$le_email" ] then - print_info "$apache_le_conf exist already" + die "Failed to install let's encrypt: 'le_domain' is empty in $configfile" + fi + nocheck_install "apt-transport-https" + # add backports to your sources.list + backports_list=/etc/apt/sources.list.d/backports.list + if [ -f $backports_list ] + then + print_info "$backports_list exist already" else - cat > $apache_le_conf < - Options FollowSymLinks - Allow from all - -END - a2ensite le-default.conf - service apache2 restart + echo "deb https://deb.debian.org/debian stretch-backports main" > $backports_list fi - # download the shell script - if [ -d $le_dir ] - then - print_info "letsenrypt exists already (nothing downloaded > no certificate created and registered)" - return 0 - fi - git clone https://github.com/lukas2511/dehydrated $le_dir - cd $le_dir - # create config file for letsencrypt.sh - echo "WELLKNOWN=$le_dir" > $le_dir/config.sh - if [ -n "$le_email" ] - then - echo "CONTACT_EMAIL=$le_email" >> $le_dir/config.sh - fi - # create domain file for letsencrypt.sh - # WATCH THIS: - # - It did not work wit "sub.domain.org www.sub.domain.org". - # - So just use "sub.domain.org" only! - echo "$le_domain" > $le_dir/domains.txt - # test apache config for letsencrpyt - url_http=http://$le_domain/.well-known/acme-challenge/domains.txt - wget_output=$(wget -nv --spider --max-redirect 0 $url_http) - if [ $? -ne 0 ] - then - die "Failed to load $url_http" - fi - # accept terms of service of letsencrypt - ./dehydrated --register --accept-terms - # run script dehydrated - # - ./dehydrated --cron --config $le_dir/config.sh -} - -function configure_apache_for_https { - print_info "configuring apache to use httpS ..." - # letsencrypt.sh - # - # "${BASEDIR}/certs/${domain}/privkey.pem" - # "${BASEDIR}/certs/${domain}/cert.pem" - # "${BASEDIR}/certs/${domain}/fullchain.pem" - # - SSLCertificateFile=${le_dir}/certs/${le_domain}/cert.pem - SSLCertificateKeyFile=${le_dir}/certs/${le_domain}/privkey.pem - SSLCertificateChainFile=${le_dir}/certs/${le_domain}/fullchain.pem - if [ ! -f $SSLCertificateFile ] - then - print_warn "Failed to configure apache for httpS: Missing certificate file $SSLCertificateFile" - return 0 - fi - # make sure that the ssl mode is enabled - print_info "...configuring apache to use httpS - a2enmod ssl ..." - a2enmod ssl - # modify apach' ssl conf file - if grep -i "ServerName" $sslconf - then - print_info "seems that apache was already configered to use httpS with $sslconf" - else - sed -i "s/ServerAdmin.*$/ServerAdmin webmaster@localhost\\n ServerName ${le_domain}/" $sslconf - fi - sed -i s#/etc/ssl/certs/ssl-cert-snakeoil.pem#$SSLCertificateFile# $sslconf - sed -i s#/etc/ssl/private/ssl-cert-snakeoil.key#$SSLCertificateKeyFile# $sslconf - sed -i s#/etc/apache2/ssl.crt/server-ca.crt#$SSLCertificateChainFile# $sslconf - sed -i s/#SSLCertificateChainFile/SSLCertificateChainFile/ $sslconf - # apply changes - a2ensite default-ssl.conf + apt-get -y update + DEBIAN_FRONTEND=noninteractive apt-get -q -y -t stretch-backports install certbot python-certbot-apache + print_info "run certbot ..." + certbot --apache -w /var/www/html -d $le_domain -m $le_email --agree-tos --non-interactive --redirect --hsts --uir service apache2 restart } @@ -572,7 +485,10 @@ function check_https { function install_hubzilla { print_info "installing hubzilla addons..." cd /var/www/html/ - util/add_addon_repo https://framagit.org/hubzilla/addons.git hzaddons + # if you install Hubzilla + util/add_addon_repo https://framagit.org/hubzilla/addons hzaddons + # if you install ZAP + #util/add_addon_repo https://framagit.org/zot/zap-addons.git zaddons mkdir -p "store/[data]/smarty3" chmod -R 777 store touch .htconfig.php @@ -582,7 +498,7 @@ function install_hubzilla { chown root:www-data /var/www/html/ chown root:www-data /var/www/html/.htaccess chmod 0644 /var/www/html/.htaccess - # try to switch off email registration + print_info "try to switch off email registration..." sed -i "s/verify_email.*1/verify_email'] = 0/" /var/www/html/view/*/ht* if [ -n "`grep -r 'verify_email.*1' /var/www/html/view/`" ] then @@ -591,49 +507,9 @@ function install_hubzilla { print_info "installed hubzilla" } -function rewrite_to_https { - print_info "configuring apache to redirect http to httpS ..." - htaccessfile=/var/www/html/.htaccess - if grep -i "https" $htaccessfile - then - print_info "...configuring apache to redirect http to httpS was already done in $htaccessfile" - else - sed -i "s#QSA]#QSA]\\n RewriteCond %{SERVER_PORT} !^443$\\n RewriteRule (.*) https://%{HTTP_HOST}/$1 [R=301,L]#" $htaccessfile - fi - service apache2 restart -} - -# This will allways overwrite both config files -# - internal disk -# - external disk (LUKS + ext4) -# of rsnapshot for hubzilla -function install_rsnapshot { - print_info "installing rsnapshot..." - nocheck_install "rsnapshot" - # internal disk - cp -f /etc/rsnapshot.conf $snapshotconfig - sed -i "s/^cmd_cp/#cmd_cp/" $snapshotconfig - sed -i "s/^backup/#backup/" $snapshotconfig - echo "backup /var/lib/mysql/ localhost/" >> $snapshotconfig - echo "backup /var/www/html/ localhost/" >> $snapshotconfig - echo "backup /var/www/letsencrypt/ localhost/" >> $snapshotconfig - # external disk - if [ -n "$backup_device_name" ] - then - cp -f /etc/rsnapshot.conf $snapshotconfig_external_device - sed -i "s#snapshot_root.*#snapshot_root $backup_mount_point#" $snapshotconfig_external_device - sed -i "/alpha/s/6/30/" $snapshotconfig_external_device - sed -i "s/^cmd_cp/#cmd_cp/" $snapshotconfig_external_device - sed -i "s/^backup/#backup/" $snapshotconfig_external_device - if [ -z "`grep 'letsencrypt' $snapshotconfig_external_device`" ] - then - echo "backup /var/lib/mysql/ localhost/" >> $snapshotconfig_external_device - echo "backup /var/www/html/ localhost/" >> $snapshotconfig_external_device - echo "backup /var/www/letsencrypt/ localhost/" >> $snapshotconfig_external_device - fi - else - print_info "No backup configuration (rsnapshot) for external device configured. Reason: backup_device_name and/or backup_device_pass not given in $configfile" - fi +function install_rsync { + print_info "installing rsync..." + nocheck_install "rsync" } function install_cryptosetup { @@ -644,28 +520,28 @@ function install_cryptosetup { function configure_cron_daily { print_info "configuring cron..." # every 10 min for poller.php - if [ -z "`grep 'poller.php' /etc/crontab`" ] + if [ -z "`grep 'Master.php' /etc/crontab`" ] then echo "*/10 * * * * www-data cd /var/www/html; php Zotlabs/Daemon/Master.php Cron >> /dev/null 2>&1" >> /etc/crontab fi # Run external script daily at 05:30 # - stop apache and mysql-server - # - backup hubzilla + # - renew the certificate of letsencrypt + # - backup db, files (/var/www/html), certificates if letsencrypt # - update hubzilla core and addon # - update and upgrade linux - # - reboot + # - reboot is done by "shutdown -h now" because "reboot" hangs sometimes depending on the system echo "#!/bin/sh" > /var/www/$hubzilladaily echo "#" >> /var/www/$hubzilladaily echo "echo \" \"" >> /var/www/$hubzilladaily echo "echo \"+++ \$(date) +++\"" >> /var/www/$hubzilladaily echo "echo \" \"" >> /var/www/$hubzilladaily echo "echo \"\$(date) - renew certificate...\"" >> /var/www/$hubzilladaily -echo "bash $le_dir/dehydrated --cron --config $le_dir/config.sh" >> /var/www/$hubzilladaily +echo "certbot renew --noninteractive" >> /var/www/$hubzilladaily echo "#" >> /var/www/$hubzilladaily -echo "# stop hubzilla" >> /var/www/$hubzilladaily -echo "echo \"\$(date) - stoping apache and mysql...\"" >> /var/www/$hubzilladaily +echo "echo \"\$(date) - stopping apache and mysql...\"" >> /var/www/$hubzilladaily echo "service apache2 stop" >> /var/www/$hubzilladaily -echo "/etc/init.d/mysql stop # to avoid inconsistancies" >> /var/www/$hubzilladaily +echo "/etc/init.d/mysql stop # to avoid inconsistencies" >> /var/www/$hubzilladaily echo "#" >> /var/www/$hubzilladaily echo "# backup" >> /var/www/$hubzilladaily echo "echo \"\$(date) - try to mount external device for backup...\"" >> /var/www/$hubzilladaily @@ -696,11 +572,13 @@ echo " if mount $backup_device_name $backup_mount_point" >> /var/www/$hub echo " then" >> /var/www/$hubzilladaily echo " device_mounted=1" >> /var/www/$hubzilladaily echo " echo \"device $backup_device_name is now mounted. Starting backup...\"" >> /var/www/$hubzilladaily -echo " rsnapshot -c $snapshotconfig_external_device alpha" >> /var/www/$hubzilladaily -echo " echo \"\$(date) - disk sizes...\"" >> /var/www/$hubzilladaily -echo " df -h" >> /var/www/$hubzilladaily -echo " echo \"\$(date) - db size...\"" >> /var/www/$hubzilladaily -echo " du -h $backup_mount_point | grep mysql/hubzilla" >> /var/www/$hubzilladaily +echo " rsync -a --delete /var/lib/mysql/ /media/hubzilla_backup/mysql" >> /var/www/$hubzilladaily +echo " rsync -a --delete /var/www/ /media/hubzilla_backup/www" >> /var/www/$hubzilladaily +echo " rsync -a --delete /etc/letsencrypt/ /media/hubzilla_backup/letsencrypt" >> /var/www/$hubzilladaily +echo " echo \"\$(date) - disk sizes...\"" >> /var/www/$hubzilladaily +echo " df -h" >> /var/www/$hubzilladaily +echo " echo \"\$(date) - db size...\"" >> /var/www/$hubzilladaily +echo " du -h $backup_mount_point | grep mysql/hubzilla" >> /var/www/$hubzilladaily echo " echo \"unmounting backup device...\"" >> /var/www/$hubzilladaily echo " umount $backup_mount_point" >> /var/www/$hubzilladaily echo " else" >> /var/www/$hubzilladaily @@ -722,18 +600,16 @@ echo "echo \"\$(date) - db size...\"" >> /var/www/$hubzilladaily echo "du -h /var/lib/mysql/ | grep mysql/hubzilla" >> /var/www/$hubzilladaily echo "#" >> /var/www/$hubzilladaily echo "# update" >> /var/www/$hubzilladaily -echo "echo \"\$(date) - updating dehydrated...\"" >> /var/www/$hubzilladaily -echo "git -C /var/www/letsencrypt/ pull" >> /var/www/$hubzilladaily -echo "echo \"\$(date) - updating hubhilla core...\"" >> /var/www/$hubzilladaily +echo "echo \"\$(date) - updating core and addons...\"" >> /var/www/$hubzilladaily echo "(cd /var/www/html/ ; util/udall)" >> /var/www/$hubzilladaily echo "chown -R www-data:www-data /var/www/html/ # make all accessable for the webserver" >> /var/www/$hubzilladaily echo "chown root:www-data /var/www/html/.htaccess" >> /var/www/$hubzilladaily echo "chmod 0644 /var/www/html/.htaccess # www-data can read but not write it" >> /var/www/$hubzilladaily echo "echo \"\$(date) - updating linux...\"" >> /var/www/$hubzilladaily echo "apt-get -q -y update && apt-get -q -y dist-upgrade && apt-get -q -y autoremove # update linux and upgrade" >> /var/www/$hubzilladaily -echo "echo \"\$(date) - Backup hubzilla and update linux finished. Rebooting...\"" >> /var/www/$hubzilladaily +echo "echo \"\$(date) - Backup and update finished. Rebooting...\"" >> /var/www/$hubzilladaily echo "#" >> /var/www/$hubzilladaily -echo "reboot" >> /var/www/$hubzilladaily +echo "shutdown -r now" >> /var/www/$hubzilladaily if [ -z "`grep 'hubzilla-daily.sh' /etc/crontab`" ] then @@ -745,38 +621,6 @@ echo "reboot" >> /var/www/$hubzilladaily print_info "configured cron for updates/upgrades" } -function write_uninstall_script { - print_info "writing uninstall script..." - - cat > /var/www/hubzilla-remove.sh < 'Image', 'id' => $x['id'], 'name' => $x['title'], - 'content' => bbcode($x['body']), + 'content' => bbcode($x['body'], [ 'cache' => true ]), 'source' => [ 'mediaType' => 'text/bbcode', 'content' => $x['body'] ], 'published' => datetime_convert('UTC','UTC',$x['created'],ATOM_TIME), 'updated' => datetime_convert('UTC','UTC', $x['edited'],ATOM_TIME), @@ -180,14 +184,24 @@ class Activity { $y = [ 'type' => 'Event', 'id' => z_root() . '/event/' . $ev['event_hash'], - 'summary' => bbcode($ev['summary']), + 'summary' => bbcode($ev['summary'], [ 'cache' => true ]), // RFC3339 Section 4.3 'startTime' => (($ev['adjust']) ? datetime_convert('UTC','UTC',$ev['dtstart'], ATOM_TIME) : datetime_convert('UTC','UTC',$ev['dtstart'],'Y-m-d\\TH:i:s-00:00')), - 'content' => bbcode($ev['description']), - 'location' => [ 'type' => 'Place', 'content' => bbcode($ev['location']) ], + 'content' => bbcode($ev['description'], [ 'cache' => true ]), + 'location' => [ 'type' => 'Place', 'content' => bbcode($ev['location'], [ 'cache' => true ]) ], 'source' => [ 'content' => format_event_bbcode($ev), 'mediaType' => 'text/bbcode' ], 'actor' => $actor, ]; + if(! $ev['nofinish']) { + $y['endTime'] = (($ev['adjust']) ? datetime_convert('UTC','UTC',$ev['dtend'], ATOM_TIME) : datetime_convert('UTC','UTC',$ev['dtend'],'Y-m-d\\TH:i:s-00:00')); + } + + // copy attachments from the passed object - these are already formatted for ActivityStreams + + if($x['attachment']) { + $y['attachment'] = $x['attachment']; + } + if($actor) { return $y; } @@ -296,15 +310,15 @@ class Activity { $ret['attributedTo'] = $i['author']['xchan_url']; if($i['id'] != $i['parent']) { - $ret['inReplyTo'] = ((strpos($i['parent_mid'],'http') === 0) ? $i['parent_mid'] : z_root() . '/item/' . urlencode($i['parent_mid'])); + $ret['inReplyTo'] = ((strpos($i['thr_parent'],'http') === 0) ? $i['thr_parent'] : z_root() . '/item/' . urlencode($i['thr_parent'])); } if($i['mimetype'] === 'text/bbcode') { if($i['title']) - $ret['name'] = bbcode($i['title']); + $ret['name'] = bbcode($i['title'], [ 'cache' => true ]); if($i['summary']) - $ret['summary'] = bbcode($i['summary']); - $ret['content'] = bbcode($i['body']); + $ret['summary'] = bbcode($i['summary'], [ 'cache' => true ]); + $ret['content'] = bbcode($i['body'], [ 'cache' => true ]); $ret['source'] = [ 'content' => $i['body'], 'mediaType' => 'text/bbcode' ]; } @@ -397,7 +411,7 @@ class Activity { $ret = []; if($item['attach']) { - $atts = json_decode($item['attach'],true); + $atts = ((is_array($item['attach'])) ? $item['attach'] : json_decode($item['attach'],true)); if($atts) { foreach($atts as $att) { if(strpos($att['type'],'image')) { @@ -409,7 +423,7 @@ class Activity { } } } - + return $ret; } @@ -462,14 +476,14 @@ class Activity { $ret['id'] = ((strpos($i['mid'],'http') === 0) ? $i['mid'] : z_root() . '/activity/' . urlencode($i['mid'])); if($i['title']) - $ret['name'] = html2plain(bbcode($i['title'])); + $ret['name'] = html2plain(bbcode($i['title'], [ 'cache' => true ])); if($i['summary']) - $ret['summary'] = bbcode($i['summary']); + $ret['summary'] = bbcode($i['summary'], [ 'cache' => true ]); if($ret['type'] === 'Announce') { $tmp = preg_replace('/\[share(.*?)\[\/share\]/ism',EMPTY_STR, $i['body']); - $ret['content'] = bbcode($tmp); + $ret['content'] = bbcode($tmp, [ 'cache' => true ]); $ret['source'] = [ 'content' => $i['body'], 'mediaType' => 'text/bbcode' @@ -495,7 +509,7 @@ class Activity { } if($i['id'] != $i['parent']) { - $ret['inReplyTo'] = ((strpos($i['parent_mid'],'http') === 0) ? $i['parent_mid'] : z_root() . '/item/' . urlencode($i['parent_mid'])); + $ret['inReplyTo'] = ((strpos($i['thr_parent'],'http') === 0) ? $i['thr_parent'] : z_root() . '/item/' . urlencode($i['thr_parent'])); $reply = true; if($i['item_private']) { @@ -526,6 +540,10 @@ class Activity { else return []; + if(strpos($i['body'],'[/share]') !== false) { + $i['obj'] = null; + } + if($i['obj']) { if(! is_array($i['obj'])) { $i['obj'] = json_decode($i['obj'],true); @@ -548,6 +566,7 @@ class Activity { return []; } + if($i['target']) { if(! is_array($i['target'])) { $i['target'] = json_decode($i['target'],true); @@ -692,7 +711,7 @@ class Activity { // Reactions will just map to normal activities if(strpos($verb,ACTIVITY_REACT) !== false) - return 'Create'; + return 'emojiReaction'; if(strpos($verb,ACTIVITY_MOOD) !== false) return 'Create'; @@ -868,7 +887,7 @@ class Activity { // Send an Accept back to them set_abconfig($channel['channel_id'],$person_obj['id'],'pubcrawl','their_follow_id', $their_follow_id); - \Zotlabs\Daemon\Master::Summon([ 'Notifier', 'permissions_accept', $contact['abook_id'] ]); + Master::Summon([ 'Notifier', 'permissions_accept', $contact['abook_id'] ]); return; case 'Accept': @@ -969,9 +988,9 @@ class Activity { if($my_perms && $automatic) { // send an Accept for this Follow activity - \Zotlabs\Daemon\Master::Summon([ 'Notifier', 'permissions_accept', $new_connection[0]['abook_id'] ]); + Master::Summon([ 'Notifier', 'permissions_accept', $new_connection[0]['abook_id'] ]); // Send back a Follow notification to them - \Zotlabs\Daemon\Master::Summon([ 'Notifier', 'permissions_create', $new_connection[0]['abook_id'] ]); + Master::Summon([ 'Notifier', 'permissions_create', $new_connection[0]['abook_id'] ]); } $clone = array(); @@ -1162,7 +1181,7 @@ class Activity { $photos = import_xchan_photo($icon,$url); $r = q("update xchan set xchan_photo_date = '%s', xchan_photo_l = '%s', xchan_photo_m = '%s', xchan_photo_s = '%s', xchan_photo_mimetype = '%s' where xchan_hash = '%s'", - dbescdate(datetime_convert('UTC','UTC',$arr['photo_updated'])), + dbescdate(datetime_convert('UTC','UTC',$photos[5])), dbesc($photos[0]), dbesc($photos[1]), dbesc($photos[2]), @@ -1406,7 +1425,7 @@ class Activity { if($parent) { if($s['owner_xchan'] === $channel['channel_hash']) { // We are the owner of this conversation, so send all received comments back downstream - Zotlabs\Daemon\Master::Summon(array('Notifier','comment-import',$x['item_id'])); + Master::Summon(array('Notifier','comment-import',$x['item_id'])); } $r = q("select * from item where id = %d limit 1", intval($x['item_id']) @@ -1468,7 +1487,7 @@ class Activity { } - if(in_array($act->type, [ 'Like', 'Dislike', 'Flag', 'Block', 'Announce', 'Accept', 'Reject', 'TentativeAccept' ])) { + if(in_array($act->type, [ 'Like', 'Dislike', 'Flag', 'Block', 'Announce', 'Accept', 'Reject', 'TentativeAccept', 'emojiReaction' ])) { $response_activity = true; @@ -1509,6 +1528,9 @@ class Activity { if($act->type === 'Announce') { $content['content'] = sprintf( t('🔁 Repeated %1$s\'s %2$s'), $mention, $act->obj['type']); } + if ($act->type === 'emojiReaction') { + $content['content'] = (($act->tgt && $act->tgt['type'] === 'Image') ? '[img=32x32]' . $act->tgt['url'] . '[/img]' : '&#x' . $act->tgt['name'] . ';'); + } } if(! $s['created']) @@ -1790,7 +1812,7 @@ class Activity { $s['item_private'] = 1; set_iconfig($s,'activitypub','recips',$act->raw_recips); - + // @FIXME: $parent is not defined if($parent) { set_iconfig($s,'activitypub','rawmsg',$act->raw,1); } @@ -1921,10 +1943,11 @@ class Activity { if(is_array($x) && $x['item_id']) { + // @FIXME: $parent is not defined if($parent) { if($s['owner_xchan'] === $channel['channel_hash']) { // We are the owner of this conversation, so send all received comments back downstream - Zotlabs\Daemon\Master::Summon(array('Notifier','comment-import',$x['item_id'])); + Master::Summon(array('Notifier','comment-import',$x['item_id'])); } $r = q("select * from item where id = %d limit 1", intval($x['item_id']) @@ -2060,7 +2083,7 @@ class Activity { if($result['success']) { // if the message isn't already being relayed, notify others if(intval($parent_item['item_origin'])) - Zotlabs\Daemon\Master::Summon(array('Notifier','comment-import',$result['item_id'])); + Master::Summon(array('Notifier','comment-import',$result['item_id'])); sync_an_item($channel['channel_id'],$result['item_id']); } diff --git a/Zotlabs/Lib/ActivityStreams.php b/Zotlabs/Lib/ActivityStreams.php index a357b6d69..006744aff 100644 --- a/Zotlabs/Lib/ActivityStreams.php +++ b/Zotlabs/Lib/ActivityStreams.php @@ -319,7 +319,10 @@ class ActivityStreams { function get_compound_property($property, $base = '', $namespace = '', $first = false) { $x = $this->get_property_obj($property, $base, $namespace); if($this->is_url($x)) { - $x = $this->fetch_property($x); + $y = $this->fetch_property($x); + if (is_array($y)) { + $x = $y; + } } // verify and unpack JSalmon signature if present diff --git a/Zotlabs/Lib/Api_router.php b/Zotlabs/Lib/Api_router.php index 404678bd9..6e3f231a9 100644 --- a/Zotlabs/Lib/Api_router.php +++ b/Zotlabs/Lib/Api_router.php @@ -12,8 +12,16 @@ class Api_router { } static function find($path) { - if(array_key_exists($path,self::$routes)) + if (array_key_exists($path,self::$routes)) { return self::$routes[$path]; + } + + $with_params = dirname($path) . '/[id]'; + + if (array_key_exists($with_params,self::$routes)) { + return self::$routes[$with_params]; + } + return null; } diff --git a/Zotlabs/Lib/Apps.php b/Zotlabs/Lib/Apps.php index 56283ff76..69996b49d 100644 --- a/Zotlabs/Lib/Apps.php +++ b/Zotlabs/Lib/Apps.php @@ -70,7 +70,7 @@ class Apps { 'Channel Home', 'View Profile', 'Photos', - 'Events', + 'Calendar', 'Directory', 'Search', 'Help', @@ -327,6 +327,8 @@ class Apps { 'Report Bug' => t('Report Bug'), 'Bookmarks' => t('Bookmarks'), 'Chatrooms' => t('Chatrooms'), + 'Content Filter' => t('Content Filter'), + 'Content Import' => t('Content Import'), 'Connections' => t('Connections'), 'Remote Diagnostics' => t('Remote Diagnostics'), 'Suggest Channels' => t('Suggest Channels'), @@ -340,7 +342,7 @@ class Apps { 'Channel Home' => t('Channel Home'), 'View Profile' => t('View Profile'), 'Photos' => t('Photos'), - 'Events' => t('Events'), + 'Calendar' => t('Calendar'), 'Directory' => t('Directory'), 'Help' => t('Help'), 'Mail' => t('Mail'), @@ -361,7 +363,6 @@ class Apps { 'Privacy Groups' => t('Privacy Groups'), 'Notifications' => t('Notifications'), 'Order Apps' => t('Order Apps'), - 'CalDAV' => t('CalDAV'), 'CardDAV' => t('CardDAV'), 'Channel Sources' => t('Channel Sources'), 'Guest Access' => t('Guest Access'), diff --git a/Zotlabs/Lib/DB_Upgrade.php b/Zotlabs/Lib/DB_Upgrade.php index 4038a2d53..b6e3f7b7b 100644 --- a/Zotlabs/Lib/DB_Upgrade.php +++ b/Zotlabs/Lib/DB_Upgrade.php @@ -58,10 +58,15 @@ class DB_Upgrade { $c = new $cls(); + $retval = $c->run(); if($retval != UPDATE_SUCCESS) { + + $source = t('Source code of failed update: ') . "\n\n" . @file_get_contents('Zotlabs/Update/' . $s . '.php'); + + // Prevent sending hundreds of thousands of emails by creating // a lockfile. @@ -86,7 +91,9 @@ class DB_Upgrade { '$sitename' => \App::$config['system']['sitename'], '$siteurl' => z_root(), '$update' => $x, - '$error' => sprintf( t('Update %s failed. See error logs.'), $x) + '$error' => sprintf( t('Update %s failed. See error logs.'), $x), + '$baseurl' => z_root(), + '$source' => $source ] ) ] diff --git a/Zotlabs/Lib/DReport.php b/Zotlabs/Lib/DReport.php index 18087e29f..7515d3292 100644 --- a/Zotlabs/Lib/DReport.php +++ b/Zotlabs/Lib/DReport.php @@ -118,7 +118,7 @@ class DReport { // So if a remote site says they can't find us, that's no big surprise // and just creates a lot of extra report noise - if(($dr['location'] !== z_root()) && ($dr['sender'] === $rxchan) && ($dr['status'] === 'recipient_not_found')) + if(($dr['location'] !== z_root()) && ($dr['sender'] === $rxchan) && ($dr['status'] === 'recipient not found')) return false; // If you have a private post with a recipient list, every single site is going to report diff --git a/Zotlabs/Lib/Enotify.php b/Zotlabs/Lib/Enotify.php index 5e5798cac..a7082f45a 100644 --- a/Zotlabs/Lib/Enotify.php +++ b/Zotlabs/Lib/Enotify.php @@ -754,9 +754,9 @@ class Enotify { // generate a multipart/alternative message header $messageHeader = $params['additionalMailHeader'] . - "From: $fromName <{$params['fromEmail']}>\n" . - "Reply-To: $fromName <{$params['replyTo']}>\n" . - "MIME-Version: 1.0\n" . + "From: $fromName <{$params['fromEmail']}>" . PHP_EOL . + "Reply-To: $fromName <{$params['replyTo']}>" . PHP_EOL . + "MIME-Version: 1.0" . PHP_EOL . "Content-Type: multipart/alternative; boundary=\"{$mimeBoundary}\""; // assemble the final multipart message body with the text and html types included @@ -764,15 +764,15 @@ class Enotify { $htmlBody = chunk_split(base64_encode($params['htmlVersion'])); $multipartMessageBody = - "--" . $mimeBoundary . "\n" . // plain text section - "Content-Type: text/plain; charset=UTF-8\n" . - "Content-Transfer-Encoding: base64\n\n" . - $textBody . "\n" . - "--" . $mimeBoundary . "\n" . // text/html section - "Content-Type: text/html; charset=UTF-8\n" . - "Content-Transfer-Encoding: base64\n\n" . - $htmlBody . "\n" . - "--" . $mimeBoundary . "--\n"; // message ending + "--" . $mimeBoundary . PHP_EOL . // plain text section + "Content-Type: text/plain; charset=UTF-8" . PHP_EOL . + "Content-Transfer-Encoding: base64" . PHP_EOL . PHP_EOL . + $textBody . PHP_EOL . + "--" . $mimeBoundary . PHP_EOL . // text/html section + "Content-Type: text/html; charset=UTF-8" . PHP_EOL . + "Content-Transfer-Encoding: base64" . PHP_EOL . PHP_EOL . + $htmlBody . PHP_EOL . + "--" . $mimeBoundary . "--" . PHP_EOL; // message ending // send the message $res = mail( diff --git a/Zotlabs/Lib/Libsync.php b/Zotlabs/Lib/Libsync.php index d037a0058..d93270bc5 100644 --- a/Zotlabs/Lib/Libsync.php +++ b/Zotlabs/Lib/Libsync.php @@ -336,7 +336,7 @@ class Libsync { $disallowed = array('abook_id','abook_account','abook_channel','abook_rating','abook_rating_text','abook_not_here'); - $fields = db_columns($abook); + $fields = db_columns('abook'); foreach($arr['abook'] as $abook) { diff --git a/Zotlabs/Lib/Libzot.php b/Zotlabs/Lib/Libzot.php index 019237568..9bf987027 100644 --- a/Zotlabs/Lib/Libzot.php +++ b/Zotlabs/Lib/Libzot.php @@ -685,9 +685,27 @@ class Libzot { $adult_changed = 1; if(intval($r[0]['xchan_deleted']) != intval($arr['deleted'])) $deleted_changed = 1; + + // new style 6-MAR-2019 + + if(array_key_exists('channel_type',$arr)) { + if($arr['channel_type'] === 'collection') { + // do nothing at this time. + } + elseif($arr['channel_type'] === 'group') { + $arr['public_forum'] = 1; + } + else { + $arr['public_forum'] = 0; + } + } + + // old style + if(intval($r[0]['xchan_pubforum']) != intval($arr['public_forum'])) $pubforum_changed = 1; + if($arr['protocols']) { $protocols = implode(',',$arr['protocols']); if($protocols !== 'zot6') { @@ -1107,9 +1125,14 @@ class Libzot { logger('Activity rejected: ' . print_r($data,true)); return; } - $arr = Activity::decode_note($AS); + if (is_array($AS->obj)) { + $arr = Activity::decode_note($AS); + } + else { + $arr = []; + } - logger($AS->debug()); + logger($AS->debug(),LOGGER_DATA); } @@ -1174,12 +1197,14 @@ class Libzot { //logger($AS->debug()); - $r = q("select hubloc_hash from hubloc where hubloc_id_url = '%s' and hubloc_network = 'zot6' limit 1", + $r = q("select hubloc_hash, hubloc_network from hubloc where hubloc_id_url = '%s' ", dbesc($AS->actor['id']) ); if($r) { - $arr['author_xchan'] = $r[0]['hubloc_hash']; + // selects a zot6 hash if available, otherwise use whatever we have + $r = self::zot_record_preferred($r); + $arr['author_xchan'] = $r['hubloc_hash']; } @@ -1212,7 +1237,7 @@ class Libzot { $relay = (($env['type'] === 'response') ? true : false ); - $result = self::process_delivery($env['sender'],$arr,$deliveries,$relay,false,$message_request); + $result = self::process_delivery($env['sender'],$AS,$arr,$deliveries,$relay,false,$message_request); } elseif($env['type'] === 'sync') { // $arr = get_channelsync_elements($data); @@ -1385,7 +1410,7 @@ class Libzot { /** * @brief * - * @param array $sender + * @param string $sender * @param array $arr * @param array $deliveries * @param boolean $relay @@ -1394,7 +1419,7 @@ class Libzot { * @return array */ - static function process_delivery($sender, $arr, $deliveries, $relay, $public = false, $request = false) { + static function process_delivery($sender, $act, $arr, $deliveries, $relay, $public = false, $request = false) { $result = []; @@ -1423,6 +1448,24 @@ class Libzot { $DR->set_name($channel['channel_name'] . ' <' . channel_reddress($channel) . '>'); + if(($act) && ($act->obj) && (! is_array($act->obj))) { + // The initial object fetch failed using the sys channel credentials. + // Try again using the delivery channel credentials. + // We will also need to re-parse the $item array, + // but preserve any values that were set during anonymous parsing. + + $o = Activity::fetch($act->obj,$channel); + if($o) { + $act->obj = $o; + $arr = array_merge(Activity::decode_note($act),$arr); + } + else { + $DR->update('Incomplete or corrupt activity'); + $result[] = $DR->get(); + continue; + } + } + /** * We need to block normal top-level message delivery from our clones, as the delivered * message doesn't have ACL information in it as the cloned copy does. That copy @@ -1841,7 +1884,7 @@ class Libzot { logger('FOF Activity received: ' . print_r($arr,true), LOGGER_DATA, LOG_DEBUG); logger('FOF Activity recipient: ' . $channel['channel_portable_id'], LOGGER_DATA, LOG_DEBUG); - $result = self::process_delivery($arr['owner_xchan'],$arr, [ $channel['channel_portable_id'] ],false,false,true); + $result = self::process_delivery($arr['owner_xchan'],$AS, $arr, [ $channel['channel_portable_id'] ],false,false,true); if ($result) { $ret = array_merge($ret, $result); } @@ -1854,8 +1897,7 @@ class Libzot { /** * @brief Remove community tag. * - * @param array $sender an associative array with - * * \e string \b hash a xchan_hash + * @param string $sender * @param array $arr an associative array * * \e int \b verb * * \e int \b obj_type @@ -1928,7 +1970,7 @@ class Libzot { * * @see item_store_update() * - * @param array $sender + * @param string $sender * @param array $item * @param array $orig * @param int $uid @@ -1979,7 +2021,7 @@ class Libzot { /** * @brief Deletes an imported item. * - * @param array $sender + * @param string $sender * * \e string \b hash a xchan_hash * @param array $item * @param int $uid @@ -1997,9 +2039,9 @@ class Libzot { $r = q("select id, author_xchan, owner_xchan, source_xchan, item_deleted from item where ( author_xchan = '%s' or owner_xchan = '%s' or source_xchan = '%s' ) and mid = '%s' and uid = %d limit 1", - dbesc($sender['hash']), - dbesc($sender['hash']), - dbesc($sender['hash']), + dbesc($sender), + dbesc($sender), + dbesc($sender), dbesc($item['mid']), intval($uid) ); @@ -2154,8 +2196,7 @@ class Libzot { * * @see import_directory_profile() * - * @param array $sender an associative array - * * \e string \b hash a xchan_hash + * @param string $sender * @param array $arr * @param array $deliveries (unused) * @return void @@ -2165,7 +2206,7 @@ class Libzot { logger('process_profile_delivery', LOGGER_DEBUG); $r = q("select xchan_addr from xchan where xchan_hash = '%s' limit 1", - dbesc($sender['hash']) + dbesc($sender) ); if($r) { Libzotdir::import_directory_profile($sender, $arr, $r[0]['xchan_addr'], UPDATE_FLAGS_UPDATED, 0); @@ -2176,8 +2217,7 @@ class Libzot { /** * @brief * - * @param array $sender an associative array - * * \e string \b hash a xchan_hash + * @param string $sender * @param array $arr * @param array $deliveries (unused) deliveries is irrelevant * @return void @@ -3058,4 +3098,26 @@ class Libzot { return(($x) ? true : false); } + + static public function zot_record_preferred($arr, $check = 'hubloc_network') { + + if(! $arr) { + return $arr; + } + + foreach($arr as $v) { + if($v[$check] === 'zot6') { + return $v; + } + } + foreach($arr as $v) { + if($v[$check] === 'zot') { + return $v; + } + } + + return $arr[0]; + + } + } diff --git a/Zotlabs/Lib/Libzotdir.php b/Zotlabs/Lib/Libzotdir.php index 91d089c86..1cb52275c 100644 --- a/Zotlabs/Lib/Libzotdir.php +++ b/Zotlabs/Lib/Libzotdir.php @@ -307,7 +307,7 @@ class Libzotdir { if ($ud['ud_addr'] && (! ($ud['ud_flags'] & UPDATE_FLAGS_DELETED))) { $success = false; - $href = \Zotlabs\Lib\Webfinger::zot_url(punify($url)); + $href = \Zotlabs\Lib\Webfinger::zot_url(punify($ud['ud_addr'])); if($href) { $zf = \Zotlabs\Lib\Zotfinger::exec($href); } @@ -651,4 +651,4 @@ class Libzotdir { -} \ No newline at end of file +} diff --git a/Zotlabs/Lib/MessageFilter.php b/Zotlabs/Lib/MessageFilter.php index eb0fc3d2c..750d6d424 100644 --- a/Zotlabs/Lib/MessageFilter.php +++ b/Zotlabs/Lib/MessageFilter.php @@ -19,7 +19,7 @@ class MessageFilter { $lang = null; - if((strpos($incl,'lang=') !== false) || (strpos($excl,'lang=') !== false)) { + if((strpos($incl,'lang=') !== false) || (strpos($excl,'lang=') !== false) || (strpos($incl,'lang!=') !== false) || (strpos($excl,'lang!=') !== false)) { $lang = detect_language($text); } @@ -39,10 +39,17 @@ class MessageFilter { if((($t['ttype'] == TERM_HASHTAG) || ($t['ttype'] == TERM_COMMUNITYTAG)) && (($t['term'] === substr($word,1)) || (substr($word,1) === '*'))) return false; } + elseif(substr($word,0,1) === '$' && $tags) { + foreach($tags as $t) + if(($t['ttype'] == TERM_CATEGORY) && (($t['term'] === substr($word,1)) || (substr($word,1) === '*'))) + return false; + } elseif((strpos($word,'/') === 0) && preg_match($word,$text)) return false; elseif((strpos($word,'lang=') === 0) && ($lang) && (strcasecmp($lang,trim(substr($word,5))) == 0)) return false; + elseif((strpos($word,'lang!=') === 0) && ($lang) && (strcasecmp($lang,trim(substr($word,6))) != 0)) + return false; elseif(stristr($text,$word) !== false) return false; } @@ -60,10 +67,17 @@ class MessageFilter { if((($t['ttype'] == TERM_HASHTAG) || ($t['ttype'] == TERM_COMMUNITYTAG)) && (($t['term'] === substr($word,1)) || (substr($word,1) === '*'))) return true; } + elseif(substr($word,0,1) === '$' && $tags) { + foreach($tags as $t) + if(($t['ttype'] == TERM_CATEGORY) && (($t['term'] === substr($word,1)) || (substr($word,1) === '*'))) + return true; + } elseif((strpos($word,'/') === 0) && preg_match($word,$text)) return true; elseif((strpos($word,'lang=') === 0) && ($lang) && (strcasecmp($lang,trim(substr($word,5))) == 0)) return true; + elseif((strpos($word,'lang!=') === 0) && ($lang) && (strcasecmp($lang,trim(substr($word,6))) != 0)) + return true; elseif(stristr($text,$word) !== false) return true; } diff --git a/Zotlabs/Lib/ThreadItem.php b/Zotlabs/Lib/ThreadItem.php index 40c0fca4b..9161aa182 100644 --- a/Zotlabs/Lib/ThreadItem.php +++ b/Zotlabs/Lib/ThreadItem.php @@ -38,6 +38,7 @@ class ThreadItem { $this->data = $data; $this->toplevel = ($this->get_id() == $this->get_data_value('parent')); + $this->threaded = get_config('system','thread_allow'); $observer = \App::get_observer(); @@ -305,6 +306,7 @@ class ThreadItem { if($this->is_commentable() && $observer) { $like = array( t("I like this \x28toggle\x29"), t("like")); $dislike = array( t("I don't like this \x28toggle\x29"), t("dislike")); + $reply_to = array( t("Reply on this comment"), t("reply"), t("Reply to")); } if ($shareable) { @@ -331,7 +333,6 @@ class ThreadItem { if(strcmp(datetime_convert('UTC','UTC',$item['created']),datetime_convert('UTC','UTC','now - 12 hours')) > 0) $is_new = true; - localize_item($item); $body = prepare_body($item,true); @@ -347,10 +348,6 @@ class ThreadItem { $comment_count_txt = sprintf( tt('%d comment','%d comments',$total_children),$total_children ); $list_unseen_txt = (($unseen_comments) ? sprintf('%d unseen',$unseen_comments) : ''); - - - - $children = $this->get_children(); $has_tags = (($body['tags'] || $body['categories'] || $body['mentions'] || $body['attachments'] || $body['folders']) ? true : false); @@ -373,13 +370,15 @@ class ThreadItem { 'text' => strip_tags($body['html']), 'id' => $this->get_id(), 'mid' => $item['mid'], + 'parent' => $item['parent'], + 'author_id' => (($item['author']['xchan_addr']) ? $item['author']['xchan_addr'] : $item['author']['xchan_url']), 'isevent' => $isevent, 'attend' => $attend, 'consensus' => $consensus, 'conlabels' => $conlabels, 'canvote' => $canvote, - 'linktitle' => sprintf( t('View %s\'s profile - %s'), $profile_name, $item['author']['xchan_addr']), - 'olinktitle' => sprintf( t('View %s\'s profile - %s'), $this->get_owner_name(), $item['owner']['xchan_addr']), + 'linktitle' => sprintf( t('View %s\'s profile - %s'), $profile_name, (($item['author']['xchan_addr']) ? $item['author']['xchan_addr'] : $item['author']['xchan_url'])), + 'olinktitle' => sprintf( t('View %s\'s profile - %s'), $this->get_owner_name(), (($item['owner']['xchan_addr']) ? $item['owner']['xchan_addr'] : $item['owner']['xchan_url'])), 'llink' => $item['llink'], 'viewthread' => $viewthread, 'to' => t('to'), @@ -425,9 +424,11 @@ class ThreadItem { 'has_tags' => $has_tags, 'reactions' => $this->reactions, // Item toolbar buttons - 'emojis' => (($this->is_toplevel() && $this->is_commentable() && $observer && feature_enabled($conv->get_profile_owner(),'emojis')) ? '1' : ''), + 'emojis' => (($this->is_toplevel() && $this->is_commentable() && $observer && feature_enabled($conv->get_profile_owner(),'emojis')) ? '1' : ''), 'like' => $like, 'dislike' => ((feature_enabled($conv->get_profile_owner(),'dislike')) ? $dislike : ''), + 'reply_to' => (((! $this->is_toplevel()) && feature_enabled($conv->get_profile_owner(),'reply_to')) ? $reply_to : ''), + 'top_hint' => t("Go to previous comment"), 'share' => $share, 'embed' => $embed, 'rawmid' => $item['mid'], @@ -440,9 +441,8 @@ class ThreadItem { 'addtocal' => (($has_event) ? t('Add to Calendar') : ''), 'drop' => $drop, 'multidrop' => ((feature_enabled($conv->get_profile_owner(),'multi_delete')) ? $multidrop : ''), - 'dropdown_extras' => $dropdown_extras, + 'dropdown_extras' => $dropdown_extras, // end toolbar buttons - 'unseen_comments' => $unseen_comments, 'comment_count' => $total_children, 'comment_count_txt' => $comment_count_txt, @@ -469,7 +469,8 @@ class ThreadItem { 'wait' => t('Please wait'), 'submid' => str_replace(['+','='], ['',''], base64_encode($item['mid'])), 'thread_level' => $thread_level, - 'settings' => $settings + 'settings' => $settings, + 'thr_parent' => (($item['parent_mid'] != $item['thr_parent']) ? $item['thr_parent'] : '') ); $arr = array('item' => $item, 'output' => $tmp_item); @@ -814,7 +815,7 @@ class ThreadItem { '$anonname' => [ 'anonname', t('Your full name (required)') ], '$anonmail' => [ 'anonmail', t('Your email address (required)') ], '$anonurl' => [ 'anonurl', t('Your website URL (optional)') ], - '$auto_save_draft' => $feature_auto_save_draft, + '$auto_save_draft' => $feature_auto_save_draft )); return $comment_box; @@ -869,4 +870,3 @@ class ThreadItem { } - diff --git a/Zotlabs/Lib/ZotURL.php b/Zotlabs/Lib/ZotURL.php index d1c705fcb..bc14c516a 100644 --- a/Zotlabs/Lib/ZotURL.php +++ b/Zotlabs/Lib/ZotURL.php @@ -66,7 +66,7 @@ class ZotURL { } - static public function is_zoturl($s) { + static public function is_zoturl($url) { if(strpos($url,'x-zot:') === 0) { return true; diff --git a/Zotlabs/Module/Acl.php b/Zotlabs/Module/Acl.php index 738e8fbe2..82c156a9c 100644 --- a/Zotlabs/Module/Acl.php +++ b/Zotlabs/Module/Acl.php @@ -166,7 +166,7 @@ class Acl extends \Zotlabs\Web\Controller { if($extra_channels) { foreach($extra_channels as $channel) { if(perm_is_allowed(intval($channel), get_observer_hash(),'view_contacts')) { - if($extra_channel_sql) + if($extra_channels_sql) $extra_channels_sql .= ','; $extra_channels_sql .= intval($channel); } diff --git a/Zotlabs/Module/Admin/Dbsync.php b/Zotlabs/Module/Admin/Dbsync.php index 469af2aa5..183834301 100644 --- a/Zotlabs/Module/Admin/Dbsync.php +++ b/Zotlabs/Module/Admin/Dbsync.php @@ -19,7 +19,47 @@ class Dbsync { info( t('Update has been marked successful') . EOL); goaway(z_root() . '/admin/dbsync'); } + + if(argc() > 3 && intval(argv(3)) && argv(2) === 'verify') { + + $s = '_' . intval(argv(3)); + $cls = '\\Zotlabs\Update\\' . $s ; + if(class_exists($cls)) { + $c = new $cls(); + if(method_exists($c,'verify')) { + $retval = $c->verify(); + if($retval === UPDATE_FAILED) { + $o .= sprintf( t('Verification of update %s failed. Check system logs.'), $s); + } + elseif($retval === UPDATE_SUCCESS) { + $o .= sprintf( t('Update %s was successfully applied.'), $s); + set_config('database',$s, 'success'); + } + else + $o .= sprintf( t('Verifying update %s did not return a status. Unknown if it succeeded.'), $s); + } + else { + $o .= sprintf( t('Update %s does not contain a verification function.'), $s ); + } + } + else + $o .= sprintf( t('Update function %s could not be found.'), $s); + return $o; + + + + + + // remove the old style config if it exists + del_config('database', 'update_r' . intval(argv(3))); + set_config('database', '_' . intval(argv(3)), 'success'); + if(intval(get_config('system','db_version')) < intval(argv(3))) + set_config('system','db_version',intval(argv(3))); + info( t('Update has been marked successful') . EOL); + goaway(z_root() . '/admin/dbsync'); + } + if(argc() > 2 && intval(argv(2))) { $x = intval(argv(2)); $s = '_' . $x; @@ -28,14 +68,14 @@ class Dbsync { $c = new $cls(); $retval = $c->run(); if($retval === UPDATE_FAILED) { - $o .= sprintf( t('Executing %s failed. Check system logs.'), $s); + $o .= sprintf( t('Executing update procedure %s failed. Check system logs.'), $s); } elseif($retval === UPDATE_SUCCESS) { $o .= sprintf( t('Update %s was successfully applied.'), $s); set_config('database',$s, 'success'); } else - $o .= sprintf( t('Update %s did not return a status. Unknown if it succeeded.'), $s); + $o .= sprintf( t('Update %s did not return a status. It cannot be determined if it was successful.'), $s); } else $o .= sprintf( t('Update function %s could not be found.'), $s); @@ -59,6 +99,7 @@ class Dbsync { '$banner' => t('Failed Updates'), '$desc' => '', '$mark' => t('Mark success (if update was manually applied)'), + '$verify' => t('Attempt to verify this update if a verification procedure exists'), '$apply' => t('Attempt to execute this update step automatically'), '$failed' => $failed )); diff --git a/Zotlabs/Module/Apschema.php b/Zotlabs/Module/Apschema.php new file mode 100644 index 000000000..5b249bfe8 --- /dev/null +++ b/Zotlabs/Module/Apschema.php @@ -0,0 +1,56 @@ + [ + 'zot' => z_root() . '/apschema#', + 'id' => '@id', + 'type' => '@type', + 'commentPolicy' => 'as:commentPolicy', + 'meData' => 'zot:meData', + 'meDataType' => 'zot:meDataType', + 'meEncoding' => 'zot:meEncoding', + 'meAlgorithm' => 'zot:meAlgorithm', + 'meCreator' => 'zot:meCreator', + 'meSignatureValue' => 'zot:meSignatureValue', + 'locationAddress' => 'zot:locationAddress', + 'locationPrimary' => 'zot:locationPrimary', + 'locationDeleted' => 'zot:locationDeleted', + 'nomadicLocation' => 'zot:nomadicLocation', + 'nomadicHubs' => 'zot:nomadicHubs', + 'emojiReaction' => 'zot:emojiReaction', + + 'magicEnv' => [ + '@id' => 'zot:magicEnv', + '@type' => '@id' + ], + + 'nomadicLocations' => [ + '@id' => 'zot:nomadicLocations', + '@type' => '@id' + ], + + 'ostatus' => 'http://ostatus.org#', + 'conversation' => 'ostatus:conversation' + + ] + ]; + + header('Content-Type: application/ld+json'); + echo json_encode($arr,JSON_UNESCAPED_SLASHES); + killme(); + + } + + + + +} \ No newline at end of file diff --git a/Zotlabs/Module/Cdav.php b/Zotlabs/Module/Cdav.php index d644e48b1..6b4f57ea5 100644 --- a/Zotlabs/Module/Cdav.php +++ b/Zotlabs/Module/Cdav.php @@ -133,10 +133,6 @@ class Cdav extends Controller { logger('loggedin'); - if((argv(1) == 'calendars') && (!Apps::system_app_installed(local_channel(), 'CalDAV'))) { - killme(); - } - if((argv(1) == 'addressbooks') && (!Apps::system_app_installed(local_channel(), 'CardDAV'))) { killme(); } @@ -221,10 +217,6 @@ class Cdav extends Controller { if(! local_channel()) return; - if((argv(1) === 'calendar') && (! Apps::system_app_installed(local_channel(), 'CalDAV'))) { - return; - } - if((argv(1) === 'addressbook') && (! Apps::system_app_installed(local_channel(), 'CardDAV'))) { return; } @@ -280,9 +272,12 @@ class Cdav extends Controller { return; $title = $_REQUEST['title']; - $dtstart = new \DateTime($_REQUEST['dtstart']); - if($_REQUEST['dtend']) - $dtend = new \DateTime($_REQUEST['dtend']); + $start = datetime_convert(App::$timezone, 'UTC', $_REQUEST['dtstart']); + $dtstart = new \DateTime($start); + if($_REQUEST['dtend']) { + $end = datetime_convert(App::$timezone, 'UTC', $_REQUEST['dtend']); + $dtend = new \DateTime($end); + } $description = $_REQUEST['description']; $location = $_REQUEST['location']; @@ -306,13 +301,17 @@ class Cdav extends Controller { 'DTSTART' => $dtstart ] ]); - if($dtend) + if($dtend) { $vcalendar->VEVENT->add('DTEND', $dtend); + $vcalendar->VEVENT->DTEND['TZID'] = App::$timezone; + } if($description) $vcalendar->VEVENT->add('DESCRIPTION', $description); if($location) $vcalendar->VEVENT->add('LOCATION', $location); + $vcalendar->VEVENT->DTSTART['TZID'] = App::$timezone; + $calendarData = $vcalendar->serialize(); $caldavBackend->createCalendarObject($id, $objectUri, $calendarData); @@ -351,8 +350,12 @@ class Cdav extends Controller { $uri = $_REQUEST['uri']; $title = $_REQUEST['title']; - $dtstart = new \DateTime($_REQUEST['dtstart']); - $dtend = $_REQUEST['dtend'] ? new \DateTime($_REQUEST['dtend']) : ''; + $start = datetime_convert(App::$timezone, 'UTC', $_REQUEST['dtstart']); + $dtstart = new \DateTime($start); + if($_REQUEST['dtend']) { + $end = datetime_convert(App::$timezone, 'UTC', $_REQUEST['dtend']); + $dtend = new \DateTime($end); + } $description = $_REQUEST['description']; $location = $_REQUEST['location']; @@ -404,8 +407,12 @@ class Cdav extends Controller { return; $uri = $_REQUEST['uri']; - $dtstart = new \DateTime($_REQUEST['dtstart']); - $dtend = $_REQUEST['dtend'] ? new \DateTime($_REQUEST['dtend']) : ''; + $start = datetime_convert(App::$timezone, 'UTC', $_REQUEST['dtstart']); + $dtstart = new \DateTime($start); + if($_REQUEST['dtend']) { + $end = datetime_convert(App::$timezone, 'UTC', $_REQUEST['dtend']); + $dtend = new \DateTime($end); + } $object = $caldavBackend->getCalendarObject($id, $uri); @@ -747,16 +754,27 @@ class Cdav extends Controller { //Import calendar or addressbook if(($_FILES) && array_key_exists('userfile',$_FILES) && intval($_FILES['userfile']['size']) && $_REQUEST['target']) { - $src = @file_get_contents($_FILES['userfile']['tmp_name']); + $src = $_FILES['userfile']['tmp_name']; if($src) { if($_REQUEST['c_upload']) { + if($_REQUEST['target'] == 'channel_calendar') { + $result = parse_ical_file($src,local_channel()); + if($result) + info( t('Calendar entries imported.') . EOL); + else + notice( t('No calendar entries found.') . EOL); + + @unlink($src); + return; + } + $id = explode(':', $_REQUEST['target']); $ext = 'ics'; $table = 'calendarobjects'; $column = 'calendarid'; - $objects = new \Sabre\VObject\Splitter\ICalendar($src); + $objects = new \Sabre\VObject\Splitter\ICalendar(@file_get_contents($src)); $profile = \Sabre\VObject\Node::PROFILE_CALDAV; $backend = new \Sabre\CalDAV\Backend\PDO($pdo); } @@ -766,7 +784,7 @@ class Cdav extends Controller { $ext = 'vcf'; $table = 'cards'; $column = 'addressbookid'; - $objects = new \Sabre\VObject\Splitter\VCard($src); + $objects = new \Sabre\VObject\Splitter\VCard(@file_get_contents($src)); $profile = \Sabre\VObject\Node::PROFILE_CARDDAV; $backend = new \Sabre\CardDAV\Backend\PDO($pdo); } @@ -832,15 +850,6 @@ class Cdav extends Controller { if(!local_channel()) return; - if((argv(1) === 'calendar') && (! Apps::system_app_installed(local_channel(), 'CalDAV'))) { - //Do not display any associated widgets at this point - App::$pdl = ''; - - $o = '' . t('CalDAV App') . ' (' . t('Not Installed') . '):
'; - $o .= t('CalDAV capable calendar'); - return $o; - } - if((argv(1) === 'addressbook') && (! Apps::system_app_installed(local_channel(), 'CardDAV'))) { //Do not display any associated widgets at this point App::$pdl = ''; @@ -869,28 +878,93 @@ class Cdav extends Controller { } if(argv(1) === 'calendar') { - nav_set_selected('CalDAV'); + nav_set_selected('Calendar'); $caldavBackend = new \Sabre\CalDAV\Backend\PDO($pdo); $calendars = $caldavBackend->getCalendarsForUser($principalUri); } //Display calendar(s) here - if(argc() == 2 && argv(1) === 'calendar') { + if(argc() <= 3 && argv(1) === 'calendar') { - head_add_css('/library/fullcalendar/fullcalendar.css'); + head_add_css('/library/fullcalendar/packages/core/main.min.css'); + head_add_css('/library/fullcalendar/packages/daygrid/main.min.css'); + head_add_css('/library/fullcalendar/packages/timegrid/main.min.css'); + head_add_css('/library/fullcalendar/packages/list/main.min.css'); head_add_css('cdav_calendar.css'); - head_add_js('/library/moment/moment.min.js', 1); - head_add_js('/library/fullcalendar/fullcalendar.min.js', 1); - head_add_js('/library/fullcalendar/locale-all.js', 1); + head_add_js('/library/fullcalendar/packages/core/main.min.js'); + head_add_js('/library/fullcalendar/packages/interaction/main.min.js'); + head_add_js('/library/fullcalendar/packages/daygrid/main.min.js'); + head_add_js('/library/fullcalendar/packages/timegrid/main.min.js'); + head_add_js('/library/fullcalendar/packages/list/main.min.js'); + + $sources = ''; + $resource_id = ''; + $resource = null; + + if(argc() == 3) + $resource_id = argv(2); + + if($resource_id) { + $r = q("SELECT event.*, item.author_xchan, item.owner_xchan, item.plink, item.id as item_id FROM event LEFT JOIN item ON event.event_hash = item.resource_id + WHERE event.uid = %d AND event.event_hash = '%s' LIMIT 1", + intval(local_channel()), + dbesc($resource_id) + ); + if($r) { + xchan_query($r); + $r = fetch_post_tags($r,true); + + $r[0]['dtstart'] = (($r[0]['adjust']) ? datetime_convert('UTC',date_default_timezone_get(),$r[0]['dtstart'], 'c') : datetime_convert('UTC','UTC',$r[0]['dtstart'],'c')); + $r[0]['dtend'] = (($r[0]['adjust']) ? datetime_convert('UTC',date_default_timezone_get(),$r[0]['dtend'], 'c') : datetime_convert('UTC','UTC',$r[0]['dtend'],'c')); + + $r[0]['plink'] = [$r[0]['plink'], t('Link to source')]; + + $resource = $r[0]; + + $catsenabled = feature_enabled(local_channel(),'categories'); + $categories = ''; + if($catsenabled){ + if($r[0]['term']) { + $cats = get_terms_oftype($r[0]['term'], TERM_CATEGORY); + foreach ($cats as $cat) { + if(strlen($categories)) + $categories .= ', '; + $categories .= $cat['term']; + } + } + } + + if($r[0]['dismissed'] == 0) { + q("UPDATE event SET dismissed = 1 WHERE event.uid = %d AND event.event_hash = '%s'", + intval(local_channel()), + dbesc($resource_id) + ); + } + } + } + + if(get_pconfig(local_channel(), 'cdav_calendar', 'channel_calendar')) { + $sources .= '{ + id: \'channel_calendar\', + url: \'/channel_calendar/json/\', + color: \'#3a87ad\' + }, '; + } + + $channel_calendars[] = [ + 'displayname' => $channel['channel_name'], + 'id' => 'channel_calendar' + ]; foreach($calendars as $calendar) { $editable = (($calendar['share-access'] == 2) ? 'false' : 'true'); // false/true must be string since we're passing it to javascript - $color = (($calendar['{http://apple.com/ns/ical/}calendar-color']) ? $calendar['{http://apple.com/ns/ical/}calendar-color'] : '#3a87ad'); + $color = (($calendar['{http://apple.com/ns/ical/}calendar-color']) ? $calendar['{http://apple.com/ns/ical/}calendar-color'] : '#6cad39'); $sharer = (($calendar['share-access'] == 3) ? $calendar['{urn:ietf:params:xml:ns:caldav}calendar-description'] : ''); $switch = get_pconfig(local_channel(), 'cdav_calendar', $calendar['id'][0]); if($switch) { $sources .= '{ + id: ' . $calendar['id'][0] . ', url: \'/cdav/calendar/json/' . $calendar['id'][0] . '/' . $calendar['id'][1] . '\', color: \'' . $color . '\' }, '; @@ -911,15 +985,29 @@ class Cdav extends Controller { $first_day = (($first_day) ? $first_day : 0); $title = ['title', t('Event title')]; - $dtstart = ['dtstart', t('Start date and time'), '', t('Example: YYYY-MM-DD HH:mm')]; - $dtend = ['dtend', t('End date and time'), '', t('Example: YYYY-MM-DD HH:mm')]; + $dtstart = ['dtstart', t('Start date and time')]; + $dtend = ['dtend', t('End date and time')]; $description = ['description', t('Description')]; $location = ['location', t('Location')]; + $catsenabled = feature_enabled(local_channel(), 'categories'); + + require_once('include/acl_selectors.php'); + + $accesslist = new \Zotlabs\Access\AccessList($channel); + $perm_defaults = $accesslist->get(); + + //$acl = (($orig_event['event_xchan']) ? '' : populate_acl(((x($orig_event)) ? $orig_event : $perm_defaults), false, \Zotlabs\Lib\PermissionDescription::fromGlobalPermission('view_stream'))); + $acl = populate_acl($perm_defaults, false, \Zotlabs\Lib\PermissionDescription::fromGlobalPermission('view_stream')); + + //$permissions = ((x($orig_event)) ? $orig_event : $perm_defaults); + $permissions = $perm_defaults; + $o .= replace_macros(get_markup_template('cdav_calendar.tpl'), [ '$sources' => $sources, '$color' => $color, '$lang' => App::$language, + '$timezone' => App::$timezone, '$first_day' => $first_day, '$prev' => t('Previous'), '$next' => t('Next'), @@ -931,6 +1019,7 @@ class Cdav extends Controller { '$list_week' => t('List week'), '$list_day' => t('List day'), '$title' => $title, + '$channel_calendars' => $channel_calendars, '$writable_calendars' => $writable_calendars, '$dtstart' => $dtstart, '$dtend' => $dtend, @@ -938,11 +1027,27 @@ class Cdav extends Controller { '$location' => $location, '$more' => t('More'), '$less' => t('Less'), + '$update' => t('Update'), '$calendar_select_label' => t('Select calendar'), + '$calendar_optiopns_label' => [t('Channel Calendars'), t('CalDAV Calendars')], '$delete' => t('Delete'), '$delete_all' => t('Delete all'), '$cancel' => t('Cancel'), - '$recurrence_warning' => t('Sorry! Editing of recurrent events is not yet implemented.') + '$create' => t('Create'), + '$recurrence_warning' => t('Sorry! Editing of recurrent events is not yet implemented.'), + + '$channel_hash' => $channel['channel_hash'], + '$acl' => $acl, + '$lockstate' => (($accesslist->is_private()) ? 'lock' : 'unlock'), + '$allow_cid' => acl2json($permissions['allow_cid']), + '$allow_gid' => acl2json($permissions['allow_gid']), + '$deny_cid' => acl2json($permissions['deny_cid']), + '$deny_gid' => acl2json($permissions['deny_gid']), + '$catsenabled' => $catsenabled, + '$categories_label' => t('Categories'), + + '$resource' => json_encode($resource), + '$categories' => $categories ]); return $o; @@ -952,10 +1057,12 @@ class Cdav extends Controller { //Provide json data for calendar if(argc() == 5 && argv(1) === 'calendar' && argv(2) === 'json' && intval(argv(3)) && intval(argv(4))) { + $events = []; + $id = [argv(3), argv(4)]; if(! cdav_perms($id[0],$calendars)) - killme(); + json_return_and_die($events); if (x($_GET,'start')) $start = new \DateTime($_GET['start']); @@ -969,16 +1076,19 @@ class Cdav extends Controller { $filters['comp-filters'][0]['time-range']['end'] = $end; $uris = $caldavBackend->calendarQuery($id, $filters); + if($uris) { - $objects = $caldavBackend->getMultipleCalendarObjects($id, $uris); - foreach($objects as $object) { $vcalendar = \Sabre\VObject\Reader::read($object['calendardata']); - if(isset($vcalendar->VEVENT->RRULE)) + if(isset($vcalendar->VEVENT->RRULE)) { + // expanding recurrent events seems to loose timezone info + // save it here so we can add it later + $recurrent_timezone = (string)$vcalendar->VEVENT->DTSTART['TZID']; $vcalendar = $vcalendar->expand($start, $end); + } foreach($vcalendar->VEVENT as $vevent) { $title = (string)$vevent->SUMMARY; @@ -986,14 +1096,15 @@ class Cdav extends Controller { $dtend = (string)$vevent->DTEND; $description = (string)$vevent->DESCRIPTION; $location = (string)$vevent->LOCATION; - + $timezone = (string)$vevent->DTSTART['TZID']; $rw = ((cdav_perms($id[0],$calendars,true)) ? true : false); + $editable = $rw ? true : false; $recurrent = ((isset($vevent->{'RECURRENCE-ID'})) ? true : false); - $editable = $rw ? true : false; - - if($recurrent) + if($recurrent) { $editable = false; + $timezone = $recurrent_timezone; + } $allDay = false; @@ -1007,8 +1118,8 @@ class Cdav extends Controller { 'calendar_id' => $id, 'uri' => $object['uri'], 'title' => $title, - 'start' => $dtstart, - 'end' => $dtend, + 'start' => datetime_convert($timezone, $timezone, $dtstart, 'c'), + 'end' => (($dtend) ? datetime_convert($timezone, $timezone, $dtend, 'c') : ''), 'description' => $description, 'location' => $location, 'allDay' => $allDay, @@ -1018,15 +1129,12 @@ class Cdav extends Controller { ]; } } - json_return_and_die($events); - } - else { - killme(); } + json_return_and_die($events); } //enable/disable calendars - if(argc() == 5 && argv(1) === 'calendar' && argv(2) === 'switch' && intval(argv(3)) && (argv(4) == 1 || argv(4) == 0)) { + if(argc() == 5 && argv(1) === 'calendar' && argv(2) === 'switch' && argv(3) && (argv(4) == 1 || argv(4) == 0)) { $id = argv(3); if(! cdav_perms($id,$calendars)) @@ -1285,12 +1393,13 @@ class Cdav extends Controller { $caldavBackend = new \Sabre\CalDAV\Backend\PDO($pdo); $properties = [ '{DAV:}displayname' => t('Default Calendar'), - '{http://apple.com/ns/ical/}calendar-color' => '#3a87ad', + '{http://apple.com/ns/ical/}calendar-color' => '#6cad39', '{urn:ietf:params:xml:ns:caldav}calendar-description' => $channel['channel_name'] ]; $id = $caldavBackend->createCalendar($uri, 'default', $properties); set_pconfig(local_channel(), 'cdav_calendar' , $id[0], 1); + set_pconfig(local_channel(), 'cdav_calendar' , 'channel_calendar', 1); //create default addressbook $carddavBackend = new \Sabre\CardDAV\Backend\PDO($pdo); diff --git a/Zotlabs/Module/Changeaddr.php b/Zotlabs/Module/Changeaddr.php index 5cd236394..ed139c9f9 100644 --- a/Zotlabs/Module/Changeaddr.php +++ b/Zotlabs/Module/Changeaddr.php @@ -31,7 +31,7 @@ class Changeaddr extends \Zotlabs\Web\Controller { if($account['account_password_changed'] > NULL_DATE) { $d1 = datetime_convert('UTC','UTC','now - 48 hours'); - if($account['account_password_changed'] > d1) { + if($account['account_password_changed'] > $d1) { notice( t('Channel name changes are not allowed within 48 hours of changing the account password.') . EOL); return; } @@ -49,7 +49,7 @@ class Changeaddr extends \Zotlabs\Web\Controller { if(check_webbie(array($new_address)) !== $new_address) { notice( t('Nickname has unsupported characters or is already being used on this site.') . EOL); - return $ret; + return; } channel_change_address($channel,$new_address); diff --git a/Zotlabs/Module/Channel.php b/Zotlabs/Module/Channel.php index 5fdefd805..144c2472a 100644 --- a/Zotlabs/Module/Channel.php +++ b/Zotlabs/Module/Channel.php @@ -46,14 +46,14 @@ class Channel extends Controller { $channel = App::get_channel(); if((local_channel()) && (argc() > 2) && (argv(2) === 'view')) { - $which = $channel['channel_address']; - $profile = argv(1); - } + $which = $channel['channel_address']; + $profile = argv(1); + } $channel = channelx_by_nick($which); - if(! $channel) { - http_status_exit(404, 'Not found'); - } + if(! $channel) { + http_status_exit(404, 'Not found'); + } // handle zot6 channel discovery @@ -310,10 +310,6 @@ class Channel extends Controller { $sql_extra2 .= protect_sprintf(sprintf(" AND item.created >= '%s' ", dbesc(datetime_convert(date_default_timezone_get(),'',$datequery2)))); } - if($datequery || $datequery2) { - $sql_extra2 .= " and item.item_thread_top != 0 "; - } - if($order === 'post') $ordering = "created"; else @@ -342,7 +338,7 @@ class Channel extends Controller { AND (abook.abook_blocked = 0 or abook.abook_flags is null) AND item.item_wall = 1 AND item.item_thread_top = 1 $sql_extra $sql_extra2 - ORDER BY $ordering DESC $pager_sql ", + ORDER BY $ordering DESC, item_id $pager_sql ", intval(App::$profile['profile_uid']) ); } @@ -414,12 +410,12 @@ class Channel extends Controller { '$page' => ((App::$pager['page'] != 1) ? App::$pager['page'] : 1), '$search' => $search, '$xchan' => '', - '$order' => $order, + '$order' => (($order) ? urlencode($order) : ''), '$list' => ((x($_REQUEST,'list')) ? intval($_REQUEST['list']) : 0), '$file' => '', '$cats' => (($category) ? urlencode($category) : ''), '$tags' => (($hashtags) ? urlencode($hashtags) : ''), - '$mid' => $mid, + '$mid' => (($mid) ? urlencode($mid) : ''), '$verb' => '', '$net' => '', '$dend' => $datequery, diff --git a/Zotlabs/Module/Channel_calendar.php b/Zotlabs/Module/Channel_calendar.php new file mode 100644 index 000000000..9229e6eb2 --- /dev/null +++ b/Zotlabs/Module/Channel_calendar.php @@ -0,0 +1,480 @@ +set($x[0]); + + $created = $x[0]['created']; + $edited = datetime_convert(); + } + else { + $created = $edited = datetime_convert(); + $acl->set_from_array($_POST); + } + + $post_tags = array(); + $channel = \App::get_channel(); + $ac = $acl->get(); + + $str_contact_allow = $ac['allow_cid']; + $str_group_allow = $ac['allow_gid']; + $str_contact_deny = $ac['deny_cid']; + $str_group_deny = $ac['deny_gid']; + + $private = $acl->is_private(); + + require_once('include/text.php'); + $results = linkify_tags($desc, local_channel()); + + if($results) { + // Set permissions based on tag replacements + set_linkified_perms($results, $str_contact_allow, $str_group_allow, local_channel(), false, $private); + + foreach($results as $result) { + $success = $result['success']; + if($success['replaced']) { + $post_tags[] = array( + 'uid' => local_channel(), + 'ttype' => $success['termtype'], + 'otype' => TERM_OBJ_POST, + 'term' => $success['term'], + 'url' => $success['url'] + ); + } + } + } + + if(strlen($categories)) { + $cats = explode(',',$categories); + foreach($cats as $cat) { + $post_tags[] = array( + 'uid' => local_channel(), + 'ttype' => TERM_CATEGORY, + 'otype' => TERM_OBJ_POST, + 'term' => trim($cat), + 'url' => $channel['xchan_url'] . '?f=&cat=' . urlencode(trim($cat)) + ); + } + } + + $datarray = array(); + $datarray['dtstart'] = $start; + $datarray['dtend'] = $finish; + $datarray['summary'] = $summary; + $datarray['description'] = $desc; + $datarray['location'] = $location; + $datarray['etype'] = $type; + $datarray['adjust'] = $adjust; + $datarray['nofinish'] = $nofinish; + $datarray['uid'] = local_channel(); + $datarray['account'] = get_account_id(); + $datarray['event_xchan'] = $channel['channel_hash']; + $datarray['allow_cid'] = $str_contact_allow; + $datarray['allow_gid'] = $str_group_allow; + $datarray['deny_cid'] = $str_contact_deny; + $datarray['deny_gid'] = $str_group_deny; + $datarray['private'] = intval($private); + $datarray['id'] = $event_id; + $datarray['created'] = $created; + $datarray['edited'] = $edited; + + if(intval($_REQUEST['preview'])) { + $html = format_event_html($datarray); + echo $html; + killme(); + } + + $event = event_store_event($datarray); + + if($post_tags) + $datarray['term'] = $post_tags; + + $item_id = event_store_item($datarray,$event); + + if($item_id) { + $r = q("select * from item where id = %d", + intval($item_id) + ); + if($r) { + xchan_query($r); + $sync_item = fetch_post_tags($r); + $z = q("select * from event where event_hash = '%s' and uid = %d limit 1", + dbesc($r[0]['resource_id']), + intval($channel['channel_id']) + ); + if($z) { + build_sync_packet($channel['channel_id'],array('event_item' => array(encode_item($sync_item[0],true)),'event' => $z)); + } + } + } + + \Zotlabs\Daemon\Master::Summon(array('Notifier','event',$item_id)); + + killme(); + + } + + + + function get() { + + if(argc() > 2 && argv(1) == 'ical') { + $event_id = argv(2); + + require_once('include/security.php'); + $sql_extra = permissions_sql(local_channel()); + + $r = q("select * from event where event_hash = '%s' $sql_extra limit 1", + dbesc($event_id) + ); + if($r) { + header('Content-type: text/calendar'); + header('content-disposition: attachment; filename="' . t('event') . '-' . $event_id . '.ics"' ); + echo ical_wrapper($r); + killme(); + } + else { + notice( t('Event not found.') . EOL ); + return; + } + } + + if(! local_channel()) { + notice( t('Permission denied.') . EOL); + return; + } + + if((argc() > 2) && (argv(1) === 'ignore') && intval(argv(2))) { + $r = q("update event set dismissed = 1 where id = %d and uid = %d", + intval(argv(2)), + intval(local_channel()) + ); + } + + if((argc() > 2) && (argv(1) === 'unignore') && intval(argv(2))) { + $r = q("update event set dismissed = 0 where id = %d and uid = %d", + intval(argv(2)), + intval(local_channel()) + ); + } + + $channel = \App::get_channel(); + + $mode = 'view'; + $export = false; + $ignored = ((x($_REQUEST,'ignored')) ? " and dismissed = " . intval($_REQUEST['ignored']) . " " : ''); + + if(argc() > 1) { + if(argc() > 2 && argv(1) === 'add') { + $mode = 'add'; + $item_id = intval(argv(2)); + } + if(argc() > 2 && argv(1) === 'drop') { + $mode = 'drop'; + $event_id = argv(2); + } + if(argc() <= 2 && argv(1) === 'export') { + $export = true; + } + if(argc() > 2 && intval(argv(1)) && intval(argv(2))) { + $mode = 'view'; + } + if(argc() <= 2) { + $mode = 'view'; + $event_id = argv(1); + } + } + + if($mode === 'add') { + event_addtocal($item_id,local_channel()); + killme(); + } + + if($mode == 'view') { + + /* edit/create form */ + if($event_id) { + $r = q("SELECT * FROM event WHERE event_hash = '%s' AND uid = %d LIMIT 1", + dbesc($event_id), + intval(local_channel()) + ); + if(count($r)) + $orig_event = $r[0]; + } + + $channel = \App::get_channel(); + + if (argv(1) === 'json'){ + if (x($_GET,'start')) $start = $_GET['start']; + if (x($_GET,'end')) $finish = $_GET['end']; + } + + $start = datetime_convert('UTC','UTC',$start); + $finish = datetime_convert('UTC','UTC',$finish); + + $adjust_start = datetime_convert('UTC', date_default_timezone_get(), $start); + $adjust_finish = datetime_convert('UTC', date_default_timezone_get(), $finish); + + if (x($_GET,'id')){ + $r = q("SELECT event.*, item.plink, item.item_flags, item.author_xchan, item.owner_xchan, item.id as item_id + from event left join item on item.resource_id = event.event_hash + where item.resource_type = 'event' and event.uid = %d and event.id = %d limit 1", + intval(local_channel()), + intval($_GET['id']) + ); + } + elseif($export) { + $r = q("SELECT * from event where uid = %d and dtstart > '%s' and dtend > dtstart", + intval(local_channel()), + dbesc(NULL_DATE) + ); + } + else { + // fixed an issue with "nofinish" events not showing up in the calendar. + // There's still an issue if the finish date crosses the end of month. + // Noting this for now - it will need to be fixed here and in Friendica. + // Ultimately the finish date shouldn't be involved in the query. + + $r = q("SELECT event.*, item.plink, item.item_flags, item.author_xchan, item.owner_xchan, item.id as item_id + from event left join item on event.event_hash = item.resource_id + where item.resource_type = 'event' and event.uid = %d and event.uid = item.uid $ignored + AND (( event.adjust = 0 AND ( event.dtend >= '%s' or event.nofinish = 1 ) AND event.dtstart <= '%s' ) + OR ( event.adjust = 1 AND ( event.dtend >= '%s' or event.nofinish = 1 ) AND event.dtstart <= '%s' )) ", + intval(local_channel()), + dbesc($start), + dbesc($finish), + dbesc($adjust_start), + dbesc($adjust_finish) + ); + + } + + if($r && ! $export) { + xchan_query($r); + $r = fetch_post_tags($r,true); + + $r = sort_by_date($r); + } + + $events = []; + + if($r) { + + foreach($r as $rr) { + + $start = (($rr['adjust']) ? datetime_convert('UTC',date_default_timezone_get(),$rr['dtstart'], 'c') : datetime_convert('UTC','UTC',$rr['dtstart'],'c')); + if ($rr['nofinish']){ + $end = null; + } else { + $end = (($rr['adjust']) ? datetime_convert('UTC',date_default_timezone_get(),$rr['dtend'], 'c') : datetime_convert('UTC','UTC',$rr['dtend'],'c')); + + // give a fake end to birthdays so they get crammed into a + // single day on the calendar + + if($rr['etype'] === 'birthday') + $end = null; + } + + $catsenabled = feature_enabled(local_channel(),'categories'); + $categories = ''; + if($catsenabled){ + if($rr['term']) { + $cats = get_terms_oftype($rr['term'], TERM_CATEGORY); + foreach ($cats as $cat) { + if(strlen($categories)) + $categories .= ', '; + $categories .= $cat['term']; + } + } + } + + $allDay = false; + + // allDay event rules + if(!strpos($start, 'T') && !strpos($end, 'T')) + $allDay = true; + if(strpos($start, 'T00:00:00') && strpos($end, 'T00:00:00')) + $allDay = true; + + $edit = ((local_channel() && $rr['author_xchan'] == get_observer_hash()) ? array(z_root().'/events/'.$rr['event_hash'].'?expandform=1',t('Edit event'),'','') : false); + + $drop = array(z_root().'/events/drop/'.$rr['event_hash'],t('Delete event'),'',''); + + $events[] = array( + 'calendar_id' => 'channel_calendar', + 'rw' => true, + 'id'=>$rr['id'], + 'uri' => $rr['event_hash'], + 'start'=> $start, + 'end' => $end, + 'drop' => $drop, + 'allDay' => $allDay, + 'title' => htmlentities($rr['summary'], ENT_COMPAT, 'UTF-8', false), + 'editable' => $edit ? true : false, + 'item'=>$rr, + 'plink' => [$rr['plink'], t('Link to source')], + 'description' => htmlentities($rr['description'], ENT_COMPAT, 'UTF-8', false), + 'location' => htmlentities($rr['location'], ENT_COMPAT, 'UTF-8', false), + 'allow_cid' => expand_acl($rr['allow_cid']), + 'allow_gid' => expand_acl($rr['allow_gid']), + 'deny_cid' => expand_acl($rr['deny_cid']), + 'deny_gid' => expand_acl($rr['deny_gid']), + 'categories' => $categories + ); + } + } + + if($export) { + header('Content-type: text/calendar'); + header('content-disposition: attachment; filename="' . t('calendar') . '-' . $channel['channel_address'] . '.ics"' ); + echo ical_wrapper($r); + killme(); + } + + if (\App::$argv[1] === 'json'){ + json_return_and_die($events); + } + } + + + if($mode === 'drop' && $event_id) { + $r = q("SELECT * FROM event WHERE event_hash = '%s' AND uid = %d LIMIT 1", + dbesc($event_id), + intval(local_channel()) + ); + + $sync_event = $r[0]; + + if($r) { + $r = q("delete from event where event_hash = '%s' and uid = %d", + dbesc($event_id), + intval(local_channel()) + ); + if($r) { + $r = q("update item set resource_type = '', resource_id = '' where resource_type = 'event' and resource_id = '%s' and uid = %d", + dbesc($event_id), + intval(local_channel()) + ); + $sync_event['event_deleted'] = 1; + build_sync_packet(0,array('event' => array($sync_event))); + killme(); + } + notice( t('Failed to remove event' ) . EOL); + killme(); + } + } + + } + +} diff --git a/Zotlabs/Module/Connections.php b/Zotlabs/Module/Connections.php index 967e9521d..7c8d71210 100644 --- a/Zotlabs/Module/Connections.php +++ b/Zotlabs/Module/Connections.php @@ -127,6 +127,20 @@ class Connections extends \Zotlabs\Web\Controller { $unblocked = true; } + switch($_REQUEST['order']) { + case 'name_desc': + $sql_order = 'xchan_name DESC'; + break; + case 'connected': + $sql_order = 'abook_created'; + break; + case 'connected_desc': + $sql_order = 'abook_created DESC'; + break; + default: + $sql_order = 'xchan_name'; + } + $search = ((x($_REQUEST,'search')) ? notags(trim($_REQUEST['search'])) : ''); $tabs = array( @@ -233,7 +247,7 @@ class Connections extends \Zotlabs\Web\Controller { } $r = q("SELECT abook.*, xchan.* FROM abook left join xchan on abook.abook_xchan = xchan.xchan_hash - WHERE abook_channel = %d and abook_self = 0 and xchan_deleted = 0 and xchan_orphan = 0 $sql_extra $sql_extra2 ORDER BY xchan_name LIMIT %d OFFSET %d ", + WHERE abook_channel = %d and abook_self = 0 and xchan_deleted = 0 and xchan_orphan = 0 $sql_extra $sql_extra2 ORDER BY $sql_order LIMIT %d OFFSET %d ", intval(local_channel()), intval(App::$pager['itemspage']), intval(App::$pager['start']) @@ -307,7 +321,7 @@ class Connections extends \Zotlabs\Web\Controller { 'ignore_hover' => t('Ignore connection'), 'ignore' => ((! $rr['abook_ignored']) ? t('Ignore') : false), 'recent_label' => t('Recent activity'), - 'recentlink' => z_root() . '/network/?f=&cid=' . intval($rr['abook_id']), + 'recentlink' => z_root() . '/network/?f=&cid=' . intval($rr['abook_id']) . '&name=' . $rr['xchan_name'], 'oneway' => $oneway ); } @@ -329,7 +343,7 @@ class Connections extends \Zotlabs\Web\Controller { killme(); } else { - $o .= ""; + $o .= ""; $o .= replace_macros(get_markup_template('connections.tpl'),array( '$header' => t('Connections') . (($head) ? ': ' . $head : ''), '$tabs' => $tabs, diff --git a/Zotlabs/Module/Connedit.php b/Zotlabs/Module/Connedit.php index a587324df..acd7cb769 100644 --- a/Zotlabs/Module/Connedit.php +++ b/Zotlabs/Module/Connedit.php @@ -848,7 +848,7 @@ class Connedit extends \Zotlabs\Web\Controller { $locstr = unpunify($contact['xchan_url']); $clone_warn = ''; - $clonable = (in_array($contact['xchan_network'],['zot','rss']) ? true : false); + $clonable = (in_array($contact['xchan_network'],['zot', 'zot6', 'rss']) ? true : false); if(! $clonable) { $clone_warn = ''; $clone_warn .= ((intval($contact['abook_not_here'])) diff --git a/Zotlabs/Module/Cover_photo.php b/Zotlabs/Module/Cover_photo.php index b911ac991..d97014f9c 100644 --- a/Zotlabs/Module/Cover_photo.php +++ b/Zotlabs/Module/Cover_photo.php @@ -48,6 +48,32 @@ class Cover_photo extends \Zotlabs\Web\Controller { $channel = \App::get_channel(); check_form_security_token_redirectOnErr('/cover_photo', 'cover_photo'); + + // Remove cover photo + if(isset($_POST['remove'])) { + + $r = q("SELECT resource_id FROM photo WHERE photo_usage = %d AND uid = %d LIMIT 1", + intval(PHOTO_COVER), + intval(local_channel()) + ); + + if($r) { + q("update photo set photo_usage = %d where photo_usage = %d and uid = %d", + intval(PHOTO_NORMAL), + intval(PHOTO_COVER), + intval(local_channel()) + ); + + $sync = attach_export_data($channel,$r[0]['resource_id']); + if($sync) + build_sync_packet($channel['channel_id'],array('file' => array($sync))); + } + + // Update directory in background + \Zotlabs\Daemon\Master::Summon(array('Directory',$channel['channel_id'])); + + goaway(z_root() . '/cover_photo'); + } if((array_key_exists('cropfinal',$_POST)) && ($_POST['cropfinal'] == 1)) { @@ -106,7 +132,7 @@ class Cover_photo extends \Zotlabs\Web\Controller { if(file_exists($tmp_name)) { $base_image = $r[0]; $gis = getimagesize($tmp_name); -logger('gis: ' . print_r($gis,true)); + logger('gis: ' . print_r($gis,true), LOGGER_DEBUG); $base_image['width'] = $gis[0]; $base_image['height'] = $gis[1]; $base_image['content'] = @file_get_contents($tmp_name); @@ -167,25 +193,18 @@ logger('gis: ' . print_r($gis,true)); 'filename' => $base_image['filename'], 'album' => t('Cover Photos'), 'os_path' => $base_image['os_path'], - 'display_path' => $base_image['display_path'] + 'display_path' => $base_image['display_path'], + 'photo_usage' => PHOTO_COVER ]; - - $p['imgscale'] = 7; - $p['photo_usage'] = PHOTO_COVER; - - $r1 = $im->save($p); + + $r1 = $im->storeThumbnail($p, PHOTO_RES_COVER_1200); $im->doScaleImage(850,310); - $p['imgscale'] = 8; - - $r2 = $im->save($p); - + $r2 = $im->storeThumbnail($p, PHOTO_RES_COVER_850); $im->doScaleImage(425,160); - $p['imgscale'] = 9; - - $r3 = $im->save($p); - + $r3 = $im->storeThumbnail($p, PHOTO_RES_COVER_425); + if($r1 === false || $r2 === false || $r3 === false) { // if one failed, delete them all so we can start over. notice( t('Image resize failed.') . EOL ); @@ -193,13 +212,28 @@ logger('gis: ' . print_r($gis,true)); dbesc($base_image['resource_id']), local_channel() ); + + $x = q("SELECT content FROM photo WHERE resource_id = '%s' AND uid = %d AND os_storage = 1 AND imgscale >= 7", + dbesc($base_image['resource_id']), + local_channel() + ); + if($x) { + foreach($x as $xx) { + @unlink(dbunescbin($xx['content'])); + } + } + return; } - - $channel = \App::get_channel(); + $this->send_cover_photo_activity($channel,$base_image,$profile); - - + + $sync = attach_export_data($channel,$base_image['resource_id']); + if($sync) + build_sync_packet($channel['channel_id'],array('file' => array($sync))); + + // Update directory in background + \Zotlabs\Daemon\Master::Summon(array('Directory',$channel['channel_id'])); } else notice( t('Unable to process image') . EOL); @@ -215,7 +249,7 @@ logger('gis: ' . print_r($gis,true)); require_once('include/attach.php'); - $res = attach_store(\App::get_channel(), get_observer_hash(), '', array('album' => t('Cover Photos'), 'hash' => $hash)); + $res = attach_store(\App::get_channel(), get_observer_hash(), '', array('album' => t('Cover Photos'), 'hash' => $hash, 'nosync' => true)); logger('attach_store: ' . print_r($res,true)); @@ -393,6 +427,7 @@ logger('gis: ' . print_r($gis,true)); '$lbl_profiles' => t('Select a profile:'), '$title' => t('Change Cover Photo'), '$submit' => t('Upload'), + '$remove' => t('Remove'), '$profiles' => $profiles, '$embedPhotos' => t('Use a photo from your albums'), '$embedPhotosModalTitle' => t('Use a photo from your albums'), diff --git a/Zotlabs/Module/Directory.php b/Zotlabs/Module/Directory.php index c29fa8326..8f5db6635 100644 --- a/Zotlabs/Module/Directory.php +++ b/Zotlabs/Module/Directory.php @@ -103,8 +103,14 @@ class Directory extends \Zotlabs\Web\Controller { $suggest = (local_channel() && x($_REQUEST,'suggest')) ? $_REQUEST['suggest'] : ''; if($suggest) { - - $r = suggestion_query(local_channel(),get_observer_hash()); + + // the directory options have no effect in suggestion mode + + $globaldir = 1; + $safe_mode = 1; + $type = 0; + + $r = suggestion_query(local_channel(),get_observer_hash(),0,60); if(! $r) { notice( t('No default suggestions were found.') . EOL); @@ -212,12 +218,17 @@ class Directory extends \Zotlabs\Web\Controller { if($j) { if($j['results']) { - + + $results = $j['results']; + if($suggest) { + $results = self::reorder_results($results,$addresses); + } + $entries = array(); $photo = 'thumb'; - foreach($j['results'] as $rr) { + foreach($results as $rr) { $profile_link = chanlink_url($rr['url']); @@ -399,7 +410,7 @@ class Directory extends \Zotlabs\Web\Controller { $dirtitle = (($globaldir) ? t('Global Directory') : t('Local Directory')); - $o .= ""; + $o .= ""; $o .= replace_macros($tpl, array( '$search' => $search, '$desc' => t('Find'), @@ -438,5 +449,22 @@ class Directory extends \Zotlabs\Web\Controller { return $o; } - + static public function reorder_results($results,$suggests) { + + if(! $suggests) + return $results; + + $out = []; + foreach($suggests as $k => $v) { + foreach($results as $rv) { + if($k == $rv['address']) { + $out[intval($v)] = $rv; + break; + } + } + } + + return $out; + } + } diff --git a/Zotlabs/Module/Dirsearch.php b/Zotlabs/Module/Dirsearch.php index 81942860f..26cb82044 100644 --- a/Zotlabs/Module/Dirsearch.php +++ b/Zotlabs/Module/Dirsearch.php @@ -116,12 +116,12 @@ class Dirsearch extends \Zotlabs\Web\Controller { $sql_extra .= $this->dir_query_build($joiner,'xchan_name',$name); if($address) $sql_extra .= $this->dir_query_build($joiner,'xchan_addr',$address); - if($city) - $sql_extra .= $this->dir_query_build($joiner,'xprof_locale',$city); + if($locale) + $sql_extra .= $this->dir_query_build($joiner,'xprof_locale',$locale); if($region) $sql_extra .= $this->dir_query_build($joiner,'xprof_region',$region); - if($post) - $sql_extra .= $this->dir_query_build($joiner,'xprof_postcode',$post); + if($postcode) + $sql_extra .= $this->dir_query_build($joiner,'xprof_postcode',$postcode); if($country) $sql_extra .= $this->dir_query_build($joiner,'xprof_country',$country); if($gender) diff --git a/Zotlabs/Module/Display.php b/Zotlabs/Module/Display.php index 04e5f9fce..5983578b3 100644 --- a/Zotlabs/Module/Display.php +++ b/Zotlabs/Module/Display.php @@ -175,6 +175,11 @@ class Display extends \Zotlabs\Web\Controller { return ''; } } + if ($target_item['item_type'] == ITEM_TYPE_CUSTOM) { + call_hooks('item_custom_display',$target_item); + notice( t('Page not found.') . EOL); + return ''; + } $static = ((array_key_exists('static',$_REQUEST)) ? intval($_REQUEST['static']) : 0); @@ -233,7 +238,7 @@ class Display extends \Zotlabs\Web\Controller { '$dbegin' => '', '$verb' => '', '$net' => '', - '$mid' => $mid + '$mid' => (($mid) ? urlencode($mid) : '') )); head_add_link([ diff --git a/Zotlabs/Module/Dreport.php b/Zotlabs/Module/Dreport.php index 2c125b7a9..0fc36dc29 100644 --- a/Zotlabs/Module/Dreport.php +++ b/Zotlabs/Module/Dreport.php @@ -16,17 +16,20 @@ class Dreport extends \Zotlabs\Web\Controller { $channel = \App::get_channel(); $mid = ((argc() > 1) ? argv(1) : ''); + $encoded_mid = ''; - if(strpos($mid,'b64.') === 0) + if(strpos($mid,'b64.') === 0) { + $encoded_mid = $mid; $mid = @base64url_decode(substr($mid,4)); - - + } if($mid === 'push') { $table = 'push'; $mid = ((argc() > 2) ? argv(2) : ''); - if(strpos($mid,'b64.') === 0) + if(strpos($mid,'b64.') === 0) { + $encoded_mid = $mid; $mid = @base64url_decode(substr($mid,4)); + } if($mid) { $i = q("select id from item where mid = '%s' and uid = %d and ( author_xchan = '%s' or ( owner_xchan = '%s' and item_wall = 1 )) ", @@ -40,7 +43,7 @@ class Dreport extends \Zotlabs\Web\Controller { } } sleep(3); - goaway(z_root() . '/dreport/' . urlencode($mid)); + goaway(z_root() . '/dreport/' . (($encoded_mid) ? $encoded_mid : $mid)); } if($mid === 'mail') { @@ -159,6 +162,7 @@ class Dreport extends \Zotlabs\Web\Controller { '$title' => sprintf( t('Delivery report for %1$s'),basename($mid)) . '...', '$table' => $table, '$mid' => urlencode($mid), + '$safe_mid' => urlencode(gen_link_id($mid)), '$options' => t('Options'), '$push' => t('Redeliver'), '$entries' => $entries diff --git a/Zotlabs/Module/Editpost.php b/Zotlabs/Module/Editpost.php index 1c9068e07..49b2892e8 100644 --- a/Zotlabs/Module/Editpost.php +++ b/Zotlabs/Module/Editpost.php @@ -45,7 +45,8 @@ class Editpost extends \Zotlabs\Web\Controller { } if($itm[0]['resource_type'] === 'event' && $itm[0]['resource_id']) { - goaway(z_root() . '/events/' . $itm[0]['resource_id'] . '?expandform=1'); + goaway(z_root() . '/cdav/calendar/' . $itm[0]['resource_id']); + //goaway(z_root() . '/events/' . $itm[0]['resource_id'] . '?expandform=1'); } $owner_uid = $itm[0]['uid']; diff --git a/Zotlabs/Module/Embedphotos.php b/Zotlabs/Module/Embedphotos.php index 2df14c239..8b0421457 100644 --- a/Zotlabs/Module/Embedphotos.php +++ b/Zotlabs/Module/Embedphotos.php @@ -41,24 +41,44 @@ class Embedphotos extends \Zotlabs\Web\Controller { json_return_and_die(array('errormsg' => 'Error retrieving link ' . $href, 'status' => false)); } $resource_id = array_pop(explode('/', $href)); - $r = q("SELECT obj from item where resource_type = 'photo' and resource_id = '%s' limit 1", - dbesc($resource_id) - ); - if (!$r) { - json_return_and_die(array('errormsg' => 'Error retrieving resource ' . $resource_id, 'status' => false)); - } - $obj = json_decode($r[0]['obj'], true); - if (x($obj, 'body')) { - $photolink = $obj['body']; - } elseif (x($obj, 'bbcode')) { - $photolink = $obj['bbcode']; - } else { - json_return_and_die(array('errormsg' => 'Error retrieving resource ' . $resource_id, 'status' => false)); - } - json_return_and_die(array('status' => true, 'photolink' => $photolink, 'resource_id' => $resource_id)); + $x = self::photolink($resource_id); + if($x) + json_return_and_die(array('status' => true, 'photolink' => $x, 'resource_id' => $resource_id)); + json_return_and_die(array('errormsg' => 'Error retrieving resource ' . $resource_id, 'status' => false)); } } + + protected static function photolink($resource) { + $channel = \App::get_channel(); + $output = EMPTY_STR; + if($channel) { + $resolution = ((feature_enabled($channel['channel_id'],'large_photos')) ? 2 : 3); + $r = q("select mimetype, height, width from photo where resource_id = '%s' and $resolution = %d and uid = %d limit 1", + dbesc($resource), + intval($resolution), + intval($channel['channel_id']) + ); + if(! $r) + return $output; + + if($r[0]['mimetype'] === 'image/jpeg') + $ext = '.jpg'; + elseif($r[0]['mimetype'] === 'image/png') + $ext = '.png'; + elseif($r[0]['mimetype'] === 'image/gif') + $ext = '.gif'; + else + $ext = EMPTY_STR; + + $output = '[zrl=' . z_root() . '/photos/' . $channel['channel_address'] . '/image/' . $resource . ']' . + '[zmg=' . $r[0]['width'] . 'x' . $r[0]['height'] . ']' . z_root() . '/photo/' . $resource . '-' . $resolution . $ext . '[/zmg][/zrl]'; + + return $output; + } + } + + /** * @brief Get photos from an album. * diff --git a/Zotlabs/Module/Getfile.php b/Zotlabs/Module/Getfile.php index abc9f50d9..583cf38f0 100644 --- a/Zotlabs/Module/Getfile.php +++ b/Zotlabs/Module/Getfile.php @@ -35,7 +35,6 @@ class Getfile extends \Zotlabs\Web\Controller { $sig = $_POST['signature']; $resource = $_POST['resource']; $revision = intval($_POST['revision']); - $resolution = (-1); if(! $hash) killme(); @@ -81,9 +80,14 @@ class Getfile extends \Zotlabs\Web\Controller { killme(); } - if(substr($resource,-2,1) == '-') { + if(isset($_POST['resolution'])) + $resolution = intval($_POST['resolution']); + elseif(substr($resource,-2,1) == '-') { $resolution = intval(substr($resource,-1,1)); $resource = substr($resource,0,-2); + } + else { + $resolution = (-1); } $slop = intval(get_pconfig($channel['channel_id'],'system','getfile_time_slop')); @@ -106,9 +110,10 @@ class Getfile extends \Zotlabs\Web\Controller { } if($resolution > 0) { - $r = q("select * from photo where resource_id = '%s' and uid = %d limit 1", + $r = q("SELECT * FROM photo WHERE resource_id = '%s' AND uid = %d AND imgscale = %d LIMIT 1", dbesc($resource), - intval($channel['channel_id']) + intval($channel['channel_id']), + $resolution ); if($r) { header('Content-type: ' . $r[0]['mimetype']); diff --git a/Zotlabs/Module/Hq.php b/Zotlabs/Module/Hq.php index 3535ac71a..848fe3e25 100644 --- a/Zotlabs/Module/Hq.php +++ b/Zotlabs/Module/Hq.php @@ -194,7 +194,7 @@ class Hq extends \Zotlabs\Web\Controller { '$dbegin' => '', '$verb' => '', '$net' => '', - '$mid' => $mid + '$mid' => (($mid) ? urlencode($mid) : '') ]); } diff --git a/Zotlabs/Module/Import.php b/Zotlabs/Module/Import.php index 0daf28aa9..18cb5560e 100644 --- a/Zotlabs/Module/Import.php +++ b/Zotlabs/Module/Import.php @@ -8,6 +8,8 @@ require_once('include/import.php'); require_once('include/perm_upgrade.php'); require_once('library/urlify/URLify.php'); +use Zotlabs\Lib\Libzot; + /** * @brief Module for channel import. @@ -228,13 +230,45 @@ class Import extends \Zotlabs\Web\Controller { ); // reset the original primary hubloc if it is being seized - if($seize) { $r = q("update hubloc set hubloc_primary = 0 where hubloc_primary = 1 and hubloc_hash = '%s' and hubloc_url != '%s' ", dbesc($channel['channel_hash']), dbesc(z_root()) ); } + + // create a new zot6 hubloc if we have got a channel_portable_id + if($channel['channel_portable_id']) { + $r = hubloc_store_lowlevel( + [ + 'hubloc_guid' => $channel['channel_guid'], + 'hubloc_guid_sig' => 'sha256.' . $channel['channel_guid_sig'], + 'hubloc_hash' => $channel['channel_portable_id'], + 'hubloc_addr' => channel_reddress($channel), + 'hubloc_network' => 'zot6', + 'hubloc_primary' => (($seize) ? 1 : 0), + 'hubloc_url' => z_root(), + 'hubloc_url_sig' => 'sha256.' . base64url_encode(rsa_sign(z_root(),$channel['channel_prvkey'])), + 'hubloc_host' => \App::get_hostname(), + 'hubloc_callback' => z_root() . '/zot', + 'hubloc_sitekey' => get_config('system','pubkey'), + 'hubloc_updated' => datetime_convert(), + 'hubloc_id_url' => channel_url($channel), + 'hubloc_site_id' => Libzot::make_xchan_hash(z_root(),get_config('system','pubkey')) + + ] + ); + + // reset the original primary hubloc if it is being seized + if($seize) { + $r = q("update hubloc set hubloc_primary = 0 where hubloc_primary = 1 and hubloc_hash = '%s' and hubloc_url != '%s' ", + dbesc($channel['channel_portable_id']), + dbesc(z_root()) + ); + } + + } + } logger('import step 5'); @@ -246,8 +280,9 @@ class Import extends \Zotlabs\Web\Controller { // replace any existing xchan we may have on this site if we're seizing control - $r = q("delete from xchan where xchan_hash = '%s'", - dbesc($channel['channel_hash']) + $r = q("delete from xchan where ( xchan_hash = '%s' or xchan_hash = '%s' ) ", + dbesc($channel['channel_hash']), + dbesc($channel['channel_portable_id']) ); $r = xchan_store_lowlevel( @@ -269,6 +304,30 @@ class Import extends \Zotlabs\Web\Controller { 'xchan_name_date' => datetime_convert() ] ); + + if($channel['channel_portable_id']) { + $r = xchan_store_lowlevel( + [ + 'xchan_hash' => \Zotlabs\Lib\Libzot::make_xchan_hash($channel['channel_guid'],$channel['channel_pubkey']), + 'xchan_guid' => $channel['channel_guid'], + 'xchan_guid_sig' => 'sha256.' . $channel['channel_guid_sig'], + 'xchan_pubkey' => $channel['channel_pubkey'], + 'xchan_photo_l' => z_root() . "/photo/profile/l/" . $channel['channel_id'], + 'xchan_photo_m' => z_root() . "/photo/profile/m/" . $channel['channel_id'], + 'xchan_photo_s' => z_root() . "/photo/profile/s/" . $channel['channel_id'], + 'xchan_addr' => channel_reddress($channel), + 'xchan_url' => z_root() . '/channel/' . $channel['channel_address'], + 'xchan_connurl' => z_root() . '/poco/' . $channel['channel_address'], + 'xchan_follow' => z_root() . '/follow?f=&url=%s', + 'xchan_name' => $channel['channel_name'], + 'xchan_network' => 'zot6', + 'xchan_photo_date' => datetime_convert(), + 'xchan_name_date' => datetime_convert() + ] + ); + } + + } logger('import step 6'); @@ -278,10 +337,20 @@ class Import extends \Zotlabs\Web\Controller { if($xchans) { foreach($xchans as $xchan) { - $hash = make_xchan_hash($xchan['xchan_guid'],$xchan['xchan_guid_sig']); - if($xchan['xchan_network'] === 'zot' && $hash !== $xchan['xchan_hash']) { - logger('forged xchan: ' . print_r($xchan,true)); - continue; + if($xchan['xchan_network'] === 'zot') { + $hash = make_xchan_hash($xchan['xchan_guid'],$xchan['xchan_guid_sig']); + if($hash !== $xchan['xchan_hash']) { + logger('forged xchan: ' . print_r($xchan,true)); + continue; + } + } + + if($xchan['xchan_network'] === 'zot6') { + $zhash = \Zotlabs\Lib\Libzot::make_xchan_hash($xchan['xchan_guid'],$xchan['xchan_pubkey']); + if($zhash !== $xchan['xchan_hash']) { + logger('forged xchan: ' . print_r($xchan,true)); + continue; + } } if(! array_key_exists('xchan_hidden',$xchan)) { @@ -472,6 +541,9 @@ class Import extends \Zotlabs\Web\Controller { if(is_array($data['app'])) import_apps($channel,$data['app']); + if(is_array($data['sysapp'])) + import_sysapps($channel,$data['sysapp']); + if(is_array($data['chatroom'])) import_chatrooms($channel,$data['chatroom']); diff --git a/Zotlabs/Module/Item.php b/Zotlabs/Module/Item.php index ebcf632ef..6bc8c645f 100644 --- a/Zotlabs/Module/Item.php +++ b/Zotlabs/Module/Item.php @@ -40,56 +40,78 @@ class Item extends Controller { function init() { - if(Libzot::is_zot_request()) { + if (Libzot::is_zot_request()) { $conversation = false; $item_id = argv(1); - if(! $item_id) + if (! $item_id) http_status_exit(404, 'Not found'); - $portable_id = EMPTY_STR; + $item_normal = " and item.item_hidden = 0 and item.item_type = 0 and item.item_unpublished = 0 and item.item_delayed = 0 and item.item_blocked = 0 "; + + $i = null; + + // do we have the item (at all)? + + $r = q("select * from item where mid = '%s' $item_normal limit 1", + dbesc(z_root() . '/item/' . $item_id) + ); + + if (! $r) { + http_status_exit(404,'Not found'); + } + + // process an authenticated fetch + $sigdata = HTTPSig::verify(EMPTY_STR); if($sigdata['portable_id'] && $sigdata['header_valid']) { $portable_id = $sigdata['portable_id']; + observer_auth($portable_id); + + // first see if we have a copy of this item's parent owned by the current signer + // include xchans for all zot-like networks - these will have the same guid and public key + + $x = q("select * from xchan where xchan_hash = '%s'", + dbesc($sigdata['portable_id']) + ); + + if ($x) { + $xchans = q("select xchan_hash from xchan where xchan_hash = '%s' OR ( xchan_guid = '%s' AND xchan_pubkey = '%s' ) ", + dbesc($sigdata['portable_id']), + dbesc($x[0]['xchan_guid']), + dbesc($x[0]['xchan_pubkey']) + ); + + if ($xchans) { + $hashes = ids_to_querystr($xchans,'xchan_hash',true); + $i = q("select id as item_id from item where mid = '%s' $item_normal and owner_xchan in ( " . protect_sprintf($hashes) . " ) limit 1", + dbesc($r[0]['parent_mid']) + ); + } + } } - $item_normal = " and item.item_hidden = 0 and item.item_type = 0 and item.item_unpublished = 0 and item.item_delayed = 0 and item.item_blocked = 0 "; + // if we don't have a parent id belonging to the signer see if we can obtain one as a visitor that we have permission to access $sql_extra = item_permissions_sql(0); - $r = q("select * from item where mid = '%s' $item_normal $sql_extra limit 1", - dbesc(z_root() . '/item/' . $item_id) - ); - if(! $r) { - - - $r = q("select * from item where mid = '%s' $item_normal limit 1", - dbesc(z_root() . '/item/' . $item_id) + if (! $i) { + $i = q("select id as item_id from item where mid = '%s' $item_normal $sql_extra limit 1", + dbesc($r[0]['parent_mid']) ); - if($r) { - http_status_exit(403, 'Forbidden'); - } - http_status_exit(404, 'Not found'); } - - $items = q("select parent as item_id from item where mid = '%s' and uid = %d $item_normal $sql_extra ", - dbesc($r[0]['parent_mid']), - intval($r[0]['uid']) - ); - if(! $items) { - http_status_exit(404, 'Not found'); + if(! $i) { + http_status_exit(403,'Forbidden'); } - $r = $items; - - $parents_str = ids_to_querystr($r,'item_id'); + $parents_str = ids_to_querystr($i,'item_id'); - $items = q("SELECT item.*, item.id AS item_id FROM item WHERE item.parent IN ( %s ) $item_normal $sql_extra ", + $items = q("SELECT item.*, item.id AS item_id FROM item WHERE item.parent IN ( %s ) $item_normal ", dbesc($parents_str) ); @@ -97,9 +119,8 @@ class Item extends Controller { http_status_exit(404, 'Not found'); } - $r = $items; - xchan_query($r,true); - $items = fetch_post_tags($r,true); + xchan_query($items,true); + $items = fetch_post_tags($items,true); $observer = App::get_observer(); $parent = $items[0]; @@ -145,7 +166,7 @@ class Item extends Controller { if(! perm_is_allowed($chan['channel_id'],get_observer_hash(),'view_stream')) http_status_exit(403, 'Forbidden'); - $i = Activity::encode_item_collection($nitems,'conversation/' . $item_id,'OrderedCollection',( defined('NOMADIC') ? false : true)); + $i = Activity::encode_item_collection($nitems,'conversation/' . $item_id,'OrderedCollection'); if($portable_id) { ThreadListener::store(z_root() . '/item/' . $item_id,$portable_id); } diff --git a/Zotlabs/Module/Like.php b/Zotlabs/Module/Like.php index 3d1f503b6..052d51d43 100644 --- a/Zotlabs/Module/Like.php +++ b/Zotlabs/Module/Like.php @@ -491,7 +491,7 @@ class Like extends \Zotlabs\Web\Controller { $arr['item_flags'] = $item_flags; $arr['item_wall'] = $item_wall; - $arr['parent_mid'] = (($extended_like) ? $mid : $item['mid']); + $arr['parent_mid'] = (($extended_like) ? $arr['mid'] : $item['mid']); $arr['owner_xchan'] = (($extended_like) ? $ch[0]['xchan_hash'] : $thread_owner['xchan_hash']); $arr['author_xchan'] = $observer['xchan_hash']; @@ -546,7 +546,7 @@ class Like extends \Zotlabs\Web\Controller { dbesc($observer['xchan_hash']), dbesc($ch[0]['channel_hash']), intval($post_id), - dbesc($mid), + dbesc($arr['mid']), dbesc($activity), dbesc(($tgttype)? $tgttype : $objtype), dbesc($obj_id), @@ -555,7 +555,7 @@ class Like extends \Zotlabs\Web\Controller { $r = q("select * from likes where liker = '%s' and likee = '%s' and i_mid = '%s' and verb = '%s' and target_type = '%s' and target_id = '%s' ", dbesc($observer['xchan_hash']), dbesc($ch[0]['channel_hash']), - dbesc($mid), + dbesc($arr['mid']), dbesc($activity), dbesc(($tgttype)? $tgttype : $objtype), dbesc($obj_id) diff --git a/Zotlabs/Module/Linkinfo.php b/Zotlabs/Module/Linkinfo.php index 32b4c0281..b9f90deec 100644 --- a/Zotlabs/Module/Linkinfo.php +++ b/Zotlabs/Module/Linkinfo.php @@ -69,6 +69,14 @@ class Linkinfo extends \Zotlabs\Web\Controller { killme(); } if(stripos($type,'video/') !== false) { + $thumb = self::get_video_poster($url); + if($thumb) { + if ($zrl) + echo $br . '[zvideo poster=\'' . $thumb . '\']' . $url . '[/zvideo]' . $br; + else + echo $br . '[video poster=\'' . $thumb . '\']' . $url . '[/video]' . $br; + killme(); + } if($zrl) echo $br . '[zvideo]' . $url . '[/zvideo]' . $br; else @@ -216,7 +224,42 @@ class Linkinfo extends \Zotlabs\Web\Controller { return($complete); } - + + public static function get_video_poster($url) { + + if(strpos($url,z_root() . '/cloud/') === false) { + return EMPTY_STR; + } + $m = parse_url($url,PHP_URL_PATH); + if($m) { + // strip leading '/cloud/' + $m = substr($m,7); + } + $nick = substr($m,0,strpos($m,'/')); + $p = substr($m,strpos($m,'/')+1); + + // get the channel to check permissions + + $u = channelx_by_nick($nick); + + if($u && $p) { + + $sql_extra = permissions_sql(intval($u['channel_id'])); + + $r = q("select hash, content from attach where display_path = '%s' and uid = %d and os_storage = 1 $sql_extra limit 1", + dbesc($p), + intval($u['channel_id']) + ); + if($r) { + $path = dbunescbin($r[0]['content']); + if($path && @file_exists($path . '.thumb')) { + return z_root() . '/poster/' . $nick . '/' . $r[0]['hash']; + } + } + } + return EMPTY_STR; + } + public static function parseurl_getsiteinfo($url) { $siteinfo = array(); diff --git a/Zotlabs/Module/Magic.php b/Zotlabs/Module/Magic.php index 71737eef8..e8e960574 100644 --- a/Zotlabs/Module/Magic.php +++ b/Zotlabs/Module/Magic.php @@ -169,8 +169,8 @@ class Magic extends \Zotlabs\Web\Controller { $token = $j['token']; } - $x = strpbrk($dest,'?&'); - $args = (($x) ? '&owt=' . $token : '?f=&owt=' . $token) . (($delegate) ? '&delegate=1' : ''); + $strp = strpbrk($dest,'?&'); + $args = (($strp) ? '&owt=' . $token : '?f=&owt=' . $token) . (($delegate) ? '&delegate=1' : ''); goaway($dest . $args); } } diff --git a/Zotlabs/Module/Network.php b/Zotlabs/Module/Network.php index 2019082ed..1c16e34ef 100644 --- a/Zotlabs/Module/Network.php +++ b/Zotlabs/Module/Network.php @@ -368,19 +368,19 @@ class Network extends \Zotlabs\Web\Controller { '$static' => $static, '$list' => ((x($_REQUEST,'list')) ? intval($_REQUEST['list']) : 0), '$page' => ((App::$pager['page'] != 1) ? App::$pager['page'] : 1), - '$search' => (($search) ? $search : ''), - '$xchan' => $xchan, + '$search' => (($search) ? urlencode($search) : ''), + '$xchan' => (($xchan) ? urlencode($xchan) : ''), '$order' => $order, - '$file' => $file, - '$cats' => urlencode($category), - '$tags' => urlencode($hashtags), + '$file' => (($file) ? urlencode($file) : ''), + '$cats' => (($category) ? urlencode($category) : ''), + '$tags' => (($hashtags) ? urlencode($hashtags) : ''), '$dend' => $datequery, '$mid' => '', - '$verb' => $verb, - '$net' => $net, + '$verb' => (($verb) ? urlencode($verb) : ''), + '$net' => (($net) ? urlencode($net) : ''), '$dbegin' => $datequery2, - '$pf' => (($pf) ? $pf : '0'), - '$unseen' => $unseen + '$pf' => (($pf) ? intval($pf) : 0), + '$unseen' => (($unseen) ? urlencode($unseen) : '') )); } diff --git a/Zotlabs/Module/Oep.php b/Zotlabs/Module/Oep.php index c0d8e15e5..3977ac8dd 100644 --- a/Zotlabs/Module/Oep.php +++ b/Zotlabs/Module/Oep.php @@ -456,7 +456,7 @@ class Oep extends \Zotlabs\Web\Controller { if(preg_match('|//(.*?)/(.*?)/(.*?)/album/|',$url,$matches)) { $chn = $matches[3]; - $res = hex2bin(basename($url)); + $res = basename($url); } if(! ($chn && $res)) diff --git a/Zotlabs/Module/Owa.php b/Zotlabs/Module/Owa.php index ad57f883c..cf116a96c 100644 --- a/Zotlabs/Module/Owa.php +++ b/Zotlabs/Module/Owa.php @@ -30,12 +30,29 @@ class Owa extends \Zotlabs\Web\Controller { $keyId = $sigblock['keyId']; if($keyId) { + + // Hubzilla connections can have both zot and zot6 hublocs + // The connections will usually be zot so match those first + $r = q("select * from hubloc left join xchan on hubloc_hash = xchan_hash - where ( hubloc_addr = '%s' or hubloc_id_url = '%s' ) ", + where ( hubloc_addr = '%s' or hubloc_id_url = '%s' ) and hubloc_network = 'zot' ", dbesc(str_replace('acct:','',$keyId)), dbesc($keyId) ); - if(! $r) { + + // If nothing was found, try searching on any network + + if (! $r) { + $r = q("select * from hubloc left join xchan on hubloc_hash = xchan_hash + where ( hubloc_addr = '%s' or hubloc_id_url = '%s' )", + dbesc(str_replace('acct:','',$keyId)), + dbesc($keyId) + ); + } + + // If nothing was found on any network, use network discovery and create a new record + + if (! $r) { $found = discover_by_webbie(str_replace('acct:','',$keyId)); if($found) { $r = q("select * from hubloc left join xchan on hubloc_hash = xchan_hash @@ -45,7 +62,8 @@ class Owa extends \Zotlabs\Web\Controller { ); } } - if($r) { + + if ($r) { foreach($r as $hubloc) { $verified = \Zotlabs\Web\HTTPSig::verify(file_get_contents('php://input'),$hubloc['xchan_pubkey']); if($verified && $verified['header_signed'] && $verified['header_valid']) { @@ -53,7 +71,7 @@ class Owa extends \Zotlabs\Web\Controller { logger('OWA success: ' . $hubloc['hubloc_addr'],LOGGER_DATA); $ret['success'] = true; $token = random_string(32); - \Zotlabs\Lib\Verify::create('owt',0,$token,$hubloc['hubloc_addr']); + \Zotlabs\Lib\Verify::create('owt',0,$token,$hubloc['hubloc_network'] . ',' . $hubloc['hubloc_addr']); $result = ''; openssl_public_encrypt($token,$result,$hubloc['xchan_pubkey']); $ret['encrypted_token'] = base64url_encode($result); diff --git a/Zotlabs/Module/Photo.php b/Zotlabs/Module/Photo.php index 96a4e1f40..0dc6d0194 100644 --- a/Zotlabs/Module/Photo.php +++ b/Zotlabs/Module/Photo.php @@ -40,7 +40,7 @@ class Photo extends \Zotlabs\Web\Controller { call_hooks('cache_mode_hook', $cache_mode); $observer_xchan = get_observer_hash(); - $ismodified = $_SERVER['HTTP_IF_MODIFIED_SINCE']; + $cachecontrol = ''; if(isset($type)) { @@ -68,39 +68,44 @@ class Photo extends \Zotlabs\Web\Controller { } } - $modified = filemtime($default); - $default = z_root() . '/' . $default; $uid = $person; + + $data = ''; - $d = [ 'imgscale' => $resolution, 'channel_id' => $uid, 'default' => $default, 'data' => '', 'mimetype' => '' ]; - call_hooks('get_profile_photo',$d); - - $resolution = $d['imgscale']; - $uid = $d['channel_id']; - $default = $d['default']; - $data = $d['data']; - $mimetype = $d['mimetype']; - + $r = q("SELECT * FROM photo WHERE imgscale = %d AND uid = %d AND photo_usage = %d LIMIT 1", + intval($resolution), + intval($uid), + intval(PHOTO_PROFILE) + ); + if($r) { + $modified = strtotime($r[0]['edited'] . "Z"); + $mimetype = $r[0]['mimetype']; + if(intval($r[0]['os_storage'])) + $data = file_get_contents(dbunescbin($r[0]['content'])); + else + $data = dbunescbin($r[0]['content']); + } + if(! $data) { - $r = q("SELECT * FROM photo WHERE imgscale = %d AND uid = %d AND photo_usage = %d LIMIT 1", - intval($resolution), - intval($uid), - intval(PHOTO_PROFILE) - ); - if($r) { - $modified = strtotime($r[0]['edited'] . "Z"); - $data = dbunescbin($r[0]['content']); - $mimetype = $r[0]['mimetype']; - } - if(intval($r[0]['os_storage'])) - $data = file_get_contents($data); + $d = [ 'imgscale' => $resolution, 'channel_id' => $uid, 'default' => $default, 'data' => '', 'mimetype' => '' ]; + call_hooks('get_profile_photo',$d); + + $resolution = $d['imgscale']; + $uid = $d['channel_id']; + $default = $d['default']; + $data = $d['data']; + $mimetype = $d['mimetype']; + $modified = 0; } if(! $data) { - $x = z_fetch_url($default,true,0,[ 'novalidate' => true ]); + $x = z_fetch_url(z_root() . '/' . $default, true, 0, [ 'novalidate' => true ]); $data = ($x['success'] ? $x['body'] : EMPTY_STR); $mimetype = 'image/png'; + $modified = filemtime($default); } + + $cachecontrol = ', must-revalidate'; } else { @@ -157,18 +162,19 @@ class Photo extends \Zotlabs\Web\Controller { $allowed = (-1); if($u === PHOTO_CACHE) { // Validate cache - $cache = array( - 'resid' => $photo, - 'status' => false - ); - if($cache_mode['on']) + if($cache_mode['on']) { + $cache = array( + 'resid' => $photo, + 'status' => false + ); call_hooks('cache_url_hook', $cache); - if(! $cache['status']) { - $url = htmlspecialchars_decode($r[0]['display_path']); - if(strpos(z_root(),'https:') !== false && strpos($url,'https:') === false) - $url = z_root() . '/sslify/' . $filename . '?f=&url=' . urlencode($url); - header("Location: " . $url); - killme(); + if(! $cache['status']) { + $url = htmlspecialchars_decode($r[0]['display_path']); + // SSLify if needed + if(strpos(z_root(),'https:') !== false && strpos($url,'https:') === false) + $url = z_root() . '/sslify/' . $filename . '?f=&url=' . urlencode($url); + goaway($url); + } } } } @@ -213,38 +219,23 @@ class Photo extends \Zotlabs\Web\Controller { http_status_exit(404,'not found'); } + if(! $data) + killme(); + + $etag = md5($data . $modified); + + if($modified == 0) + $modified = time(); + header_remove('Pragma'); - if($ismodified === gmdate("D, d M Y H:i:s", $modified) . " GMT") { + if($_SERVER['HTTP_IF_NONE_MATCH'] === $etag || $_SERVER['HTTP_IF_MODIFIED_SINCE'] === gmdate("D, d M Y H:i:s", $modified) . " GMT") { header_remove('Expires'); header_remove('Cache-Control'); header_remove('Set-Cookie'); http_status_exit(304,'not modified'); - } - - if(! isset($data)) { - if(isset($resolution)) { - switch($resolution) { - case 4: - $default = get_default_profile_photo(); - break; - case 5: - $default = get_default_profile_photo(80); - break; - case 6: - $default = get_default_profile_photo(48); - break; - default: - killme(); - // NOTREACHED - break; - } - $x = z_fetch_url(z_root() . '/' . $default,true,0,[ 'novalidate' => true ]); - $data = ($x['success'] ? $x['body'] : EMPTY_STR); - $mimetype = 'image/png'; - } } - + if(isset($res) && intval($res) && $res < 500) { $ph = photo_factory($data, $mimetype); if($ph->is_valid()) { @@ -281,12 +272,13 @@ class Photo extends \Zotlabs\Web\Controller { $maxage = $expires - time(); header("Expires: " . gmdate("D, d M Y H:i:s", $expires) . " GMT"); - header("Cache-Control: max-age=" . $maxage); + header("Cache-Control: max-age=" . $maxage . $cachecontrol); } header("Content-type: " . $mimetype); header("Last-Modified: " . gmdate("D, d M Y H:i:s", $modified) . " GMT"); + header("ETag: " . $etag); header("Content-Length: " . (isset($filesize) ? $filesize : strlen($data))); // If it's a file resource, stream it. diff --git a/Zotlabs/Module/Photos.php b/Zotlabs/Module/Photos.php index a761dbd14..13ec64ab9 100644 --- a/Zotlabs/Module/Photos.php +++ b/Zotlabs/Module/Photos.php @@ -239,95 +239,53 @@ class Photos extends \Zotlabs\Web\Controller { intval($page_owner_uid) ); if(count($r)) { - $d = (($r[0]['os_storage']) ? @file_get_contents(dbunescbin($r[0]['content'])) : dbunescbin($r[0]['content'])); - $ph = photo_factory($d, $r[0]['mimetype']); + + $ph = photo_factory(@file_get_contents(dbunescbin($r[0]['content'])), $r[0]['mimetype']); if($ph->is_valid()) { $rotate_deg = ( (intval($_POST['rotate']) == 1) ? 270 : 90 ); $ph->rotate($rotate_deg); - - $width = $ph->getWidth(); - $height = $ph->getHeight(); - - if(intval($r[0]['os_storage'])) { - @file_put_contents($r[0]['content'],$ph->imageString()); - $data = $r[0]['content']; - $fsize = @filesize($r[0]['content']); - q("update attach set filesize = %d where hash = '%s' and uid = %d", - intval($fsize), - dbesc($resource_id), - intval($page_owner_uid) - ); - } - else { - $data = $ph->imageString(); - $fsize = strlen($data); - } - - $x = q("update photo set edited = '%s', content = '%s', filesize = %d, height = %d, width = %d where resource_id = '%s' and uid = %d and imgscale = 0", - dbesc(datetime_convert()), - dbescbin($data), - intval($fsize), - intval($height), - intval($width), + + $edited = datetime_convert(); + + q("update attach set filesize = %d, edited = '%s' where hash = '%s' and uid = %d", + strlen($ph->imageString()), + dbescdate($edited), dbesc($resource_id), intval($page_owner_uid) ); - + + $ph->saveImage(dbunescbin($r[0]['content'])); + + $arr = [ + 'aid' => get_account_id(), + 'uid' => intval($page_owner_uid), + 'resource_id' => dbesc($resource_id), + 'filename' => $r[0]['filename'], + 'imgscale' => 0, + 'album' => $r[0]['album'], + 'os_path' => $r[0]['os_path'], + 'os_storage' => 1, + 'os_syspath' => dbunescbin($r[0]['content']), + 'display_path' => $r[0]['display_path'], + 'photo_usage' => PHOTO_NORMAL, + 'edited' => dbescdate($edited) + ]; + + $ph->save($arr); + + unset($arr['os_syspath']); + if($width > 1024 || $height > 1024) $ph->scaleImage(1024); - - $width = $ph->getWidth(); - $height = $ph->getHeight(); - $data = $ph->imageString(); - $fsize = strlen($data); - - $x = q("update photo set edited = '%s', content = '%s', filesize = %d, height = %d, width = %d where resource_id = '%s' and uid = %d and imgscale = 1", - dbesc(datetime_convert()), - dbescbin($data), - intval($fsize), - intval($height), - intval($width), - dbesc($resource_id), - intval($page_owner_uid) - ); - - + $ph->storeThumbnail($arr, PHOTO_RES_1024); + if($width > 640 || $height > 640) $ph->scaleImage(640); - - $width = $ph->getWidth(); - $height = $ph->getHeight(); - $data = $ph->imageString(); - $fsize = strlen($data); - - $x = q("update photo set edited = '%s', content = '%s', filesize = %d, height = %d, width = %d where resource_id = '%s' and uid = %d and imgscale = 2", - dbesc(datetime_convert()), - dbescbin($data), - intval($fsize), - intval($height), - intval($width), - dbesc($resource_id), - intval($page_owner_uid) - ); - - + $ph->storeThumbnail($arr, PHOTO_RES_640); + if($width > 320 || $height > 320) $ph->scaleImage(320); - - $width = $ph->getWidth(); - $height = $ph->getHeight(); - $data = $ph->imageString(); - $fsize = strlen($data); - - $x = q("update photo set edited = '%s', content = '%s', filesize = %d, height = %d, width = %d where resource_id = '%s' and uid = %d and imgscale = 3", - dbesc(datetime_convert()), - dbescbin($data), - intval($fsize), - intval($height), - intval($width), - dbesc($resource_id), - intval($page_owner_uid) - ); + $ph->storeThumbnail($arr, PHOTO_RES_320); } } } @@ -848,7 +806,7 @@ class Photos extends \Zotlabs\Web\Controller { killme(); } else { - $o .= ""; + $o .= ""; $tpl = get_markup_template('photo_album.tpl'); $o .= replace_macros($tpl, array( '$photos' => $photos, @@ -1396,7 +1354,7 @@ class Photos extends \Zotlabs\Web\Controller { killme(); } else { - $o .= ""; + $o .= ""; $tpl = get_markup_template('photos_recent.tpl'); $o .= replace_macros($tpl, array( '$title' => t('Recent Photos'), diff --git a/Zotlabs/Module/Ping.php b/Zotlabs/Module/Ping.php index a367b1062..3dabe0f7b 100644 --- a/Zotlabs/Module/Ping.php +++ b/Zotlabs/Module/Ping.php @@ -447,7 +447,7 @@ class Ping extends \Zotlabs\Web\Controller { $when = day_translate(datetime_convert('UTC', (($rr['adjust']) ? date_default_timezone_get() : 'UTC'), $rr['dtstart'], $bd_format)) . (($today) ? ' ' . t('[today]') : ''); $result[] = array( - 'notify_link' => z_root() . '/events', /// @FIXME this takes you to an edit page and it may not be yours, we really want to just view the single event --> '/events/event/' . $rr['event_hash'], + 'notify_link' => z_root() . '/cdav/calendar/' . $rr['event_hash'], 'name' => $rr['xchan_name'], 'addr' => $rr['xchan_addr'], 'url' => $rr['xchan_url'], diff --git a/Zotlabs/Module/Poster.php b/Zotlabs/Module/Poster.php new file mode 100644 index 000000000..10317ee61 --- /dev/null +++ b/Zotlabs/Module/Poster.php @@ -0,0 +1,37 @@ + array($sync))); + } + + $_SESSION['reload_avatar'] = true; + + goaway(z_root() . '/profiles'); + } if((array_key_exists('cropfinal',$_POST)) && (intval($_POST['cropfinal']) == 1)) { // logger('crop: ' . print_r($_POST,true)); - - // phase 2 - we have finished cropping if(argc() != 2) { @@ -119,39 +144,48 @@ class Profile_photo extends \Zotlabs\Web\Controller { 'filename' => $base_image['filename'], 'album' => t('Profile Photos'), 'os_path' => $base_image['os_path'], - 'display_path' => $base_image['display_path'] + 'display_path' => $base_image['display_path'], + 'photo_usage' => PHOTO_PROFILE, + 'edited' => dbescdate($base_image['edited']) ]; - $p['imgscale'] = PHOTO_RES_PROFILE_300; $p['photo_usage'] = (($is_default_profile) ? PHOTO_PROFILE : PHOTO_NORMAL); - $r1 = $im->save($p); + $r1 = $im->storeThumbnail($p, PHOTO_RES_PROFILE_300); $im->scaleImage(80); - $p['imgscale'] = PHOTO_RES_PROFILE_80; - - $r2 = $im->save($p); + $r2 = $im->storeThumbnail($p, PHOTO_RES_PROFILE_80); $im->scaleImage(48); - $p['imgscale'] = PHOTO_RES_PROFILE_48; - - $r3 = $im->save($p); - + $r3 = $im->storeThumbnail($p, PHOTO_RES_PROFILE_48); + if($r1 === false || $r2 === false || $r3 === false) { // if one failed, delete them all so we can start over. notice( t('Image resize failed.') . EOL ); - $x = q("delete from photo where resource_id = '%s' and uid = %d and imgscale in ( %d, %d, %d ) ", + $x = q("delete from photo where resource_id = '%s' and uid = %d and imgscale in ( %d, %d, %d )", dbesc($base_image['resource_id']), local_channel(), intval(PHOTO_RES_PROFILE_300), intval(PHOTO_RES_PROFILE_80), intval(PHOTO_RES_PROFILE_48) ); + + $x = q("SELECT content FROM photo WHERE resource_id = '%s' AND uid = %d AND os_storage = 1 AND imgscale IN ( %d, %d, %d )", + dbesc($base_image['resource_id']), + local_channel(), + intval(PHOTO_RES_PROFILE_300), + intval(PHOTO_RES_PROFILE_80), + intval(PHOTO_RES_PROFILE_48) + ); + if($x) { + foreach($x as $xx) { + @unlink(dbunescbin($xx['content'])); + } + } + return; } - $channel = \App::get_channel(); - // If setting for the default profile, unset the profile photo flag from any other photos I own if($is_default_profile) { @@ -198,7 +232,7 @@ class Profile_photo extends \Zotlabs\Web\Controller { $r = q("UPDATE xchan set xchan_photo_mimetype = '%s', xchan_photo_date = '%s', xchan_photo_l = '%s', xchan_photo_m = '%s', xchan_photo_s = '%s' where xchan_hash = '%s'", dbesc($im->getType()), - dbesc(datetime_convert()), + dbescdate($base_image['edited']), dbesc(z_root() . '/photo/profile/l/' . $channel['channel_id']), dbesc(z_root() . '/photo/profile/m/' . $channel['channel_id']), dbesc(z_root() . '/photo/profile/s/' . $channel['channel_id']), @@ -245,7 +279,7 @@ class Profile_photo extends \Zotlabs\Web\Controller { else { require_once('include/attach.php'); - $res = attach_store(\App::get_channel(), get_observer_hash(), '', array('album' => t('Profile Photos'), 'hash' => $hash)); + $res = attach_store(\App::get_channel(), get_observer_hash(), '', array('album' => t('Profile Photos'), 'hash' => $hash, 'nosync' => true)); logger('attach_store: ' . print_r($res,true)); } @@ -353,20 +387,23 @@ class Profile_photo extends \Zotlabs\Web\Controller { if($havescale) { // unset any existing profile photos - $r = q("UPDATE photo SET photo_usage = %d WHERE photo_usage = %d AND uid = %d", + $x = q("UPDATE photo SET photo_usage = %d WHERE photo_usage = %d AND uid = %d", intval(PHOTO_NORMAL), intval(PHOTO_PROFILE), - intval(local_channel())); - - $r = q("UPDATE photo SET photo_usage = %d WHERE uid = %d AND resource_id = '%s'", + intval(local_channel()) + ); + + $edited = datetime_convert(); + + $x = q("UPDATE photo SET photo_usage = %d, edited = '%s' WHERE uid = %d AND resource_id = '%s' AND imgscale > 0", intval(PHOTO_PROFILE), + dbescdate($edited), intval(local_channel()), dbesc($resource_id) - ); + ); - $r = q("UPDATE xchan set xchan_photo_date = '%s' - where xchan_hash = '%s'", - dbesc(datetime_convert()), + $x = q("UPDATE xchan SET xchan_photo_date = '%s' WHERE xchan_hash = '%s'", + dbescdate($edited), dbesc($channel['xchan_hash']) ); @@ -376,8 +413,10 @@ class Profile_photo extends \Zotlabs\Web\Controller { if($sync) build_sync_packet($channel['channel_id'],array('file' => array($sync))); + $_SESSION['reload_avatar'] = true; \Zotlabs\Daemon\Master::Summon(array('Directory',local_channel())); + goaway(z_root() . '/profiles'); } @@ -457,6 +496,7 @@ class Profile_photo extends \Zotlabs\Web\Controller { '$lbl_profiles' => t('Select a profile:'), '$title' => (($importing) ? t('Use Photo for Profile') : t('Change Profile Photo')), '$submit' => (($importing) ? t('Use') : t('Upload')), + '$remove' => t('Remove'), '$profiles' => $profiles, '$single' => ((count($profiles) == 1) ? true : false), '$profile0' => $profiles[0], diff --git a/Zotlabs/Module/Pubstream.php b/Zotlabs/Module/Pubstream.php index 94df29984..84ac42f72 100644 --- a/Zotlabs/Module/Pubstream.php +++ b/Zotlabs/Module/Pubstream.php @@ -149,11 +149,11 @@ class Pubstream extends \Zotlabs\Web\Controller { '$order' => 'comment', '$file' => '', '$cats' => '', - '$tags' => $hashtags, + '$tags' => (($hashtags) ? urlencode($hashtags) : ''), '$dend' => '', - '$mid' => $mid, + '$mid' => (($mid) ? urlencode($mid) : ''), '$verb' => '', - '$net' => $net, + '$net' => (($net) ? urlencode($net) : ''), '$dbegin' => '' )); } diff --git a/Zotlabs/Module/React.php b/Zotlabs/Module/React.php index 3920301f5..f80b04a3f 100644 --- a/Zotlabs/Module/React.php +++ b/Zotlabs/Module/React.php @@ -59,6 +59,14 @@ class React extends \Zotlabs\Web\Controller { $n['body'] = "\n\n[zmg=32x32]" . z_root() . '/images/emoji/' . $emoji . '.png[/zmg]' . "\n\n"; $n['author_xchan'] = $channel['channel_hash']; + $n['tgt_type'] = 'Image'; + $n['target'] = [ + 'type' => 'Image', + 'name' => $emoji, + 'url' => z_root() . '/images/emoji/' . $emoji . '.png' + ]; + + $x = item_store($n); retain_item($postid); diff --git a/Zotlabs/Module/Register.php b/Zotlabs/Module/Register.php index f9d81be0c..bc813f8e1 100644 --- a/Zotlabs/Module/Register.php +++ b/Zotlabs/Module/Register.php @@ -1,10 +1,11 @@ %s - INTERVAL %s", @@ -269,7 +272,8 @@ class Register extends \Zotlabs\Web\Controller { require_once('include/bbcode.php'); $o = replace_macros(get_markup_template('register.tpl'), array( - + + '$form_security_token' => get_form_security_token("register"), '$title' => t('Registration'), '$reg_is' => $registration_is, '$registertext' => bbcode(get_config('system','register_text')), diff --git a/Zotlabs/Module/Settings/Calendar.php b/Zotlabs/Module/Settings/Calendar.php index a27bf0fa5..0298b412e 100644 --- a/Zotlabs/Module/Settings/Calendar.php +++ b/Zotlabs/Module/Settings/Calendar.php @@ -36,7 +36,7 @@ class Calendar { '$rpath' => $rpath, '$action_url' => 'settings/' . $module, '$form_security_token' => get_form_security_token('settings_' . $module), - '$title' => t('CalDAV Settings'), + '$title' => t('Calendar Settings'), '$features' => process_module_features_get(local_channel(), $features), '$submit' => t('Submit') )); diff --git a/Zotlabs/Module/Setup.php b/Zotlabs/Module/Setup.php index 370b7b9f8..541e4fa21 100644 --- a/Zotlabs/Module/Setup.php +++ b/Zotlabs/Module/Setup.php @@ -39,12 +39,12 @@ class Setup extends \Zotlabs\Web\Controller { ini_set('display_errors', '1'); // $baseurl/setup/testrewrite to test if rewrite in .htaccess is working - if (argc() == 2 && argv(1) == "testrewrite") { + if(argc() == 2 && argv(1) == 'testrewrite') { echo 'ok'; killme(); } - if (x($_POST, 'pass')) { + if(x($_POST, 'pass')) { $this->install_wizard_pass = intval($_POST['pass']); } else { $this->install_wizard_pass = 1; @@ -63,7 +63,6 @@ class Setup extends \Zotlabs\Web\Controller { return; // implied break; case 3: - $urlpath = \App::get_path(); $dbhost = trim($_POST['dbhost']); $dbport = intval(trim($_POST['dbport'])); $dbuser = trim($_POST['dbuser']); @@ -89,7 +88,6 @@ class Setup extends \Zotlabs\Web\Controller { return; // implied break; case 4: - $urlpath = \App::get_path(); $dbhost = trim($_POST['dbhost']); $dbport = intval(trim($_POST['dbport'])); $dbuser = trim($_POST['dbuser']); @@ -162,7 +160,6 @@ class Setup extends \Zotlabs\Web\Controller { * * @return string parsed HTML output */ - function get() { $o = ''; @@ -213,10 +210,10 @@ class Setup extends \Zotlabs\Web\Controller { } if(x(\App::$data, 'txt') && strlen(\App::$data['txt'])) { - $db_return_text .= $this->manual_config($a); + $db_return_text .= $this->manual_config(); } - if ($db_return_text != "") { + if($db_return_text != '') { $tpl = get_markup_template('install.tpl'); return replace_macros($tpl, array( '$title' => $install_title, @@ -242,7 +239,7 @@ class Setup extends \Zotlabs\Web\Controller { $this->check_keys($checks); - if (x($_POST, 'phpath')) + if(x($_POST, 'phpath')) $phpath = notags(trim($_POST['phpath'])); $this->check_php($phpath, $checks); @@ -278,7 +275,6 @@ class Setup extends \Zotlabs\Web\Controller { $dbtype = intval(trim($_POST['dbtype'])); $phpath = trim($_POST['phpath']); $adminmail = trim($_POST['adminmail']); - $siteurl = trim($_POST['siteurl']); $tpl = get_markup_template('install_db.tpl'); $o .= replace_macros($tpl, array( @@ -320,7 +316,6 @@ class Setup extends \Zotlabs\Web\Controller { $phpath = trim($_POST['phpath']); $adminmail = trim($_POST['adminmail']); - $siteurl = trim($_POST['siteurl']); $timezone = ((x($_POST,'timezone')) ? ($_POST['timezone']) : 'America/Los_Angeles'); @@ -363,12 +358,12 @@ class Setup extends \Zotlabs\Web\Controller { * @param string $help optional help string */ function check_add(&$checks, $title, $status, $required, $help = '') { - $checks[] = array( + $checks[] = [ 'title' => $title, 'status' => $status, 'required' => $required, 'help' => $help - ); + ]; } /** @@ -380,12 +375,12 @@ class Setup extends \Zotlabs\Web\Controller { function check_php(&$phpath, &$checks) { $help = ''; - if(version_compare(PHP_VERSION, '5.5') < 0) { - $help .= t('PHP version 5.5 or greater is required.'); - $this->check_add($checks, t('PHP version'), false, false, $help); + if(version_compare(PHP_VERSION, '7.1') < 0) { + $help .= t('PHP version 7.1 or greater is required.'); + $this->check_add($checks, t('PHP version'), false, true, $help); } - if (strlen($phpath)) { + if(strlen($phpath)) { $passed = file_exists($phpath); } elseif(function_exists('shell_exec')) { @@ -419,6 +414,7 @@ class Setup extends \Zotlabs\Web\Controller { $result = trim(shell_exec($cmd)); else $help .= t('Unable to check command line PHP, as shell_exec() is disabled. This is required.') . EOL; + $passed2 = (($result == $str) ? true : false); if(!$passed2) { $help .= t('The command line version of PHP on your system does not have "register_argc_argv" enabled.'). EOL; @@ -445,14 +441,14 @@ class Setup extends \Zotlabs\Web\Controller { $result = getPhpiniUploadLimits(); if($result['post_max_size'] < 4194304 || $result['max_upload_filesize'] < 4194304) { - $mem_warning = '' .t('This is not sufficient to upload larger images or files. You should be able to upload at least 4 MB at once.') . ''; - } + $mem_warning = '' .t('This is not sufficient to upload larger images or files. You should be able to upload at least 4 MB at once.') . ''; + } $help = sprintf(t('Your max allowed total upload size is set to %s. Maximum size of one file to upload is set to %s. You are allowed to upload up to %d files at once.'), userReadableSize($result['post_max_size']), userReadableSize($result['max_upload_filesize']), $result['max_file_uploads'] ); - $help .= $mem_warning; + $help .= $mem_warning; $help .= '

' . t('You can adjust these settings in the server php.ini file.'); $this->check_add($checks, t('PHP upload limits'), true, false, $help); @@ -467,7 +463,7 @@ class Setup extends \Zotlabs\Web\Controller { $help = ''; $res = false; - if (function_exists('openssl_pkey_new')) { + if(function_exists('openssl_pkey_new')) { $res = openssl_pkey_new(array( 'digest_alg' => 'sha1', 'private_key_bits' => 4096, @@ -477,7 +473,7 @@ class Setup extends \Zotlabs\Web\Controller { // Get private key - if (! $res) { + if(! $res) { $help .= t('Error: the "openssl_pkey_new" function on this system is not able to generate encryption keys'). EOL; $help .= t('If running under Windows, please see "http://www.php.net/manual/en/openssl.installation.php".'); } @@ -508,7 +504,7 @@ class Setup extends \Zotlabs\Web\Controller { $this->check_add($ck_funcs, t('zip PHP module'), true, true); if(function_exists('apache_get_modules')){ - if (! in_array('mod_rewrite', apache_get_modules())) { + if(! in_array('mod_rewrite', apache_get_modules())) { $this->check_add($ck_funcs, t('Apache mod_rewrite module'), false, true, t('Error: Apache webserver mod-rewrite module is required but not installed.')); } else { $this->check_add($ck_funcs, t('Apache mod_rewrite module'), true, true); @@ -577,7 +573,7 @@ class Setup extends \Zotlabs\Web\Controller { $fname = '.htconfig.php'; - if((file_exists($fname) && is_writable($fname)) || + if((file_exists($fname) && is_writable($fname)) || (! (file_exists($fname) && is_writable('.')))) { $this->check_add($checks, t('.htconfig.php is writable'), $status, true, $help); return; @@ -643,7 +639,7 @@ class Setup extends \Zotlabs\Web\Controller { $url = z_root() . '/setup/testrewrite'; - if (function_exists('curl_init')){ + if(function_exists('curl_init')){ $test = z_fetch_url($url); if(! $test['success']) { if(strstr($url,'https://')) { @@ -666,14 +662,13 @@ class Setup extends \Zotlabs\Web\Controller { $help .= t('If your certificate is not recognized, members of other sites (who may themselves have valid certificates) will get a warning message on their own site complaining about security issues.') . EOL; $help .= t('This can cause usability issues elsewhere (not just on your own site) so we must insist on this requirement.') .EOL; $help .= t('Providers are available that issue free certificates which are browser-valid.'). EOL; - $help .= t('If you are confident that the certificate is valid and signed by a trusted authority, check to see if you have failed to install an intermediate cert. These are not normally required by browsers, but are required for server-to-server communications.') . EOL; $this->check_add($checks, t('SSL certificate validation'), false, true, $help); } } - if ((! $test['success']) || ($test['body'] != "ok")) { + if((! $test['success']) || ($test['body'] != "ok")) { $status = false; $help = t('Url rewrite in .htaccess is not working. Check your server configuration.'.'Test: '.var_export($test,true)); } @@ -687,10 +682,9 @@ class Setup extends \Zotlabs\Web\Controller { /** * @brief * - * @param App &$a * @return string with paresed HTML */ - function manual_config(&$a) { + function manual_config() { $data = htmlspecialchars(\App::$data['txt'], ENT_COMPAT, 'UTF-8'); $o = t('The database configuration file ".htconfig.php" could not be written. Please use the enclosed text to create a configuration file in your web server root.'); $o .= ""; @@ -700,14 +694,19 @@ class Setup extends \Zotlabs\Web\Controller { function load_database_rem($v, $i){ $l = trim($i); - if (strlen($l)>1 && ($l[0]=="-" || ($l[0]=="/" && $l[1]=="*"))){ + if(strlen($l)>1 && ($l[0]=="-" || ($l[0]=="/" && $l[1]=="*"))){ return $v; } else { return $v."\n".$i; } } - + /** + * @brief Executes the SQL install script and create database tables. + * + * @param dba_driver $db (unused) + * @return boolean|string false on success or error message as string + */ function load_database($db) { $str = file_get_contents(\DBA::$dba->get_install_script()); $arr = explode(';', $str); @@ -733,6 +732,12 @@ class Setup extends \Zotlabs\Web\Controller { // install the standard theme set_config('system', 'allowed_themes', 'redbasic'); + // if imagick converter is installed, use it + if(@is_executable('/usr/bin/convert')) { + set_config('system','imagick_convert_path','/usr/bin/convert'); + } + + // Set a lenient list of ciphers if using openssl. Other ssl engines // (e.g. NSS used in RedHat) require different syntax, so hopefully // the default curl cipher list will work for most sites. If not, @@ -767,12 +772,12 @@ class Setup extends \Zotlabs\Web\Controller { /** * @brief * - * @param unknown $v + * @param array $v * @param array $c * @return array */ static private function check_passed($v, $c) { - if ($c['required']) + if($c['required']) $v = $v && $c['status']; return $v; diff --git a/Zotlabs/Module/Subthread.php b/Zotlabs/Module/Subthread.php index 54343fdfa..30e57197d 100644 --- a/Zotlabs/Module/Subthread.php +++ b/Zotlabs/Module/Subthread.php @@ -33,7 +33,7 @@ class Subthread extends \Zotlabs\Web\Controller { if(! $i) { $i = q("select * from item where id = %d and uid = %d", - intval($postid), + intval($item_id), intval($sys['channel_id']) ); diff --git a/Zotlabs/Module/Tagger.php b/Zotlabs/Module/Tagger.php index 24adf1bde..e6e80dce3 100644 --- a/Zotlabs/Module/Tagger.php +++ b/Zotlabs/Module/Tagger.php @@ -69,7 +69,7 @@ class Tagger extends \Zotlabs\Web\Controller { $post_type = t('photo'); break; case 'event': - $targgettype = ACTIVITY_OBJ_EVENT; + $targettype = ACTIVITY_OBJ_EVENT; $post_type = t('event'); break; default: diff --git a/Zotlabs/Module/Viewconnections.php b/Zotlabs/Module/Viewconnections.php index 0a5e86907..30df0b9e4 100644 --- a/Zotlabs/Module/Viewconnections.php +++ b/Zotlabs/Module/Viewconnections.php @@ -107,7 +107,7 @@ class Viewconnections extends \Zotlabs\Web\Controller { killme(); } else { - $o .= ""; + $o .= ""; $tpl = get_markup_template("viewcontact_template.tpl"); $o .= replace_macros($tpl, array( '$title' => t('View Connections'), diff --git a/Zotlabs/Module/Viewsrc.php b/Zotlabs/Module/Viewsrc.php index b73d81283..3e49b9db4 100644 --- a/Zotlabs/Module/Viewsrc.php +++ b/Zotlabs/Module/Viewsrc.php @@ -25,7 +25,7 @@ class Viewsrc extends \Zotlabs\Web\Controller { notice( t('Item not found.') . EOL); } - $item_normal = item_normal(); + $item_normal = item_normal_search(); if(local_channel() && $item_id) { $r = q("select id, mid, item_flags, mimetype, item_obscured, body, llink, plink from item where uid in (%d , %d) and id = %d $item_normal limit 1", diff --git a/Zotlabs/Module/Wall_attach.php b/Zotlabs/Module/Wall_attach.php index 2250e6e44..0ede3ad90 100644 --- a/Zotlabs/Module/Wall_attach.php +++ b/Zotlabs/Module/Wall_attach.php @@ -96,9 +96,26 @@ class Wall_attach extends \Zotlabs\Web\Controller { $s = "\n\n" . $r['body'] . "\n\n"; } else { - $s = "\n\n" . '[attachment]' . $r['data']['hash'] . ',' . $r['data']['revision'] . '[/attachment]' . "\n"; + if(strpos($r['data']['filetype'],'video') === 0) { + // give a wee bit of time for the background thumbnail processor to do its thing + // or else we'll never see a video poster + sleep(3); + $url = z_root() . '/cloud/' . $channel['channel_address'] . '/' . $r['data']['display_path']; + $thumb = Linkinfo::get_video_poster($url); + if($thumb) { + $s = "\n\n" . '[zvideo poster=\'' . $thumb . '\']' . $url . '[/zvideo]' . "\n\n"; + } + else { + $s = "\n\n" . '[zvideo]' . $url . '[/zvideo]' . "\n\n"; + } + } + if(strpos($r['data']['filetype'],'audio') === 0) { + $url = z_root() . '/cloud/' . $channel['channel_address'] . '/' . $r['data']['display_path']; + echo "\n\n" . '[zaudio]' . $url . '[/zaudio]' . "\n\n"; + } + + $s .= "\n\n" . '[attachment]' . $r['data']['hash'] . ',' . $r['data']['revision'] . '[/attachment]' . "\n"; } - $sync = attach_export_data($channel,$r['data']['hash']); if($sync) { diff --git a/Zotlabs/Module/Wfinger.php b/Zotlabs/Module/Wfinger.php index 03275abbc..a19bdbedc 100644 --- a/Zotlabs/Module/Wfinger.php +++ b/Zotlabs/Module/Wfinger.php @@ -128,7 +128,7 @@ class Wfinger extends \Zotlabs\Web\Controller { 'http://webfinger.net/ns/name' => $r[0]['channel_name'], 'http://xmlns.com/foaf/0.1/name' => $r[0]['channel_name'], 'https://w3id.org/security/v1#publicKeyPem' => $r[0]['xchan_pubkey'], - 'http://purl.org/zot/federation' => 'zot' + 'http://purl.org/zot/federation' => 'zot,zot6' ]; foreach($aliases as $alias) diff --git a/Zotlabs/Module/Wiki.php b/Zotlabs/Module/Wiki.php index 502f96a62..169dc6de1 100644 --- a/Zotlabs/Module/Wiki.php +++ b/Zotlabs/Module/Wiki.php @@ -293,9 +293,9 @@ class Wiki extends Controller { } //$wikiheaderName = urldecode($wikiUrlName); - $wikiheaderName = NativeWiki::name_decode($wikiUrlName); + $wikiheaderName = escape_tags(NativeWiki::name_decode($wikiUrlName)); //$wikiheaderPage = urldecode($pageUrlName); - $wikiheaderPage = NativeWiki::name_decode($pageUrlName); + $wikiheaderPage = escape_tags(NativeWiki::name_decode($pageUrlName)); $renamePage = (($wikiheaderPage === 'Home') ? '' : t('Rename page')); $sharePage = t('Share'); @@ -373,13 +373,13 @@ class Wiki extends Controller { $placeholder = t('Short description of your changes (optional)'); - $zrl = urlencode( z_root() . '/wiki/' . argv(1) . '/' . NativeWiki::name_encode($wikiUrlName) . '/' . NativeWiki::name_encode($pageUrlName) ); + $zrl = z_root() . '/wiki/' . argv(1) . '/' . NativeWiki::name_encode($wikiUrlName) . '/' . NativeWiki::name_encode($pageUrlName); $o .= replace_macros(get_markup_template('wiki.tpl'),array( '$wikiheaderName' => $wikiheaderName, '$wikiheaderPage' => $wikiheaderPage, '$renamePage' => $renamePage, '$sharePage' => $sharePage, - '$shareLink' => '#^[zrl=' . $zrl . ']' . '[ ' . $owner['channel_name'] . ' ] ' . $wikiheaderName . ' - ' . $wikiheaderPage . '[/zrl]', + '$shareLink' => urlencode('#^[zrl=' . $zrl . ']' . '[ ' . $owner['channel_name'] . ' ] ' . $wikiheaderName . ' - ' . $wikiheaderPage . '[/zrl]'), '$showPageControls' => $showPageControls, '$editOrSourceLabel' => (($showPageControls) ? t('Edit') : t('Source')), '$tools_label' => 'Page Tools', diff --git a/Zotlabs/Photo/PhotoDriver.php b/Zotlabs/Photo/PhotoDriver.php index c47a7c3b2..bacf926ff 100644 --- a/Zotlabs/Photo/PhotoDriver.php +++ b/Zotlabs/Photo/PhotoDriver.php @@ -448,6 +448,7 @@ abstract class PhotoDriver { $p['width'] = (($arr['width']) ? $arr['width'] : $this->getWidth()); $p['height'] = (($arr['height']) ? $arr['height'] : $this->getHeight()); $p['expires'] = (($arr['expires']) ? $arr['expires'] : gmdate('Y-m-d H:i:s', time() + get_config('system', 'photo_cache_time', 86400))); + $p['profile'] = ((array_key_exists('profile', $arr)) ? intval($arr['profile']) : 0); if(! intval($p['imgscale'])) logger('save: ' . print_r($arr, true), LOGGER_DATA); @@ -481,18 +482,49 @@ abstract class PhotoDriver { allow_gid = '%s', deny_cid = '%s', deny_gid = '%s', - expires = '%s' + expires = '%s', + profile = %d where id = %d", - intval($p['aid']), intval($p['uid']), dbesc($p['xchan']), dbesc($p['resource_id']), dbescdate($p['created']), dbescdate($p['edited']), dbesc(basename($p['filename'])), dbesc($p['mimetype']), dbesc($p['album']), intval($p['height']), intval($p['width']), (intval($p['os_storage']) ? dbescbin($p['os_syspath']) : dbescbin($this->imageString())), intval($p['os_storage']), (intval($p['os_storage']) ? @filesize($p['os_syspath']) : strlen($this->imageString())), intval($p['imgscale']), intval($p['photo_usage']), dbesc($p['title']), dbesc($p['description']), dbesc($p['os_path']), dbesc($p['display_path']), dbesc($p['allow_cid']), dbesc($p['allow_gid']), dbesc($p['deny_cid']), dbesc($p['deny_gid']), dbescdate($p['expires']), intval($x[0]['id'])); + intval($p['aid']), intval($p['uid']), dbesc($p['xchan']), dbesc($p['resource_id']), dbescdate($p['created']), dbescdate($p['edited']), dbesc(basename($p['filename'])), dbesc($p['mimetype']), dbesc($p['album']), intval($p['height']), intval($p['width']), (intval($p['os_storage']) ? dbescbin($p['os_syspath']) : dbescbin($this->imageString())), intval($p['os_storage']), (intval($p['os_storage']) ? @filesize($p['os_syspath']) : strlen($this->imageString())), intval($p['imgscale']), intval($p['photo_usage']), dbesc($p['title']), dbesc($p['description']), dbesc($p['os_path']), dbesc($p['display_path']), dbesc($p['allow_cid']), dbesc($p['allow_gid']), dbesc($p['deny_cid']), dbesc($p['deny_gid']), dbescdate($p['expires']), intval($p['profile']), intval($x[0]['id'])); } else { $p['created'] = (($arr['created']) ? $arr['created'] : $p['edited']); $r = q("INSERT INTO photo - ( aid, uid, xchan, resource_id, created, edited, filename, mimetype, album, height, width, content, os_storage, filesize, imgscale, photo_usage, title, description, os_path, display_path, allow_cid, allow_gid, deny_cid, deny_gid, expires ) - VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', %d, %d, %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s' )", intval($p['aid']), intval($p['uid']), dbesc($p['xchan']), dbesc($p['resource_id']), dbescdate($p['created']), dbescdate($p['edited']), dbesc(basename($p['filename'])), dbesc($p['mimetype']), dbesc($p['album']), intval($p['height']), intval($p['width']), (intval($p['os_storage']) ? dbescbin($p['os_syspath']) : dbescbin($this->imageString())), intval($p['os_storage']), (intval($p['os_storage']) ? @filesize($p['os_syspath']) : strlen($this->imageString())), intval($p['imgscale']), intval($p['photo_usage']), dbesc($p['title']), dbesc($p['description']), dbesc($p['os_path']), dbesc($p['display_path']), dbesc($p['allow_cid']), dbesc($p['allow_gid']), dbesc($p['deny_cid']), dbesc($p['deny_gid']), dbescdate($p['expires'])); + ( aid, uid, xchan, resource_id, created, edited, filename, mimetype, album, height, width, content, os_storage, filesize, imgscale, photo_usage, title, description, os_path, display_path, allow_cid, allow_gid, deny_cid, deny_gid, expires, profile ) + VALUES ( %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d, %d, '%s', %d, %d, %d, %d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s', %d)", intval($p['aid']), intval($p['uid']), dbesc($p['xchan']), dbesc($p['resource_id']), dbescdate($p['created']), dbescdate($p['edited']), dbesc(basename($p['filename'])), dbesc($p['mimetype']), dbesc($p['album']), intval($p['height']), intval($p['width']), (intval($p['os_storage']) ? dbescbin($p['os_syspath']) : dbescbin($this->imageString())), intval($p['os_storage']), (intval($p['os_storage']) ? @filesize($p['os_syspath']) : strlen($this->imageString())), intval($p['imgscale']), intval($p['photo_usage']), dbesc($p['title']), dbesc($p['description']), dbesc($p['os_path']), dbesc($p['display_path']), dbesc($p['allow_cid']), dbesc($p['allow_gid']), dbesc($p['deny_cid']), dbesc($p['deny_gid']), dbescdate($p['expires']), intval($p['profile'])); } logger('Photo save imgscale ' . $p['imgscale'] . ' returned ' . intval($r)); return $r; } + + /** + * @brief Stores thumbnail to database or filesystem. + * + * @param array $arr + * @param scale int + * @return boolean|array + */ + public function storeThumbnail($arr, $scale = 0) { + + $arr['imgscale'] = $scale; + + if(boolval(get_config('system','filesystem_storage_thumbnails', 0)) && $scale > 0) { + $channel = \App::get_channel(); + $arr['os_storage'] = 1; + $arr['os_syspath'] = 'store/' . $channel['channel_address'] . '/' . $arr['os_path'] . '-' . $scale; + if(! $this->saveImage($arr['os_syspath'])) + return false; + } + else + $arr['os_storage'] = 0; + + if(! $this->save($arr)) { + if(array_key_exists('os_syspath', $arr)) + @unlink($arr['os_syspath']); + return false; + } + + return true; + } } diff --git a/Zotlabs/Render/SmartyInterface.php b/Zotlabs/Render/SmartyInterface.php index 9c9a501c0..a40effecf 100755 --- a/Zotlabs/Render/SmartyInterface.php +++ b/Zotlabs/Render/SmartyInterface.php @@ -2,7 +2,10 @@ namespace Zotlabs\Render; -class SmartyInterface extends \Smarty { +use Smarty; +use App; + +class SmartyInterface extends Smarty { public $filename; @@ -16,26 +19,27 @@ class SmartyInterface extends \Smarty { // The order is thus very important here $template_dirs = array('theme' => "view/theme/$thname/tpl/"); - if( x(\App::$theme_info,"extends") ) + if ( x(App::$theme_info,"extends") ) { $template_dirs = $template_dirs + array('extends' => "view/theme/" . \App::$theme_info["extends"] . "/tpl/"); + } $template_dirs = $template_dirs + array('base' => 'view/tpl/'); $this->setTemplateDir($template_dirs); - $basecompiledir = \App::$config['system']['smarty3_folder']; + $basecompiledir = App::$config['system']['smarty3_folder']; $this->setCompileDir($basecompiledir.'/compiled/'); $this->setConfigDir($basecompiledir.'/config/'); $this->setCacheDir($basecompiledir.'/cache/'); - $this->left_delimiter = \App::get_template_ldelim('smarty3'); - $this->right_delimiter = \App::get_template_rdelim('smarty3'); + $this->left_delimiter = App::get_template_ldelim('smarty3'); + $this->right_delimiter = App::get_template_rdelim('smarty3'); // Don't report errors so verbosely $this->error_reporting = E_ALL & (~E_NOTICE); } function parsed($template = '') { - if($template) { + if ($template) { return $this->fetch('string:' . $template); } return $this->fetch('file:' . $this->filename); diff --git a/Zotlabs/Render/SmartyTemplate.php b/Zotlabs/Render/SmartyTemplate.php index f14d63064..61fb72f8a 100755 --- a/Zotlabs/Render/SmartyTemplate.php +++ b/Zotlabs/Render/SmartyTemplate.php @@ -2,28 +2,33 @@ namespace Zotlabs\Render; +use App; + + class SmartyTemplate implements TemplateEngine { static $name ="smarty3"; - public function __construct(){ + public function __construct() { // Cannot use get_config() here because it is called during installation when there is no DB. // FIXME: this may leak private information such as system pathnames. - $basecompiledir = ((array_key_exists('smarty3_folder',\App::$config['system'])) - ? \App::$config['system']['smarty3_folder'] : ''); - if (!$basecompiledir) $basecompiledir = str_replace('Zotlabs','',dirname(__dir__)) . "/" . TEMPLATE_BUILD_PATH; - if (!is_dir($basecompiledir)) { + $basecompiledir = ((array_key_exists('smarty3_folder', App::$config['system'])) + ? App::$config['system']['smarty3_folder'] : ''); + if (! $basecompiledir) { + $basecompiledir = str_replace('Zotlabs','',dirname(__dir__)) . "/" . TEMPLATE_BUILD_PATH; + } + if (! is_dir($basecompiledir)) { @os_mkdir(TEMPLATE_BUILD_PATH, STORAGE_DEFAULT_PERMISSIONS, true); - if (!is_dir($basecompiledir)) { + if (! is_dir($basecompiledir)) { echo "ERROR: folder $basecompiledir does not exist."; killme(); } } - if(!is_writable($basecompiledir)){ + if (! is_writable($basecompiledir)) { echo "ERROR: folder $basecompiledir must be writable by webserver."; killme(); } - \App::$config['system']['smarty3_folder'] = $basecompiledir; + App::$config['system']['smarty3_folder'] = $basecompiledir; } // TemplateEngine interface @@ -31,18 +36,18 @@ class SmartyTemplate implements TemplateEngine { public function replace_macros($s, $r) { $template = ''; - // these are available for use in all templates + // macro or macros available for use in all templates $r['$z_baseurl'] = z_root(); $r['$z_server_role'] = \Zotlabs\Lib\System::get_server_role(); $r['$z_techlevel'] = get_account_techlevel(); - if(gettype($s) === 'string') { + if (gettype($s) === 'string') { $template = $s; $s = new SmartyInterface(); } - foreach($r as $key=>$value) { - if($key[0] === '$') { + foreach ($r as $key=>$value) { + if ($key[0] === '$') { $key = substr($key, 1); } $s->assign($key, $value); @@ -50,32 +55,32 @@ class SmartyTemplate implements TemplateEngine { return $s->parsed($template); } - public function get_markup_template($file, $root=''){ + public function get_markup_template($file, $root = '') { $template_file = theme_include($file, $root); - if($template_file) { + if ($template_file) { $template = new SmartyInterface(); $template->filename = $template_file; return $template; } - return ""; + return EMPTY_STR; } - public function get_intltext_template($file, $root='') { + public function get_intltext_template($file, $root = '') { - $lang = \App::$language; - if ($root != '' && substr($root,-1) != '/' ) { - $root .= '/'; - } - foreach (Array( - $root."view/$lang/$file", - $root."view/en/$file", - '' - ) as $template_file) { - if (is_file($template_file)) { break; } - } - if ($template_file=='') {$template_file = theme_include($file,$root);} - if($template_file) { + $lang = App::$language; + if ($root != '' && substr($root,-1) != '/' ) { + $root .= '/'; + } + foreach ( [ $root . "view/$lang/$file", $root . "view/en/$file", '' ] as $template_file) { + if (is_file($template_file)) { + break; + } + } + if ($template_file == '') { + $template_file = theme_include($file,$root); + } + if ($template_file) { $template = new SmartyInterface(); $template->filename = $template_file; return $template; diff --git a/Zotlabs/Update/_1231.php b/Zotlabs/Update/_1231.php new file mode 100644 index 000000000..a685c5b28 --- /dev/null +++ b/Zotlabs/Update/_1231.php @@ -0,0 +1,73 @@ + $channel['channel_address'], + 'displayname' => $channel['channel_name'], + 'calendarid' => 'channel_calendar', + 'json_source' => '/channel_calendar/json', + 'color' => '#3a87ad', + 'editable' => true, + 'switch' => get_pconfig(local_channel(), 'cdav_calendar', 'channel_calendar') + ]; + $o .= replace_macros(get_markup_template('cdav_widget_calendar.tpl'), [ - '$my_calendars_label' => t('My Calendars'), + '$channel_calendars_label' => t('Channel Calendar'), + '$channel_calendars' => $channel_calendars, + '$my_calendars_label' => t('CalDAV Calendars'), '$my_calendars' => $my_calendars, - '$shared_calendars_label' => t('Shared Calendars'), + '$shared_calendars_label' => t('Shared CalDAV Calendars'), '$shared_calendars' => $shared_calendars, '$sharee_options' => $sharee_options, '$access_options' => $access_options, @@ -124,10 +136,11 @@ class Cdav { '$share' => t('Share'), '$edit_label' => t('Calendar name and color'), '$edit' => t('Edit'), - '$create_label' => t('Create new calendar'), + '$create_label' => t('Create new CalDAV calendar'), '$create' => t('Create'), '$create_placeholder' => t('Calendar Name'), '$tools_label' => t('Calendar Tools'), + '$tools_options_label' => [t('Channel Calendars'), t('CalDAV Calendars')], '$import_label' => t('Import calendar'), '$import_placeholder' => t('Select a calendar to import to'), '$upload' => t('Upload'), diff --git a/Zotlabs/Widget/Suggestions.php b/Zotlabs/Widget/Suggestions.php index 5fb3d3e8b..b4f384e9d 100644 --- a/Zotlabs/Widget/Suggestions.php +++ b/Zotlabs/Widget/Suggestions.php @@ -2,6 +2,8 @@ namespace Zotlabs\Widget; +use Zotlabs\Lib\Apps; + require_once('include/socgraph.php'); @@ -9,9 +11,9 @@ class Suggestions { function widget($arr) { - if((! local_channel()) || (! feature_enabled(local_channel(),'suggest'))) - return ''; + if((! local_channel()) || (! Apps::system_app_installed(local_channel(), 'Suggest Channels'))) + return EMPTY_STR; $r = suggestion_query(local_channel(),get_observer_hash(),0,20); diff --git a/Zotlabs/Zot6/HTTPSig.php b/Zotlabs/Zot6/HTTPSig.php index 72785b1e9..d3a09b858 100644 --- a/Zotlabs/Zot6/HTTPSig.php +++ b/Zotlabs/Zot6/HTTPSig.php @@ -5,6 +5,7 @@ namespace Zotlabs\Zot6; use Zotlabs\Lib\ActivityStreams; use Zotlabs\Lib\Webfinger; use Zotlabs\Web\HTTPHeaders; +use Zotlabs\Lib\Libzot; /** * @brief Implements HTTP Signatures per draft-cavage-http-signatures-10. @@ -324,7 +325,7 @@ class HTTPSig { if($l['rel'] === 'http://purl.org/zot/protocol/6.0' && array_key_exists('href',$l) && $l['href'] !== EMPTY_STR) { $z = \Zotlabs\Lib\Zotfinger::exec($l['href']); if($z) { - $i = Zotlabs\Lib\Libzot::import_xchan($z['data']); + $i = Libzot::import_xchan($z['data']); if($i['success']) { $key['portable_id'] = $i['hash']; diff --git a/Zotlabs/Zot6/Zot6Handler.php b/Zotlabs/Zot6/Zot6Handler.php index 8f8957037..37ce11980 100644 --- a/Zotlabs/Zot6/Zot6Handler.php +++ b/Zotlabs/Zot6/Zot6Handler.php @@ -151,8 +151,7 @@ class Zot6Handler implements IHandler { /* * fetch the requested conversation */ - /// @FIXME $sender_hash is undefined - $messages = zot_feed($c[0]['channel_id'],$sender_hash, [ 'message_id' => $data['message_id'], 'encoding' => 'activitystreams' ]); + $messages = zot_feed($c[0]['channel_id'], $sender, [ 'message_id' => $data['message_id'], 'encoding' => 'activitystreams' ]); return (($messages) ? : [] ); } diff --git a/app/caldav.apd b/app/calendar.apd similarity index 59% rename from app/caldav.apd rename to app/calendar.apd index 30b45b4bd..8de046e48 100644 --- a/app/caldav.apd +++ b/app/calendar.apd @@ -1,6 +1,6 @@ -version: 2 +version: 1 url: $baseurl/cdav/calendar, $baseurl/settings/calendar requires: local_channel -name: CalDAV +name: Calendar photo: icon:calendar -categories: Productivity, Personal +categories: Productivity, nav_featured_app diff --git a/app/events.apd b/app/events.apd deleted file mode 100644 index b69ee35ee..000000000 --- a/app/events.apd +++ /dev/null @@ -1,6 +0,0 @@ -version: 2 -url: $baseurl/events, $baseurl/settings/events -requires: local_channel -name: Events -photo: icon:calendar -categories: nav_featured_app, Productivity diff --git a/app/events.png b/app/events.png deleted file mode 100644 index a5670fd1e..000000000 Binary files a/app/events.png and /dev/null differ diff --git a/boot.php b/boot.php index 939fa6d01..1b13b335a 100755 --- a/boot.php +++ b/boot.php @@ -50,10 +50,10 @@ require_once('include/attach.php'); require_once('include/bbcode.php'); define ( 'PLATFORM_NAME', 'hubzilla' ); -define ( 'STD_VERSION', '4.1' ); +define ( 'STD_VERSION', '4.3' ); define ( 'ZOT_REVISION', '6.0a' ); -define ( 'DB_UPDATE_VERSION', 1230 ); +define ( 'DB_UPDATE_VERSION', 1234 ); define ( 'PROJECT_BASE', __DIR__ ); @@ -467,7 +467,7 @@ define ( 'NAMESPACE_YMEDIA', 'http://search.yahoo.com/mrss/' ); define ( 'ACTIVITYSTREAMS_JSONLD_REV', 'https://www.w3.org/ns/activitystreams' ); -define ( 'ZOT_APSCHEMA_REV', '/apschema/v1.3' ); +define ( 'ZOT_APSCHEMA_REV', '/apschema/v1.4' ); /** * activity stream defines */ @@ -1507,12 +1507,13 @@ function fix_system_urls($oldurl, $newurl) { dbesc($rv['xchan_hash']) ); - $y = q("update hubloc set hubloc_addr = '%s', hubloc_url = '%s', hubloc_url_sig = '%s', hubloc_host = '%s', hubloc_callback = '%s' where hubloc_hash = '%s' and hubloc_url = '%s'", + $y = q("update hubloc set hubloc_addr = '%s', hubloc_url = '%s', hubloc_id_url = '%s', hubloc_url_sig = '%s', hubloc_host = '%s', hubloc_callback = '%s' where hubloc_hash = '%s' and hubloc_url = '%s'", dbesc($channel_address . '@' . $rhs), dbesc($newurl), - dbesc(base64url_encode(rsa_sign($newurl,$c[0]['channel_prvkey']))), + dbesc(str_replace($oldurl,$newurl,$rv['hubloc_id_url'])), + dbesc(($rv['hubloc_network'] === 'zot6') ? \Zotlabs\Lib\Libzot::sign($newurl,$c[0]['channel_prvkey']) : base64url_encode(rsa_sign($newurl,$c[0]['channel_prvkey']))), dbesc($newhost), - dbesc($newurl . '/post'), + dbesc(($rv['hubloc_network'] === 'zot6') ? $newurl . '/zot' : $newurl . '/post'), dbesc($rv['xchan_hash']), dbesc($oldurl) ); diff --git a/composer.json b/composer.json index e6cefa241..b8fcd51b6 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,7 @@ "lukasreschke/id3parser": "^0.0.1", "smarty/smarty": "~3.1", "ramsey/uuid": "^3.8", - "twbs/bootstrap": "4.1.3", + "twbs/bootstrap": "^4.3.1", "blueimp/jquery-file-upload": "^9.25", "desandro/imagesloaded": "^4.1" }, @@ -49,7 +49,9 @@ "behat/mink-extension": "@stable", "behat/mink-goutte-driver": "@stable", "php-mock/php-mock-phpunit": "@stable", - "phpunit/dbunit": "@stable" + "phpunit/dbunit": "@stable", + "phpmd/phpmd": "^2.6", + "squizlabs/php_codesniffer": "*" }, "autoload" : { "psr-4" : { diff --git a/composer.lock b/composer.lock index c4fba0975..2520df134 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "fe5e71d7076eeddf1c174be4a5c052dd", + "content-hash": "f4dce457cd65f92a26d8197617f2f560", "packages": [ { "name": "blueimp/jquery-file-upload", - "version": "v9.28.0", + "version": "v9.30.0", "source": { "type": "git", "url": "https://github.com/vkhramtsov/jQuery-File-Upload.git", - "reference": "ff5accfe2e5c4a522777faa980a90cf86a636d1d" + "reference": "1fceec556879403e5c1ae32a7c448aa12b8c3558" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/vkhramtsov/jQuery-File-Upload/zipball/ff5accfe2e5c4a522777faa980a90cf86a636d1d", - "reference": "ff5accfe2e5c4a522777faa980a90cf86a636d1d", + "url": "https://api.github.com/repos/vkhramtsov/jQuery-File-Upload/zipball/1fceec556879403e5c1ae32a7c448aa12b8c3558", + "reference": "1fceec556879403e5c1ae32a7c448aa12b8c3558", "shasum": "" }, "type": "library", @@ -59,7 +59,7 @@ "upload", "widget" ], - "time": "2018-11-13T05:41:39+00:00" + "time": "2019-04-22T09:21:57+00:00" }, { "name": "bshaffer/oauth2-server-php", @@ -861,16 +861,16 @@ }, { "name": "sabre/vobject", - "version": "4.1.6", + "version": "4.2.0", "source": { "type": "git", "url": "https://github.com/sabre-io/vobject.git", - "reference": "122cacbdea2c6133ac04db86ec05854beef75adf" + "reference": "bd500019764e434ff65872d426f523e7882a0739" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sabre-io/vobject/zipball/122cacbdea2c6133ac04db86ec05854beef75adf", - "reference": "122cacbdea2c6133ac04db86ec05854beef75adf", + "url": "https://api.github.com/repos/sabre-io/vobject/zipball/bd500019764e434ff65872d426f523e7882a0739", + "reference": "bd500019764e434ff65872d426f523e7882a0739", "shasum": "" }, "require": { @@ -879,8 +879,7 @@ "sabre/xml": ">=1.5 <3.0" }, "require-dev": { - "phpunit/phpunit": "> 4.8.35, <6.0.0", - "sabre/cs": "^1.0.0" + "phpunit/phpunit": "> 4.8.35, <6.0.0" }, "suggest": { "hoa/bench": "If you would like to run the benchmark scripts" @@ -954,7 +953,7 @@ "xCal", "xCard" ], - "time": "2018-04-20T07:22:50+00:00" + "time": "2019-02-19T13:05:37+00:00" }, { "name": "sabre/xml", @@ -1207,16 +1206,16 @@ }, { "name": "twbs/bootstrap", - "version": "v4.1.3", + "version": "v4.3.1", "source": { "type": "git", "url": "https://github.com/twbs/bootstrap.git", - "reference": "3b558734382ce58b51e5fc676453bfd53bba9201" + "reference": "8fa0d3010112dca5dd6dd501173415856001ba8b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twbs/bootstrap/zipball/3b558734382ce58b51e5fc676453bfd53bba9201", - "reference": "3b558734382ce58b51e5fc676453bfd53bba9201", + "url": "https://api.github.com/repos/twbs/bootstrap/zipball/8fa0d3010112dca5dd6dd501173415856001ba8b", + "reference": "8fa0d3010112dca5dd6dd501173415856001ba8b", "shasum": "" }, "replace": { @@ -1254,7 +1253,7 @@ "sass", "web" ], - "time": "2018-07-24T15:54:34+00:00" + "time": "2019-02-13T16:01:40+00:00" } ], "packages-dev": [ @@ -2038,6 +2037,46 @@ ], "time": "2018-06-11T23:09:50+00:00" }, + { + "name": "pdepend/pdepend", + "version": "2.5.2", + "source": { + "type": "git", + "url": "https://github.com/pdepend/pdepend.git", + "reference": "9daf26d0368d4a12bed1cacae1a9f3a6f0adf239" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pdepend/pdepend/zipball/9daf26d0368d4a12bed1cacae1a9f3a6f0adf239", + "reference": "9daf26d0368d4a12bed1cacae1a9f3a6f0adf239", + "shasum": "" + }, + "require": { + "php": ">=5.3.7", + "symfony/config": "^2.3.0|^3|^4", + "symfony/dependency-injection": "^2.3.0|^3|^4", + "symfony/filesystem": "^2.3.0|^3|^4" + }, + "require-dev": { + "phpunit/phpunit": "^4.8|^5.7", + "squizlabs/php_codesniffer": "^2.0.0" + }, + "bin": [ + "src/bin/pdepend" + ], + "type": "library", + "autoload": { + "psr-4": { + "PDepend\\": "src/main/php/PDepend" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "description": "Official version of pdepend to be handled with Composer", + "time": "2017-12-13T13:21:38+00:00" + }, { "name": "phar-io/manifest", "version": "1.0.3", @@ -2460,6 +2499,72 @@ ], "time": "2017-07-14T14:27:02+00:00" }, + { + "name": "phpmd/phpmd", + "version": "2.6.0", + "source": { + "type": "git", + "url": "https://github.com/phpmd/phpmd.git", + "reference": "4e9924b2c157a3eb64395460fcf56b31badc8374" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpmd/phpmd/zipball/4e9924b2c157a3eb64395460fcf56b31badc8374", + "reference": "4e9924b2c157a3eb64395460fcf56b31badc8374", + "shasum": "" + }, + "require": { + "ext-xml": "*", + "pdepend/pdepend": "^2.5", + "php": ">=5.3.9" + }, + "require-dev": { + "phpunit/phpunit": "^4.0", + "squizlabs/php_codesniffer": "^2.0" + }, + "bin": [ + "src/bin/phpmd" + ], + "type": "project", + "autoload": { + "psr-0": { + "PHPMD\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Manuel Pichler", + "email": "github@manuel-pichler.de", + "homepage": "https://github.com/manuelpichler", + "role": "Project Founder" + }, + { + "name": "Other contributors", + "homepage": "https://github.com/phpmd/phpmd/graphs/contributors", + "role": "Contributors" + }, + { + "name": "Marc Würth", + "email": "ravage@bluewin.ch", + "homepage": "https://github.com/ravage84", + "role": "Project Maintainer" + } + ], + "description": "PHPMD is a spin-off project of PHP Depend and aims to be a PHP equivalent of the well known Java tool PMD.", + "homepage": "http://phpmd.org/", + "keywords": [ + "mess detection", + "mess detector", + "pdepend", + "phpmd", + "pmd" + ], + "time": "2017-01-20T14:41:10+00:00" + }, { "name": "phpspec/prophecy", "version": "1.8.0", @@ -3614,6 +3719,57 @@ "homepage": "https://github.com/sebastianbergmann/version", "time": "2016-10-03T07:35:21+00:00" }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.4.0", + "source": { + "type": "git", + "url": "https://github.com/squizlabs/PHP_CodeSniffer.git", + "reference": "379deb987e26c7cd103a7b387aea178baec96e48" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/squizlabs/PHP_CodeSniffer/zipball/379deb987e26c7cd103a7b387aea178baec96e48", + "reference": "379deb987e26c7cd103a7b387aea178baec96e48", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" + }, + "bin": [ + "bin/phpcs", + "bin/phpcbf" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "lead" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "http://www.squizlabs.com/php-codesniffer", + "keywords": [ + "phpcs", + "standards" + ], + "time": "2018-12-19T23:57:18+00:00" + }, { "name": "symfony/browser-kit", "version": "v4.2.1", diff --git a/doc/admin/administrator_guide.md b/doc/admin/administrator_guide.md index 5f1d40428..c884bb0bd 100644 --- a/doc/admin/administrator_guide.md +++ b/doc/admin/administrator_guide.md @@ -1,4 +1,3 @@ - ### Overview $Projectname is more than a simple web application. It is a @@ -17,48 +16,47 @@ such as XAMPP and WAMP are not officially supported at this time however we welcome patches if you manage to get it working. ### Where to find more help -If you encounter problems or have issues not addressed in this documentation, +If you encounter problems or have issues not addressed in this documentation, please let us know via the [Github issue tracker](https://framagit.org/hubzilla/core/issues). Please be as clear as you can about your operating environment and provide as much detail as possible about any error messages you may see, so that we can prevent it from happening in the future. Due to the large variety of operating systems and PHP platforms in existence we may have only limited ability to debug your PHP installation or -acquire any missing modules * but we will do our best to solve any general code +acquire any missing modules, but we will do our best to solve any general code issues. -### Before you begin +### Before you begin #### Choose a domain name or subdomain name for your server -$Projectname can only be installed into the root of a domain or sub-domain, and can +$Projectname can only be installed into the root of a domain or sub-domain, and can not be installed using alternate TCP ports. #### Decide if you will use SSL and obtain an SSL certificate before software installation -You SHOULD use SSL. If you use SSL, you MUST use a "browser-valid" certificate. +You SHOULD use SSL. If you use SSL, you MUST use a "browser-valid" certificate. *You MUST NOT use self-signed certificates!* -Please test your certificate prior to installation. A web tool for testing your -certificate is available at "http://www.digicert.com/help/". When visiting your -site for the first time, please use the SSL ("https://") URL if SSL is available. -This will avoid problems later. The installation routine will not allow you to +Please test your certificate prior to installation. A web tool for testing your +certificate is available at "http://www.digicert.com/help/". When visiting your +site for the first time, please use the SSL ("https://") URL if SSL is available. +This will avoid problems later. The installation routine will not allow you to use a non browser-valid certificate. - -This restriction is incorporated because public posts from you may contain +This restriction is incorporated because public posts from you may contain references to images on your own hub. Other members viewing their stream on other hubs will get warnings if your certificate is not trusted by their web browser. This will confuse many people because this is a decentralised network -and they will get the warning about your hub while viewing their own hub and may -think their own hub has an issue. These warnings are very technical and scary to +and they will get the warning about your hub while viewing their own hub and may +think their own hub has an issue. These warnings are very technical and scary to some folks, many of whom will not know how to proceed except to follow the browser advice. This is disruptive to the community. That said, we recognise the issues surrounding the current certificate infrastructure and agree there are many -problems, but that doesn't change the requirement. +problems, but that doesn't change the requirement. Free "browser-valid" certificates are available from providers such as StartSSL -and LetsEncrypt. +and LetsEncrypt. If you do NOT use SSL, there may be a delay of up to a minute for the initial install script - while we check the SSL port to see if anything responds there. @@ -66,11 +64,11 @@ When communicating with new sites, $Projectname always attempts connection on th SSL port first, before falling back to a less secure connection. If you do not use SSL, your webserver MUST NOT listen on port 443 at all. -If you use LetsEncrypt to provide certificates and create a file under -.well-known/acme-challenge so that LetsEncrypt can verify your domain ownership, -please remove or rename the .well-known directory as soon as the certificate is +If you use LetsEncrypt to provide certificates and create a file under +.well-known/acme-challenge so that LetsEncrypt can verify your domain ownership, +please remove or rename the .well-known directory as soon as the certificate is generated. $Projectname will provide its own handler for ".well-known" services when -it is installed, and an existing directory in this location may prevent some of +it is installed, and an existing directory in this location may prevent some of these services from working correctly. This should not be a problem with Apache, but may be an issue with nginx or other web server platforms. @@ -82,20 +80,20 @@ There are several ways to deploy a new hub. * Automated deployment using an OpenShift virtual private server (VPS) ### Requirements -* Apache with mod-rewrite enabled and "AllowOverride All" so you can use a +* Apache with mod-rewrite enabled and "AllowOverride All" so you can use a local .htaccess file. Some folks have successfully used nginx and lighttpd. Example config scripts are available for these platforms in doc/install. - Apache and nginx have the most support. + Apache and nginx have the most support. -* PHP 5.5 or later. - * Note that on some shared hosting environments, the _command line_ version of -PHP might differ from the _webserver_ version +* PHP 7.1 or later. + * Note that on some shared hosting environments, the _command line_ + version of PHP might differ from the _webserver_ version -* PHP *command line* access with register_argc_argv set to true in the - php.ini file * and with no hosting provider restrictions on the use of +* PHP *command line* access with register_argc_argv set to true in the + php.ini file * and with no hosting provider restrictions on the use of exec() and proc_open(). -* curl, gd (with at least jpeg and png support), mysqli, mbstring, mcrypt, +* curl, gd (with at least jpeg and png support), mysqli, mbstring, mcrypt, zip, and openssl extensions. The imagick extension is not required but desirable. * xml extension is required if you want webdav to work. @@ -106,7 +104,7 @@ PHP might differ from the _webserver_ version * ability to schedule jobs with cron. -* Installation into a top-level domain or sub-domain (without a +* Installation into a top-level domain or sub-domain (without a directory/path component in the URL) is REQUIRED. ### Manual Installation @@ -115,9 +113,9 @@ PHP might differ from the _webserver_ version If you copy the directory tree to your webserver, make sure that you include the hidden files like .htaccess. -If you are able to do so, we recommend using git to clone the source -repository rather than to use a packaged tar or zip file. This makes the -software much easier to update. The Linux command to clone the repository +If you are able to do so, we recommend using git to clone the source +repository rather than to use a packaged tar or zip file. This makes the +software much easier to update. The Linux command to clone the repository into a directory "mywebsite" would be: git clone https://framagit.org/hubzilla/core.git mywebsite @@ -126,7 +124,7 @@ and then you can pick up the latest changes at any time with: git pull -make sure folders ``store/[data]/smarty3`` and ``store`` exist and are +make sure folders ``store/[data]/smarty3`` and ``store`` exist and are writable by the webserver: mkdir -p "store/[data]/smarty3" @@ -150,7 +148,7 @@ web-based administrative tools to function: #### Official addons ##### Installation -Navigate to your website. Then you should clone the addon repository (separately). We'll give this repository a nickname of 'hzaddons'. You can pull in other hubzilla addon repositories by giving them different nicknames:: +Navigate to your website. Then you should clone the addon repository (separately). We'll give this repository a nickname of 'hzaddons'. You can pull in other hubzilla addon repositories by giving them different nicknames: cd mywebsite util/add_addon_repo https://framagit.org/hubzilla/addons.git hzaddons @@ -161,9 +159,9 @@ For keeping the addon tree updated, you should be on your top level website dire cd mywebsite util/update_addon_repo hzaddons -Create searchable representations of the online documentation. You may do this -any time that the documentation is updated : - +Create searchable representations of the online documentation. You may do this +any time that the documentation is updated : + cd mywebsite util/importdoc @@ -203,45 +201,28 @@ We recommend the following addons be installed on all public sites: Several web communities have begun to converge using common protocols. The protocols involved are somewhat limited in their abilities. The GNU-Social protocol for instance offers no privacy modes, and the Diaspora protocol is somewhat restrictive in what kinds of communications are allowed. All comments must be signed in a very unique manner by the original author. The ActivityPub protocol is also being considered and may be supported at a future date. No other existing protocol supports nomadic location as used by this project. This presents some support challenges as some features work with some networks and don't work with others. Nevertheless the federation protocols allow connections to be made to a much larger community of people worldwide. They are provided as addons. -> diaspora - The Diaspora Protocol used by Diaspora and Friendica. You should enable 'Diaspora Statistics' (statistics) first to enable all the available features. +* diaspora - The Diaspora Protocol used by Diaspora and Friendica. You should enable 'Diaspora Statistics' (statistics) first to enable all the available features. -> gnusoc - The GNU-Social Protocol, used by GNU-Social, Mastodon and several other communities. This addon requires you first install the 'pubsubhubbub' service (also an addon). +* gnusoc - The GNU-Social Protocol, used by GNU-Social, Mastodon and several other communities. This addon requires you first install the 'pubsubhubbub' service (also an addon). Each member of your site must choose whether or not to allow these protocols individually as they may conflict with several desirable core features and abilities of this software (such as channel migration and cloning). They do this from their 'Settings -> Feature/Addon Settings' page. The administrator may also set the following: util/config system.diaspora_allowed 1 util/config system.gnusoc_allowed 1 -and enable these protocols automatically for all newly created channels. - - - - - - -### Techlevels - -We've implemented several different mechanisms in order to reduce the apparent complexity and learning curve presented to new members. At the same time, we do not wish to limit any functionality for people who are able to grasp some slightly advanced technical technical features. The first mechanism was to move several features to an optional 'Features' page where they could be enabled at will; with the default interface kept somewhat lean. - -The problem we had now is that the number of features began to grow dramatically, and the Feature page is daunting in possibilities. There are also features present which probably should not be available to all members, but may be extremely useful to those with technical backgrounds. - -The techlevels seeeks to remedy this by grouping features within different levels of technical ability; starting at 0 (uncomfortable with technology), and up to 5 (Unix wizard or equivalent). - -When a new member registers, their account is provided a techlevel setting of 0. On the account settings page they may change this to any available level. A higher level opens more advanced features and possible interactions. - -The account administrator may also lock a particular level, lock a maximum level, or change/re-arrange the features available to any level. Those with the minimum level are typically not very exploratory and are unlikely to discover the advanced modes. This is by design. Those that look around and desire more interactions will find them. In the absence of administrator defaults they may choose any level. As they look at the features available to the level in question, it is generally expected that they will discover some features are beyond their comprehension and it is hoped they will back off to a level where the interface and features are comfortable to their skill level. +and enable these protocols automatically for all newly created channels. ### Service Classes Service classes allow you to set limits on system resources by limiting what individual -accounts can do, including file storage and top-level post limits. Define custom service -classes according to your needs in the `.htconfig.php` file. For example, create +accounts can do, including file storage and top-level post limits. Define custom service +classes according to your needs in the `.htconfig.php` file. For example, create a _standard_ and _premium_ class using the following lines: // Service classes - + App::$config['system']['default_service_class']='standard'; // this is the default service class that is attached to every new account - + // configuration for standard service class App::$config['service_class']['standard'] = array('photo_upload_limit'=>2097152, // total photo storage limit per channel (here 2MB) @@ -251,7 +232,7 @@ a _standard_ and _premium_ class using the following lines: 'total_channels' =>100, // number of channels the user can add, other users can still add this channel, even if the limit is reached 'attach_upload_limit' =>2097152, // total attachment storage limit per channel (here 2MB) 'chatters_inroom' =>20); - + // configuration for premium service class App::$config['service_class']['premium'] = array('photo_upload_limit'=>20000000000, // total photo storage limit per channel (here 20GB) @@ -262,7 +243,7 @@ a _standard_ and _premium_ class using the following lines: 'attach_upload_limit' =>20000000000, // total attachment storage limit per channel (here 20GB) 'chatters_inroom' =>100); -To apply a service class to an existing account, use the command line utility from the +To apply a service class to an existing account, use the command line utility from the web root: `util/service_class` @@ -298,11 +279,11 @@ set the account that owns channel 'blogchan' to service class 'firstclass' (with * access_tokens - maximum number of Guest Access Tokens per channel ### Theme management -#### Repo management example +#### Repo management example 1. Navigate to your hub web root ``` - root@hub:/root# cd /var/www + root@hub:/root# cd /var/www ``` 2. Add the theme repo and give it a name @@ -319,70 +300,136 @@ set the account that owns channel 'blogchan' to service class 'firstclass' (with #### Keywords -There is a "tag cloud" of keywords that can appear on the channel directory page. -If you wish to hide these keywords, which are drawn from the directory server, you +There is a "tag cloud" of keywords that can appear on the channel directory page. +If you wish to hide these keywords, which are drawn from the directory server, you can use the *config* tool: util/config system disable_directory_keywords 1 - -If your hub is in the standalone mode because you do not wish to connect to the -global grid, you may instead ensure the the _directory_server_ system option is + +If your hub is in the standalone mode because you do not wish to connect to the +global grid, you may instead ensure the the _directory_server_ system option is empty: util/config system directory_server "" - ### Administration #### Site Administration -Administration of the website is commonly done through the admin webpage located at /admin on your website. In order to access this page you must have administration rights to the server. Administration rights are granted to the first account to register on your site, **provided** the email address of that account exactly matches the email address you provided as the administrator's email address during setup. +Administration of the website is commonly done through the admin webpage located at /admin on your website. In order to access this page you must have administration rights to the server. Administration rights are granted to the first account to register on your site, **provided** the email address of that account exactly matches the email address you provided as the administrator's email address during setup. -There are several ways that this can fail and leave the system without an administrator account, for instance if the first account that was created provided a different email address than the administrator email address that was supplied during setup. +There are several ways that this can fail and leave the system without an administrator account, for instance if the first account that was created provided a different email address than the administrator email address that was supplied during setup. For security reasons there is no web page or interface on the system which will give you administrator access. If you need to correct a situation where a system has no administrator account it **must** be done by editing the account table in the database. There is no other way. To do this, you will need to locate the entry in the account table which belongs to the desired administrator, and set 'account_roles' for that entry to 4096. You will then be able to access the admin page from your system's profile menu or directly via /admin . -A hub can have multiple admins and there is no limit to how administrators you can have. Repeat the above process for every account you wish to provide with administration rights. - +A hub can have multiple admins and there is no limit to how administrators you can have. Repeat the above process for every account you wish to provide with administration rights. ### Troubleshooting #### Log files -The system logfile is an extremely useful resource for tracking down things that go wrong. This can be enabled in the admin/log configuration page. A loglevel setting of LOGGER_DEBUG is preferred for stable production sites. Most things that go wrong with communications or storage are listed here. A setting of LOGGER_DATA provides [b]much[/b] more detail, but may fill your disk. In either case we recommend the use of logrotate on your operating system to cycle logs and discard older entries. +The system logfile is an extremely useful resource for tracking down +things that go wrong. This can be enabled in the admin/log +configuration page. A loglevel setting of `LOGGER_DEBUG` is preferred +for stable production sites. Most things that go wrong with +communications or storage are listed here. A setting of LOGGER_DATA +provides *much* more detail, but may fill your disk. In either +case we recommend the use of logrotate on your operating system to +cycle logs and discard older entries. -At the bottom of your .htconfig.php file are several lines (commented out) which enable PHP error logging. This reports issues with code syntax and executing the code and is the first place you should look for issues which result in a "white screen" or blank page. This is typically the result of code/syntax problems. -Database errors are reported to the system logfile, but we've found it useful to have a file in your top-level directory called dbfail.out which [b]only[/b] collects database related issues. If the file exists and is writable, database errors will be logged to it as well as to the system logfile. +At the bottom of your .htconfig.php file are several lines (commented +out) which enable PHP error logging. This reports issues with code +syntax and executing the code and is the first place you should look +for issues which result in a "white screen" or blank page. This is +typically the result of code/syntax problems. Database errors are +reported to the system logfile, but we've found it useful to have a +file in your top-level directory called dbfail.out which *only* +collects database related issues. If the file exists and is writable, +database errors will be logged to it as well as to the system logfile. -In the case of "500" errors, the issues may often be logged in your webserver logs, often /var/log/apache2/error.log or something similar. Consult your operating system documentation. +In the case of "500" errors, the issues may often be logged in your +webserver logs, often /var/log/apache2/error.log or something +similar. Consult your operating system documentation. There are three different log facilities. -**The first is the database failure log**. This is only used if you create a file called specifically 'dbfail.out' in the root folder of your website and make it write-able by the web server. If we have any database failed queries, they are all reported here. They generally indicate typos in our queries, but also occur if the database server disconnects or tables get corrupted. On rare occasions we'll see race conditions in here where two processes tried to create an xchan or cache entry with the same ID. Any other errors (especially persistent errors) should be investigated. +**The first is the database failure log**. This is only used if you + create a file called specifically `dbfail.out` in the root folder of + your website and make it write-able by the web server. If we have + any database failed queries, they are all reported here. They + generally indicate typos in our queries, but also occur if the + database server disconnects or tables get corrupted. On rare + occasions we'll see race conditions in here where two processes + tried to create an xchan or cache entry with the same ID. Any other + errors (especially persistent errors) should be investigated. -**The second is the PHP error log**. This is created by the language processor and only reports issues in the language environment. Again these can be syntax errors or programming errors, but these generally are fatal and result in a "white screen of death"; e.g. PHP terminates. You should probably look at this file if something goes wrong that doesn't result in a white screen of death, but it isn't uncommon for this file to be empty for days on end. +**The second is the PHP error log**. This is created by the language + processor and only reports issues in the language environment. Again + these can be syntax errors or programming errors, but these + generally are fatal and result in a "white screen of death"; + e.g. PHP terminates. You should probably look at this file if + something goes wrong that doesn't result in a white screen of death, + but it isn't uncommon for this file to be empty for days on end. -There are some lines at the bottom of the supplied .htconfig.php file; which if uncommented will enable a PHP error log (*extremely* useful for finding the source of white screen failures). This isn't done by default due to potential issues with logfile ownership and write permissions and the fact that there is no logfile rotation by default. - + There are some lines at the bottom of the supplied `.htconfig.php` + file; which if uncommented will enable a PHP error log (*extremely* + useful for finding the source of white screen failures). This isn't + done by default due to potential issues with logfile ownership and + write permissions and the fact that there is no logfile rotation by + default. -**The third is the "application log"**. This is used by $Projectname to report what is going on in the program and usually reports any difficulties or unexpected data we received. It also occasionally reports "heartbeat" status messages to indicate that we reached a certain point in a script. **This** is the most important log file to us, as we create it ourself for the sole purpose of reporting the status of background tasks and anything that seems weird or out of place. It may not be fatal, but maybe just unexpected. If you're performing a task and there's a problem, let us know what is in this file when the problem occurred. (Please don't send me 100M dumps you'll only piss me off). Just a few relevant lines so I can rule out a few hundred thousand lines of code and concentrate on where the problem starts showing up. +**The third is the "application log"**. This is used by $Projectname + to report what is going on in the program and usually reports any + difficulties or unexpected data we received. It also occasionally + reports "heartbeat" status messages to indicate that we reached a + certain point in a script. **This** is the most important log file + to us, as we create it ourself for the sole purpose of reporting the + status of background tasks and anything that seems weird or out of + place. It may not be fatal, but maybe just unexpected. If you're + performing a task and there's a problem, let us know what is in this + file when the problem occurred. (Please don't send me 100M dumps + you'll only piss me off). Just a few relevant lines so I can rule + out a few hundred thousand lines of code and concentrate on where + the problem starts showing up. -These are your site logs, not mine. We report serious issues at any log level. I highly recommend 'DEBUG' log level for most sites - which provides a bit of additional info and doesn't create huge logfiles. When there's a problem which defies all attempts to track, you might wish to use DATA log level for a short period of time to capture all the detail of what structures we were dealing with at the time. This log level will use a lot of space so is recommended only for brief periods or for developer test sites. +These are your site logs, not mine. We report serious issues at any +log level. I highly recommend `DEBUG` log level for most sites - which +provides a bit of additional info and doesn't create huge +logfiles. When there's a problem which defies all attempts to track, +you might wish to use `DATA` log level for a short period of time to +capture all the detail of what structures we were dealing with at the +time. This log level will use a lot of space so is recommended only +for brief periods or for developer test sites. -I recommend configuring logrotate for both the php log and the application log. I usually have a look at dbfail.out every week or two, fix any issues reported and then starting over with a fresh file. Likewise with the PHP logfile. I refer to it once in a while to see if there's something that needs fixing. +I recommend configuring logrotate for both the php log and the +application log. I usually have a look at dbfail.out every week or +two, fix any issues reported and then starting over with a fresh +file. Likewise with the PHP logfile. I refer to it once in a while to +see if there's something that needs fixing. + +If something goes wrong, and it's not a fatal error, I look at the +application logfile. Often I will -If something goes wrong, and it's not a fatal error, I look at the application logfile. Often I will ``` -tail -f logfile.out +tail -f logfile.out ``` -While repeating an operation that has problems. Often I'll insert extra logging statements in the code if there isn't any hint what's going wrong. Even something as simple as "got here" or printing out the value of a variable that might be suspect. You can do this too - in fact I encourage you to do so. Once you've found what you need to find, you can +While repeating an operation that has problems. Often I'll insert +extra logging statements in the code if there isn't any hint what's +going wrong. Even something as simple as "got here" or printing out +the value of a variable that might be suspect. You can do this too - +in fact I encourage you to do so. Once you've found what you need to +find, you can ``` git checkout file.php ``` -To immediately clear out all the extra logging stuff you added. Use the information from this log and any detail you can provide from your investigation of the problem to file your bug report - unless your analysis points to the source of the problem. In that case, just fix it. +To immediately clear out all the extra logging stuff you added. Use +the information from this log and any detail you can provide from your +investigation of the problem to file your bug report - unless your +analysis points to the source of the problem. In that case, just fix +it. ##### Rotating log files @@ -390,13 +437,25 @@ To immediately clear out all the extra logging stuff you added. Use the informa 1. Create a directory in your web root called `log` with webserver write permissions 1. Go to the **logrot** admin settings and enter this folder name as well as the max size and number of retained log files. - #### Reporting issues -When reporting issues, please try to provide as much detail as may be necessary for developers to reproduce the issue and provide the complete text of all error messages. - -We encourage you to try to the best of your abilities to use these logs combined with the source code in your possession to troubleshoot issues and find their cause. The community is often able to help, but only you have access to your site logfiles and it is considered a security risk to share them. - -If a code issue has been uncovered, please report it on the project bugtracker (https://framagit.org/hubzilla/core/issues). Again provide as much detail as possible to avoid us going back and forth asking questions about your configuration or how to duplicate the problem, so that we can get right to the problem and figure out what to do about it. You are also welcome to offer your own solutions and submit patches. In fact we encourage this as we are all volunteers and have little spare time available. The more people that help, the easier the workload for everybody. It's OK if your solution isn't perfect. Every little bit helps and perhaps we can improve on it. +When reporting issues, please try to provide as much detail as may be +necessary for developers to reproduce the issue and provide the +complete text of all error messages. +We encourage you to try to the best of your abilities to use these +logs combined with the source code in your possession to troubleshoot +issues and find their cause. The community is often able to help, but +only you have access to your site logfiles and it is considered a +security risk to share them. +If a code issue has been uncovered, please report it on the project +bugtracker (https://framagit.org/hubzilla/core/issues). Again provide +as much detail as possible to avoid us going back and forth asking +questions about your configuration or how to duplicate the problem, so +that we can get right to the problem and figure out what to do about +it. You are also welcome to offer your own solutions and submit +patches. In fact we encourage this as we are all volunteers and have +little spare time available. The more people that help, the easier the +workload for everybody. It's OK if your solution isn't perfect. Every +little bit helps and perhaps we can improve on it. diff --git a/doc/bugs.bb b/doc/bugs.bb index f773da025..3bf2a0e29 100644 --- a/doc/bugs.bb +++ b/doc/bugs.bb @@ -21,7 +21,7 @@ If you get a blank white screen when doing something, this is almost always a co [h3]I'm stumped. I can't figure out what is wrong.[/h3] -At this point it might be worthwhile discussing the issue on one of the online forums. There may be several of these and some may be more suited to your spoken language. At this time, the 'Hubzilla Support' channel (support@gravizot.de) is the recommended forum for discussing bugs. +At this point it might be worthwhile discussing the issue on one of the online forums. There may be several of these and some may be more suited to your spoken language. At this time, the 'Hubzilla Support' channel (support@zotadel.net) is the recommended forum for discussing bugs. If community members with software engineering training/expertise can't help you right away, understand that they are volunteers and may have a lot of other work and demands on their time. At this point you need to file a bug report. You will need an account on framagit.org to do this. So register, and then visit https://framagit.org/hubzilla/core/issues . Create an issue here and provide all the same information that you provided online. Don't leave out anything. @@ -29,4 +29,4 @@ Then you wait. If it's a high profile issue, it may get fixed quickly. But nobod Other people working to fix the problem may need to find out more, so do your homework and document what is happening and everything you've tried. Don't say "I did xyz and it didn't work." That doesn't tell us anything. Tell us precisely what steps you took and what you expected the result to be, and precisely what happened as a result. What page/URL were you looking at or what form were you filling in? If there were any error messages, don't say "there was an error message". Tell us exactly what the message said. Also tell us what hub you are using, what software version you're running and any other details that may be unique about your site configuration. It is understood that you might wish to keep some information and your connections private, however if you aren't willing to share the information other people need to reproduce/fix the problem, it may not get fixed. - \ No newline at end of file + diff --git a/doc/hidden_configs.bb b/doc/hidden_configs.bb index 8b5429deb..dc3906df1 100644 --- a/doc/hidden_configs.bb +++ b/doc/hidden_configs.bb @@ -51,6 +51,7 @@ Options are: [*= system.email_notify_icon_url ] URL of image (32x32) to display in email notifications (HTML bodies). [*= system.expire_delivery_reports ] Expiration in days for delivery reports - default 10 [*= system.expire_limit ] Don't expire any more than this number of posts per channel per expiration run to keep from exhausting memory. Default 5000. + [*= system.filesystem_storage_thumbnails ] If '1', use filesystem instead SQL database to store thumbnails. Default is '0'. Introduced in 4.2 [*= system.hidden_version_siteinfo ] If true, do not report the software version on siteinfo pages (system.hide_version also hides the version on these pages, this setting *only* hides the version on siteinfo pages). [*= system.hide_help ] Don't display help documentation link in nav bar [*= system.hide_in_statistics ] Tell the red statistics servers to completely hide this hub in hub lists. @@ -79,7 +80,7 @@ Options are: [*= system.startpage ] Set the default page to be taken to after a login for all channels at this website. Can be overwritten by user settings. [*= system.sys_expire_days ] How many days to keep discovered public content from other sites [*= system.taganyone ] Allow the @mention tagging of anyone whether you are connected or not. - [*= system.tempdir ] Place to store temporary files (currently unused), default is defined in the PHP configuration + [*= system.tempdir ] Place to store temporary files (currently unused), default is defined in the PHP configuration. [*= system.tos_url ] Set an alternative link for the ToS location. [*= system.transport_security_header ] if non-zero and SSL is being used, include a strict-transport-security header on webpages [*= system.uploaddir ] Location to upload files (default is system.tempdir, currently used only by js_upload plugin) diff --git a/include/account.php b/include/account.php index 2ab99ce19..5f0c8737f 100644 --- a/include/account.php +++ b/include/account.php @@ -142,12 +142,10 @@ function create_account($arr) { $invite_code = ((x($arr,'invite_code')) ? notags(trim($arr['invite_code'])) : ''); $email = ((x($arr,'email')) ? notags(punify(trim($arr['email']))) : ''); $password = ((x($arr,'password')) ? trim($arr['password']) : ''); - $password2 = ((x($arr,'password2')) ? trim($arr['password2']) : ''); $parent = ((x($arr,'parent')) ? intval($arr['parent']) : 0 ); $flags = ((x($arr,'account_flags')) ? intval($arr['account_flags']) : ACCOUNT_OK); $roles = ((x($arr,'account_roles')) ? intval($arr['account_roles']) : 0 ); $expires = ((x($arr,'expires')) ? intval($arr['expires']) : NULL_DATE); - $techlevel = ((array_key_exists('techlevel',$arr)) ? intval($arr['techlevel']) : intval(get_config('system','techlevel'))); $default_service_class = get_config('system','default_service_class'); @@ -264,9 +262,8 @@ function create_account($arr) { function verify_email_address($arr) { if(array_key_exists('resend',$arr)) { - $email = $arr['email']; $a = q("select * from account where account_email = '%s' limit 1", - dbesc($arr['email']) + dbesc($arr['email']) ); if(! ($a && ($a[0]['account_flags'] & ACCOUNT_UNVERIFIED))) { return false; @@ -285,7 +282,7 @@ function verify_email_address($arr) { else { $hash = random_string(24); - $r = q("INSERT INTO register ( hash, created, uid, password, lang ) VALUES ( '%s', '%s', %d, '%s', '%s' ) ", + q("INSERT INTO register ( hash, created, uid, password, lang ) VALUES ( '%s', '%s', %d, '%s', '%s' ) ", dbesc($hash), dbesc(datetime_convert()), intval($arr['account']['account_id']), @@ -304,7 +301,7 @@ function verify_email_address($arr) { '$email' => $arr['email'], '$uid' => $account['account_id'], '$hash' => $hash, - '$details' => $details + '$details' => '' ] ); @@ -318,9 +315,7 @@ function verify_email_address($arr) { pop_lang(); - if($res) - $delivered ++; - else + if(! $res) logger('send_reg_approval_email: failed to account_id: ' . $arr['account']['account_id']); return $res; @@ -442,16 +437,17 @@ function account_allow($hash) { if(! $account) return $ret; - $r = q("DELETE FROM register WHERE hash = '%s'", + q("DELETE FROM register WHERE hash = '%s'", dbesc($register[0]['hash']) ); - $r = q("update account set account_flags = (account_flags & ~%d) where (account_flags & %d)>0 and account_id = %d", + q("update account set account_flags = (account_flags & ~%d) where (account_flags & %d)>0 and account_id = %d", intval(ACCOUNT_BLOCKED), intval(ACCOUNT_BLOCKED), intval($register[0]['uid']) ); - $r = q("update account set account_flags = (account_flags & ~%d) where (account_flags & %d)>0 and account_id = %d", + + q("update account set account_flags = (account_flags & ~%d) where (account_flags & %d)>0 and account_id = %d", intval(ACCOUNT_PENDING), intval(ACCOUNT_PENDING), intval($register[0]['uid']) @@ -516,11 +512,11 @@ function account_deny($hash) { if(! $account) return false; - $r = q("DELETE FROM account WHERE account_id = %d", + q("DELETE FROM account WHERE account_id = %d", intval($register[0]['uid']) ); - $r = q("DELETE FROM register WHERE id = %d", + q("DELETE FROM register WHERE id = %d", dbesc($register[0]['id']) ); notice( sprintf(t('Registration revoked for %s'), $account[0]['account_email']) . EOL); @@ -551,21 +547,23 @@ function account_approve($hash) { if(! $account) return $ret; - $r = q("DELETE FROM register WHERE hash = '%s' and password = 'verify'", + q("DELETE FROM register WHERE hash = '%s' and password = 'verify'", dbesc($register[0]['hash']) ); - $r = q("update account set account_flags = (account_flags & ~%d) where (account_flags & %d)>0 and account_id = %d", + q("update account set account_flags = (account_flags & ~%d) where (account_flags & %d)>0 and account_id = %d", intval(ACCOUNT_BLOCKED), intval(ACCOUNT_BLOCKED), intval($register[0]['uid']) ); - $r = q("update account set account_flags = (account_flags & ~%d) where (account_flags & %d)>0 and account_id = %d", + + q("update account set account_flags = (account_flags & ~%d) where (account_flags & %d)>0 and account_id = %d", intval(ACCOUNT_PENDING), intval(ACCOUNT_PENDING), intval($register[0]['uid']) ); - $r = q("update account set account_flags = (account_flags & ~%d) where (account_flags & %d)>0 and account_id = %d", + + q("update account set account_flags = (account_flags & ~%d) where (account_flags & %d)>0 and account_id = %d", intval(ACCOUNT_UNVERIFIED), intval(ACCOUNT_UNVERIFIED), intval($register[0]['uid']) @@ -620,7 +618,7 @@ function downgrade_accounts() { foreach($r as $rr) { if(($basic) && ($rr['account_service_class']) && ($rr['account_service_class'] != $basic)) { - $x = q("UPDATE account set account_service_class = '%s', account_expires = '%s' + q("UPDATE account set account_service_class = '%s', account_expires = '%s' where account_id = %d", dbesc($basic), dbesc(NULL_DATE), @@ -631,7 +629,7 @@ function downgrade_accounts() { logger('downgrade_accounts: Account id ' . $rr['account_id'] . ' downgraded.'); } else { - $x = q("UPDATE account SET account_flags = (account_flags | %d) where account_id = %d", + q("UPDATE account SET account_flags = (account_flags | %d) where account_id = %d", intval(ACCOUNT_EXPIRED), intval($rr['account_id']) ); diff --git a/include/acl_selectors.php b/include/acl_selectors.php index c7a87afee..35e385058 100644 --- a/include/acl_selectors.php +++ b/include/acl_selectors.php @@ -84,7 +84,7 @@ function populate_acl($defaults = null,$show_jotnets = true, $emptyACL_descripti ); if($r) { foreach($r as $rv) { - $selected = (($single_group && 'vp.' . $rr['hash'] === $allow_gid[0]) ? ' selected = "selected" ' : ''); + $selected = (($single_group && 'vp.' . $rv['profile_guid'] === $allow_gid[0]) ? ' selected = "selected" ' : ''); $groups .= '' . "\r\n"; } } diff --git a/include/api_zot.php b/include/api_zot.php index 6a5b9a268..b332aea71 100644 --- a/include/api_zot.php +++ b/include/api_zot.php @@ -6,6 +6,8 @@ api_register_func('api/export/basic','api_export_basic', true); api_register_func('api/red/channel/export/basic','api_export_basic', true); api_register_func('api/z/1.0/channel/export/basic','api_export_basic', true); + api_register_func('api/red/item/export/page','api_item_export_page', true); + api_register_func('api/z/1.0/item/export/page','api_item_export_page', true); api_register_func('api/red/channel/list','api_channel_list', true); api_register_func('api/z/1.0/channel/list','api_channel_list', true); api_register_func('api/red/channel/stream','api_channel_stream', true); @@ -80,6 +82,26 @@ json_return_and_die(identity_basic_export(api_user(),$sections)); } + function api_item_export_page($type) { + if(api_user() === false) { + logger('api_item_export_page: no user'); + return false; + } + $page = intval($_REQUEST['page']); + $records = intval($_REQUEST['records']); + if(! $records) { + $records = 50; + } + if(! $_REQUEST['since']) + $start = NULL_DATE; + else { + $start = datetime_convert(date_default_timezone_get(),'UTC', $_REQUEST['since']); + } + $finish = datetime_convert(date_default_timezone_get(),'UTC', (($_REQUEST['until']) ? $_REQUEST['until'] : 'now')); + + json_return_and_die(channel_export_items_page(api_user(),$start,$finish,$page,$records)); + } + function api_network_stream($type) { if(api_user() === false) { diff --git a/include/attach.php b/include/attach.php index 17a47d9ac..f169e0669 100644 --- a/include/attach.php +++ b/include/attach.php @@ -321,7 +321,6 @@ function attach_can_view_folder($uid,$ob_hash,$folder_hash) { $sql_extra = permissions_sql($uid,$ob_hash); $hash = $folder_hash; - $result = false; if(! $folder_hash) { return perm_is_allowed($uid,$ob_hash,'view_storage'); @@ -352,7 +351,7 @@ function attach_can_view_folder($uid,$ob_hash,$folder_hash) { * @param string $hash * @param string $observer_hash * @param int $rev (optional) revision default 0 - * @return associative array with everything except data + * @return array (associative) with everything except data * * \e boolean \b success boolean true or false * * \e string \b message (optional) only when success is false * * \e array \b data array of attach DB entry without data component @@ -1224,7 +1223,7 @@ function attach_mkdir($channel, $observer_hash, $arr = null) { $ret['success'] = true; // update the parent folder's lastmodified timestamp - $e = q("UPDATE attach SET edited = '%s' WHERE hash = '%s' AND uid = %d", + q("UPDATE attach SET edited = '%s' WHERE hash = '%s' AND uid = %d", dbesc($created), dbesc($arr['folder']), intval($channel_id) @@ -1270,8 +1269,6 @@ function attach_mkdirp($channel, $observer_hash, $arr = null) { $ret = array('success' => false); $channel_id = $channel['channel_id']; - $sql_options = ''; - $basepath = 'store/' . $channel['channel_address']; logger('basepath: ' . $basepath); @@ -1374,7 +1371,7 @@ function attach_change_permissions($channel_id, $resource, $allow_cid, $allow_gi } } - $x = q("update attach set allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s' where hash = '%s' and uid = %d", + q("update attach set allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s' where hash = '%s' and uid = %d", dbesc($allow_cid), dbesc($allow_gid), dbesc($deny_cid), @@ -1383,7 +1380,7 @@ function attach_change_permissions($channel_id, $resource, $allow_cid, $allow_gi intval($channel_id) ); if($r[0]['is_photo']) { - $x = q("update photo set allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s' where resource_id = '%s' and uid = %d", + q("update photo set allow_cid = '%s', allow_gid = '%s', deny_cid = '%s', deny_gid = '%s' where resource_id = '%s' and uid = %d", dbesc($allow_cid), dbesc($allow_gid), dbesc($deny_cid), @@ -1482,7 +1479,7 @@ function attach_delete($channel_id, $resource, $is_photo = 0) { } // delete from database - $z = q("DELETE FROM attach WHERE hash = '%s' AND uid = %d", + q("DELETE FROM attach WHERE hash = '%s' AND uid = %d", dbesc($resource), intval($channel_id) ); @@ -1493,7 +1490,7 @@ function attach_delete($channel_id, $resource, $is_photo = 0) { // update the parent folder's lastmodified timestamp - $e = q("UPDATE attach SET edited = '%s' WHERE hash = '%s' AND uid = %d", + q("UPDATE attach SET edited = '%s' WHERE hash = '%s' AND uid = %d", dbesc(datetime_convert()), dbesc($r[0]['folder']), intval($channel_id) @@ -1524,6 +1521,17 @@ function attach_drop_photo($channel_id,$resource) { if($x) { drop_item($x[0]['id'],false,(($x[0]['item_hidden']) ? DROPITEM_NORMAL : DROPITEM_PHASE1),true); } + + $r = q("SELECT content FROM photo WHERE resource_id = '%s' AND uid = %d AND os_storage = 1", + dbesc($resource), + intval($channel_id) + ); + if($r) { + foreach($r as $i) { + @unlink(dbunescbin($i['content'])); + } + } + q("DELETE FROM photo WHERE uid = %d AND resource_id = '%s'", intval($channel_id), dbesc($resource) @@ -1815,7 +1823,7 @@ function file_activity($channel_id, $object, $allow_cid, $allow_gid, $deny_cid, $u_jsonobject = json_encode($object); //we have got the relevant info - delete the old item before we create the new one - $z = q("DELETE FROM item WHERE obj_type = '%s' AND verb = '%s' AND mid = '%s'", + q("DELETE FROM item WHERE obj_type = '%s' AND verb = '%s' AND mid = '%s'", dbesc(ACTIVITY_OBJ_FILE), dbesc(ACTIVITY_POST), dbesc($y[0]['mid']) @@ -1946,7 +1954,6 @@ function attach_recursive_perms($arr_allow_cid, $arr_allow_gid, $arr_deny_cid, $ $ret = array(); $parent_arr = array(); $count_values = array(); - $poster = App::get_observer(); //lookup all channels in sharee group and add them to sharee $arr_allow_cid if($arr_allow_gid) { @@ -2351,7 +2358,6 @@ function attach_move($channel_id, $resource_id, $new_folder_hash) { if(! $n) return false; - $newdirname = $n[0]['filename']; $newalbumname = $n[0]['display_path']; $newstorepath = dbunescbin($n[0]['content']) . '/' . $resource_id; } @@ -2359,7 +2365,6 @@ function attach_move($channel_id, $resource_id, $new_folder_hash) { // root directory - $newdirname = EMPTY_STR; $newalbumname = EMPTY_STR; $newstorepath = 'store/' . $c['channel_address'] . '/' . $resource_id; } @@ -2428,7 +2433,7 @@ function attach_move($channel_id, $resource_id, $new_folder_hash) { } } - $t = q("update attach set content = '%s', folder = '%s', filename = '%s' where id = %d", + q("update attach set content = '%s', folder = '%s', filename = '%s' where id = %d", dbescbin($newstorepath), dbesc($new_folder_hash), dbesc($filename), @@ -2438,7 +2443,7 @@ function attach_move($channel_id, $resource_id, $new_folder_hash) { $x = attach_syspaths($channel_id,$resource_id); - $t1 = q("update attach set os_path = '%s', display_path = '%s' where id = %d", + q("update attach set os_path = '%s', display_path = '%s' where id = %d", dbesc($x['os_path']), dbesc($x['path']), intval($r[0]['id']) @@ -2446,7 +2451,7 @@ function attach_move($channel_id, $resource_id, $new_folder_hash) { if($r[0]['is_photo']) { - $t = q("update photo set album = '%s', filename = '%s', os_path = '%s', display_path = '%s' + q("update photo set album = '%s', filename = '%s', os_path = '%s', display_path = '%s' where resource_id = '%s' and uid = %d", dbesc($newalbumname), dbesc($filename), @@ -2456,7 +2461,7 @@ function attach_move($channel_id, $resource_id, $new_folder_hash) { intval($channel_id) ); - $t = q("update photo set content = '%s' where resource_id = '%s' and uid = %d and imgscale = 0", + q("update photo set content = '%s' where resource_id = '%s' and uid = %d and imgscale = 0", dbescbin($newstorepath), dbesc($resource_id), intval($channel_id) @@ -2587,12 +2592,12 @@ function attach_upgrade() { foreach($r as $rv) { $x = attach_syspaths($rv['uid'],$rv['hash']); if($x) { - $w = q("update attach set os_path = '%s', display_path = '%s' where id = %d", + q("update attach set os_path = '%s', display_path = '%s' where id = %d", dbesc($x['os_path']), dbesc($x['path']), intval($rv['id']) ); - $y = q("update photo set os_path = '%s', display_path = '%s' where uid = %d and resource_id = '%s'", + q("update photo set os_path = '%s', display_path = '%s' where uid = %d and resource_id = '%s'", dbesc($x['os_path']), dbesc($x['path']), intval($rv['uid']), diff --git a/include/bbcode.php b/include/bbcode.php index 7531bd774..485a1f5b2 100644 --- a/include/bbcode.php +++ b/include/bbcode.php @@ -87,12 +87,11 @@ function nakedoembed($match) { $strip_url = strip_escaped_zids($url); - $o = oembed_fetch_url($strip_url); - - if ($o['type'] == 'error') - return str_replace($url,$strip_url,$match[0]); - - return '[embed]' . $strip_url . '[/embed]'; + // this function no longer performs oembed on naked links + // because they author may have created naked links intentionally. + // Now it just strips zids on naked links. + + return str_replace($url,$strip_url,$match[0]); } function tryzrlaudio($match) { @@ -117,6 +116,26 @@ function tryzrlvideo($match) { return ''; } +function videowithopts($match) { + $link = $match[2]; + $zrl = is_matrix_url($link); + if($zrl) + $link = zid($link); + + $attributes = $match[1]; + + $poster = ""; + + preg_match("/poster='(.*?)'/ism", $attributes, $matches); + if ($matches[1] != "") + $poster = 'poster="' . (($zrl) ? zid($matches[1]) : $matches[1]) . '"'; + + return ''; +} + + + + // [noparse][i]italic[/i][/noparse] turns into // [noparse][ i ]italic[ /i ][/noparse], // to hide them from parser. @@ -966,17 +985,22 @@ function bbcode($Text, $options = []) { // leave open the posibility of [map=something] // this is replaced in prepare_body() which has knowledge of the item location - - if (strpos($Text,'[/map]') !== false) { - $Text = preg_replace_callback("/\[map\](.*?)\[\/map\]/ism", 'bb_map_location', $Text); + if ($cache) { + $Text = str_replace([ '[map]','[/map]' ], [ '','' ], $Text); + $Text = preg_replace('/\[map=(.*?)\]/ism','$1',$Text); } - if (strpos($Text,'[map=') !== false) { - $Text = preg_replace_callback("/\[map=(.*?)\]/ism", 'bb_map_coords', $Text); + else { + if (strpos($Text,'[/map]') !== false) { + $Text = preg_replace_callback("/\[map\](.*?)\[\/map\]/ism", 'bb_map_location', $Text); + } + if (strpos($Text,'[map=') !== false) { + $Text = preg_replace_callback("/\[map=(.*?)\]/ism", 'bb_map_coords', $Text); + } + if (strpos($Text,'[map]') !== false) { + $Text = preg_replace("/\[map\]/", '
', $Text); + } } - if (strpos($Text,'[map]') !== false) { - $Text = preg_replace("/\[map\]/", '
', $Text); - } - + // Check for bold text if (strpos($Text,'[b]') !== false) { $Text = preg_replace("(\[b\](.*?)\[\/b\])ism", '$1', $Text); @@ -1251,12 +1275,14 @@ function bbcode($Text, $options = []) { // html5 video and audio if (strpos($Text,'[/video]') !== false) { + $Text = preg_replace_callback("/\[video (.*?)\](.*?\.(ogg|ogv|oga|ogm|webm|mp4|mpeg|mpg))\[\/video\]/ism", 'videowithopts', $Text); $Text = preg_replace_callback("/\[video\](.*?\.(ogg|ogv|oga|ogm|webm|mp4|mpeg|mpg))\[\/video\]/ism", 'tryzrlvideo', $Text); } if (strpos($Text,'[/audio]') !== false) { $Text = preg_replace_callback("/\[audio\](.*?\.(ogg|ogv|oga|ogm|webm|mp4|mp3|opus|m4a))\[\/audio\]/ism", 'tryzrlaudio', $Text); } if (strpos($Text,'[/zvideo]') !== false) { + $Text = preg_replace_callback("/\[zvideo (.*?)\](.*?\.(ogg|ogv|oga|ogm|webm|mp4|mpeg|mpg))\[\/zvideo\]/ism", 'videowithopts', $Text); $Text = preg_replace_callback("/\[zvideo\](.*?\.(ogg|ogv|oga|ogm|webm|mp4|mpeg|mpg))\[\/zvideo\]/ism", 'tryzrlvideo', $Text); } if (strpos($Text,'[/zaudio]') !== false) { @@ -1360,4 +1386,3 @@ function bbcode($Text, $options = []) { return $Text; } - diff --git a/include/channel.php b/include/channel.php index 95a3f96cf..e4b6df47b 100644 --- a/include/channel.php +++ b/include/channel.php @@ -873,6 +873,13 @@ function identity_basic_export($channel_id, $sections = null) { $ret['abook'][$x]['abconfig'] = $abconfig; translate_abook_perms_outbound($ret['abook'][$x]); } + + // pick up the zot6 xchan and hublocs also + + if($ret['channel']['channel_portable_id']) { + $xchans[] = $ret['channel']['channel_portable_id']; + } + stringify_array_elms($xchans); } @@ -948,6 +955,18 @@ function identity_basic_export($channel_id, $sections = null) { } $ret['app'] = $r; } + $r = q("select * from app where app_channel = %d and app_system = 1", + intval($channel_id) + ); + if($r) { + for($x = 0; $x < count($r); $x ++) { + $r[$x]['term'] = q("select * from term where otype = %d and oid = %d", + intval(TERM_OBJ_APP), + intval($r[$x]['id']) + ); + } + $ret['sysapp'] = $r; + } } if(in_array('chatrooms',$sections)) { @@ -1080,6 +1099,7 @@ function identity_basic_export($channel_id, $sections = null) { return $ret; } + /** * @brief Export items for a year, or a month of a year. * @@ -1102,34 +1122,14 @@ function identity_export_year($channel_id, $year, $month = 0) { else $target_month = '01'; - $ret = array(); - - $ch = channelx_by_n($channel_id); - if($ch) { - $ret['relocate'] = [ 'channel_address' => $ch['channel_address'], 'url' => z_root()]; - } $mindate = datetime_convert('UTC', 'UTC', $year . '-' . $target_month . '-01 00:00:00'); if($month && $month < 12) $maxdate = datetime_convert('UTC', 'UTC', $year . '-' . $target_month_plus . '-01 00:00:00'); else $maxdate = datetime_convert('UTC', 'UTC', $year+1 . '-01-01 00:00:00'); - $r = q("select * from item where ( item_wall = 1 or item_type != %d ) and item_deleted = 0 and uid = %d and created >= '%s' and created < '%s' and resource_type = '' order by created", - intval(ITEM_TYPE_POST), - intval($channel_id), - dbesc($mindate), - dbesc($maxdate) - ); + return channel_export_items_date($channel_id,$mindate,$maxdate); - if($r) { - $ret['item'] = array(); - xchan_query($r); - $r = fetch_post_tags($r, true); - foreach($r as $rr) - $ret['item'][] = encode_item($rr, true); - } - - return $ret; } /** @@ -1142,7 +1142,8 @@ function identity_export_year($channel_id, $year, $month = 0) { * @param string $finish * @return array */ -function channel_export_items($channel_id, $start, $finish) { + +function channel_export_items_date($channel_id, $start, $finish) { if(! $start) return array(); @@ -1179,6 +1180,71 @@ function channel_export_items($channel_id, $start, $finish) { } + +/** + * @brief Export items with pagination + * + * + * @param int $channel_id The channel ID + * @param int $page + * @param int $limit (default 50) + * @return array + */ + +function channel_export_items_page($channel_id, $start, $finish, $page = 0, $limit = 50) { + + if(intval($page) < 1) { + $page = 0; + } + + if(intval($limit) < 1) { + $limit = 1; + } + + if(intval($limit) > 5000) { + $limit = 5000; + } + + if(! $start) + $start = NULL_DATE; + else + $start = datetime_convert('UTC', 'UTC', $start); + + $finish = datetime_convert('UTC', 'UTC', (($finish) ? $finish : 'now')); + if($finish < $start) + return []; + + $offset = intval($limit) * intval($page); + + $ret = []; + + $ch = channelx_by_n($channel_id); + if($ch) { + $ret['relocate'] = [ 'channel_address' => $ch['channel_address'], 'url' => z_root()]; + } + + $r = q("select * from item where ( item_wall = 1 or item_type != %d ) and item_deleted = 0 and uid = %d and resource_type = '' and created >= '%s' and created <= '%s' order by created limit %d offset %d", + intval(ITEM_TYPE_POST), + intval($channel_id), + dbesc($start), + dbesc($finish), + intval($limit), + intval($offset) + ); + + if($r) { + $ret['item'] = array(); + xchan_query($r); + $r = fetch_post_tags($r, true); + foreach($r as $rr) + $ret['item'][] = encode_item($rr, true); + } + + return $ret; +} + + + /** * @brief Loads a profile into the App structure. * @@ -1392,7 +1458,7 @@ function profile_edit_menu($uid) { * @param boolean $show_connect (optional) default true * @param mixed $zcard (optional) default false * - * @return HTML string suitable for sidebar inclusion + * @return string (HTML) suitable for sidebar inclusion * Exceptions: Returns empty string if passed $profile is wrong type or not populated */ function profile_sidebar($profile, $block = 0, $show_connect = true, $zcard = false) { @@ -1753,13 +1819,16 @@ function zid_init() { call_hooks('zid_init', $arr); if(! local_channel()) { - $r = q("select * from hubloc where hubloc_addr = '%s' order by hubloc_connected desc limit 1", + $r = q("select * from hubloc where hubloc_addr = '%s' order by hubloc_connected desc", dbesc($tmp_str) ); if(! $r) { Master::Summon(array('Gprobe',bin2hex($tmp_str))); } - if($r && remote_channel() && remote_channel() === $r[0]['hubloc_hash']) + if($r) { + $r = zot_record_preferred($r); + } + if($r && remote_channel() && remote_channel() === $r['hubloc_hash']) return; logger('Not authenticated. Invoking reverse magic-auth for ' . $tmp_str); @@ -1767,8 +1836,8 @@ function zid_init() { $query = App::$query_string; $query = str_replace(array('?zid=','&zid='),array('?rzid=','&rzid='),$query); $dest = '/' . $query; - if($r && ($r[0]['hubloc_url'] != z_root()) && (! strstr($dest,'/magic')) && (! strstr($dest,'/rmagic'))) { - goaway($r[0]['hubloc_url'] . '/magic' . '?f=&rev=1&owa=1&bdest=' . bin2hex(z_root() . $dest)); + if($r && ($r['hubloc_url'] != z_root()) && (! strstr($dest,'/magic')) && (! strstr($dest,'/rmagic'))) { + goaway($r['hubloc_url'] . '/magic' . '?f=&rev=1&owa=1&bdest=' . bin2hex(z_root() . $dest)); } else logger('No hubloc found.'); @@ -1789,6 +1858,7 @@ function zat_init() { ); if($r) { $xchan = atoken_xchan($r[0]); + atoken_create_xchan($xchan); atoken_login($xchan); } } diff --git a/include/connections.php b/include/connections.php index d97ea3887..e942503f0 100644 --- a/include/connections.php +++ b/include/connections.php @@ -373,19 +373,46 @@ function contact_remove($channel_id, $abook_id) { if(intval($abook['abook_self'])) return false; - $r = q("select id from item where (owner_xchan = '%s' or author_xchan = '%s') and uid = %d and item_retained = 0 and item_starred = 0", + $r = q("select id, parent from item where (owner_xchan = '%s' or author_xchan = '%s') and uid = %d and item_retained = 0 and item_starred = 0", dbesc($abook['abook_xchan']), dbesc($abook['abook_xchan']), intval($channel_id) ); if($r) { + $already_saved = []; foreach($r as $rr) { - $x = q("select uid from term where otype = %d and oid = %d and ttype = %d limit 1", + $w = $x = $y = null; + + // optimise so we only process newly seen parent items + if (in_array($rr['parent'],$already_saved)) { + continue; + } + // if this isn't the parent, fetch the parent's item_retained and item_starred to see if the conversation + // should be retained + if($rr['id'] != $rr['parent']) { + $w = q("select id, item_retained, item_starred from item where id = %d", + intval($rr['parent']) + ); + if($w) { + // see if the conversation was filed + $x = q("select uid from term where otype = %d and oid = %d and ttype = %d limit 1", + intval(TERM_OBJ_POST), + intval($w[0]['id']), + intval(TERM_FILE) + ); + if (intval($w[0]['item_retained']) || intval($w[0]['item_starred']) || $x) { + $already_saved[] = $rr['parent']; + continue; + } + } + } + // see if this item was filed + $y = q("select uid from term where otype = %d and oid = %d and ttype = %d limit 1", intval(TERM_OBJ_POST), intval($rr['id']), intval(TERM_FILE) ); - if($x) { + if ($y) { continue; } drop_item($rr['id'],false); diff --git a/include/dba/dba_driver.php b/include/dba/dba_driver.php index ee0e06a91..cfb208e2d 100755 --- a/include/dba/dba_driver.php +++ b/include/dba/dba_driver.php @@ -485,3 +485,28 @@ function db_columns($table) { return []; } + + +function db_indexes($table) { + + if($table) { + if(ACTIVE_DBTYPE === DBTYPE_POSTGRES) { + $r = q("SELECT indexname from pg_indexes where tablename = '%s'", + dbesc($table) + ); + if($r) { + return ids_to_array($r,'indexname'); + } + } + else { + $r = q("show index from %s", + dbesc($table) + ); + if($r) { + return ids_to_array($r,'Key_name'); + } + } + } + + return []; +} diff --git a/include/dba/dba_pdo.php b/include/dba/dba_pdo.php index 5002f53e7..0279342ec 100755 --- a/include/dba/dba_pdo.php +++ b/include/dba/dba_pdo.php @@ -19,7 +19,7 @@ class dba_pdo extends dba_driver { $this->driver_dbtype = $scheme; if(strpbrk($server,':;')) { - $dsn = $server; + $dsn = $this->driver_dbtype . ':unix_socket=' . trim($server, ':;'); } else { $dsn = $this->driver_dbtype . ':host=' . $server . (intval($port) ? ';port=' . $port : ''); @@ -161,23 +161,17 @@ class dba_pdo extends dba_driver { } function unescapebin($str) { - if($this->driver_dbtype === 'pgsql' && (! is_null($str))) { - $x = ''; - while(! feof($str)) { - $x .= fread($str,8192); + if($this->driver_dbtype === 'pgsql') { + if(gettype($str) === 'resource') { + $str = stream_get_contents($str); } - if(substr($x,0,2) === '\\x') { - $x = hex2bin(substr($x,2)); + if(substr($str,0,2) === '\\x') { + $str = hex2bin(substr($str,2)); } - return $x; - - } - else { - return $str; } + return $str; } - function getdriver() { return 'pdo'; } diff --git a/include/event.php b/include/event.php index fdb9e1415..471fb7afa 100644 --- a/include/event.php +++ b/include/event.php @@ -4,8 +4,11 @@ * @brief Event related functions. */ + use Sabre\VObject; +use Zotlabs\Lib\Activity; + use Ramsey\Uuid\Uuid; use Ramsey\Uuid\Exception\UnsatisfiedDependencyException; @@ -65,7 +68,7 @@ function format_event_html($ev) { } function format_event_obj($jobject) { - $event = array(); + $event = []; $object = json_decode($jobject,true); @@ -1046,6 +1049,7 @@ function event_store_item($arr, $event) { 'location' => $arr['location'], 'adjust' => $arr['adjust'], 'content' => format_event_bbcode($arr), + 'attachment' => Activity::encode_attachment($r[0]), 'author' => array( 'name' => $r[0]['xchan_name'], 'address' => $r[0]['xchan_addr'], @@ -1159,7 +1163,7 @@ function event_store_item($arr, $event) { $item_arr['item_thread_top'] = $item_thread_top; $attach = array(array( - 'href' => z_root() . '/events/ical/' . urlencode($event['event_hash']), + 'href' => z_root() . '/channel_calendar/ical/' . urlencode($event['event_hash']), 'length' => 0, 'type' => 'text/calendar', 'title' => t('event') . '-' . $event['event_hash'], @@ -1181,7 +1185,7 @@ function event_store_item($arr, $event) { // otherwise we'll fallback to /display/$message_id if($wall) - $item_arr['plink'] = z_root() . '/channel/' . $z[0]['channel_address'] . '/?f=&mid=' . urlencode($item_arr['mid']); + $item_arr['plink'] = z_root() . '/channel/' . $z[0]['channel_address'] . '/?f=&mid=' . gen_link_id($item_arr['mid']); else $item_arr['plink'] = z_root() . '/display/' . gen_link_id($item_arr['mid']); @@ -1200,6 +1204,7 @@ function event_store_item($arr, $event) { 'location' => $arr['location'], 'adjust' => $arr['adjust'], 'content' => format_event_bbcode($arr), + 'attachment' => Activity::encode_attachment($item_arr), 'author' => array( 'name' => $x[0]['xchan_name'], 'address' => $x[0]['xchan_addr'], @@ -1279,6 +1284,10 @@ function cdav_principal($uri) { } function cdav_perms($needle, $haystack, $check_rw = false) { + + if($needle == 'channel_calendar') + return true; + foreach ($haystack as $item) { if($check_rw) { if(is_array($item['id'])) { diff --git a/include/features.php b/include/features.php index 35a4c0dd4..d8607f447 100644 --- a/include/features.php +++ b/include/features.php @@ -79,7 +79,7 @@ function get_features($filtered = true, $level = (-1)) { 'calendar' => [ - t('CalDAV'), + t('Calendar'), [ 'cal_first_day', @@ -167,6 +167,14 @@ function get_features($filtered = true, $level = (-1)) { t('Ability to mark special posts with a star indicator'), false, get_config('feature_lock','star_posts'), + ], + + [ + 'reply_to', + t('Reply on comment'), + t('Ability to reply on selected comment'), + false, + get_config('feature_lock','reply_to'), ] ], @@ -360,14 +368,6 @@ function get_features($filtered = true, $level = (-1)) { get_config('feature_lock','personal_tab') ], - [ - 'suggest', - t('Suggest Channels'), - t('Show friend and connection suggestions'), - false, - get_config('feature_lock','suggest') - ], - [ 'network_list_mode', t('Use blog/list mode'), diff --git a/include/follow.php b/include/follow.php index db77a0160..50b952881 100644 --- a/include/follow.php +++ b/include/follow.php @@ -119,6 +119,7 @@ function new_contact($uid,$url,$channel,$interactive = false, $confirm = false) if( array_key_exists('permissions',$j) && array_key_exists('data',$j['permissions'])) { $permissions = crypto_unencapsulate(array( 'data' => $j['permissions']['data'], + 'alg' => $j['permissions']['alg'], 'key' => $j['permissions']['key'], 'iv' => $j['permissions']['iv']), $channel['channel_prvkey']); @@ -141,7 +142,7 @@ function new_contact($uid,$url,$channel,$interactive = false, $confirm = false) $sql_options = (($protocol) ? " and xchan_network = '" . dbesc($protocol) . "' " : ''); - $r = q("select * from xchan where xchan_hash = '%s' or xchan_url = '%s' $sql_options limit 1", + $r = q("select * from xchan where xchan_hash = '%s' or xchan_url = '%s' $sql_options ", dbesc($url), dbesc($url) ); @@ -168,18 +169,19 @@ function new_contact($uid,$url,$channel,$interactive = false, $confirm = false) } if($wf || $d) { - $r = q("select * from xchan where xchan_hash = '%s' or xchan_url = '%s' limit 1", + $r = q("select * from xchan where xchan_hash = '%s' or xchan_url = '%s'", dbesc(($wf) ? $wf : $url), dbesc($url) ); } } + $xchan = zot_record_preferred($r,'xchan_network'); + // if discovery was a success we should have an xchan record in $r - if($r) { - $xchan = $r[0]; - $xchan_hash = $r[0]['xchan_hash']; + if($xchan) { + $xchan_hash = $xchan['xchan_hash']; $their_perms = 0; } } @@ -190,9 +192,9 @@ function new_contact($uid,$url,$channel,$interactive = false, $confirm = false) return $result; } - $allowed = (($is_zot || in_array($r[0]['xchan_network'],['rss','zot6'])) ? 1 : 0); + $allowed = (($is_zot || in_array($xchan['xchan_network'],['rss','zot6'])) ? 1 : 0); - $x = array('channel_id' => $uid, 'follow_address' => $url, 'xchan' => $r[0], 'allowed' => $allowed, 'singleton' => 0); + $x = array('channel_id' => $uid, 'follow_address' => $url, 'xchan' => $xchan, 'allowed' => $allowed, 'singleton' => 0); call_hooks('follow_allow',$x); diff --git a/include/html2bbcode.php b/include/html2bbcode.php index 1a03fbdaf..c916421b8 100644 --- a/include/html2bbcode.php +++ b/include/html2bbcode.php @@ -219,8 +219,7 @@ function html2bbcode($message) $message = $doc->saveHTML(); - // I'm removing something really disturbing - // Don't know exactly what it is + // I'm removing the UTF-8 encoding of a NO-BREAK SPACE codepoint $message = str_replace(chr(194).chr(160), ' ', $message); $message = str_replace(" ", " ", $message); diff --git a/include/hubloc.php b/include/hubloc.php index b2903b0ee..4a1f77733 100644 --- a/include/hubloc.php +++ b/include/hubloc.php @@ -305,3 +305,41 @@ function ping_site($url) { return $ret; } + + +function z6_discover() { + + // find unregistered zot6 clone hublocs + + $c = q("select channel_hash, channel_portable_id from channel where channel_deleted = '%s'", + dbesc(NULL_DATE) + ); + if ($c) { + foreach ($c as $entry) { + $q1 = q("select * from hubloc left join site on hubloc_url = site_url where hubloc_deleted = 0 and site_dead = 0 and hubloc_hash = '%s' and hubloc_url != '%s'", + dbesc($entry['channel_hash']), + dbesc(z_root()) + ); + if (! $q1) { + // channel has no zot clones + continue; + } + // does this particular server have a zot6 clone registered on our site for this channel? + foreach ($q1 as $q) { + $q2 = q("select * from hubloc left join site on hubloc_url = site_url where hubloc_deleted = 0 and site_dead = 0 and hubloc_hash = '%s' and hubloc_url = '%s'", + dbesc($entry['channel_portable_id']), + dbesc($q['hubloc_url']) + ); + if ($q2) { + continue; + } + // zot6 hubloc not found. + if(strpos($q['site_project'],'hubzilla') !== false && version_compare($q['site_version'],'4.0') >= 0) { + // probe and store results - only for zot6 (over-ride the zot default) + discover_by_webbie($q['hubloc_addr'],'zot6'); + } + } + } + } + +} diff --git a/include/import.php b/include/import.php index f391400bd..92ba014d8 100644 --- a/include/import.php +++ b/include/import.php @@ -94,7 +94,8 @@ function import_channel($channel, $account_id, $seize, $newname = '') { 'channel_w_comment', 'channel_w_mail', 'channel_w_like', 'channel_w_tagwall', 'channel_w_chat', 'channel_w_storage', 'channel_w_pages', 'channel_a_republish', 'channel_a_delegate', 'perm_limits', 'channel_password', 'channel_salt', - 'channel_moved', 'channel_removed', 'channel_deleted', 'channel_system' + 'channel_moved', 'channel_removed', 'channel_deleted', 'channel_system', + 'channel_r_photos', 'channel_w_photos' ]; $clean = array(); @@ -147,7 +148,9 @@ function import_config($channel, $configs) { foreach($configs as $config) { unset($config['id']); $config['uid'] = $channel['channel_id']; - + if($config['cat'] === 'system' && $config['k'] === 'import_system_apps') { + continue; + } create_table_from_array('pconfig', $config); } @@ -364,6 +367,9 @@ function import_apps($channel, $apps) { if($channel && $apps) { foreach($apps as $app) { + if(array_key_exists('app_system',$app) && intval($app['app_system'])) + continue; + $term = ((array_key_exists('term',$app) && is_array($app['term'])) ? $app['term'] : null); unset($app['id']); @@ -413,6 +419,9 @@ function sync_apps($channel, $apps) { $exists = false; $term = ((array_key_exists('term',$app)) ? $app['term'] : null); + if(array_key_exists('app_system',$app) && intval($app['app_system'])) + continue; + $x = q("select * from app where app_id = '%s' and app_channel = %d limit 1", dbesc($app['app_id']), intval($channel['channel_id']) @@ -504,6 +513,84 @@ function sync_apps($channel, $apps) { } } + + +/** + * @brief Import system apps. + * System apps from the original server may not exist on this system + * (e.g. apps associated with addons that are not installed here). + * Check the system apps that were provided in the import file to see if they + * exist here and if so, install them locally. Preserve categories that + * might have been added by this channel on the other server. + * Do not use any paths from the original as they will point to a different server. + * @param array $channel + * @param array $apps + */ +function import_sysapps($channel, $apps) { + + if($channel && $apps) { + + $sysapps = \Zotlabs\Lib\Apps::get_system_apps(false); + + foreach($apps as $app) { + + if(array_key_exists('app_system',$app) && (! intval($app['app_system']))) + continue; + + $term = ((array_key_exists('term',$app) && is_array($app['term'])) ? $app['term'] : null); + + foreach($sysapps as $sysapp) { + if($app['app_id'] === hash('whirlpool',$sysapp['app_name'])) { + // install this app on this server + $newapp = $sysapp; + $newapp['uid'] = $channel['channel_id']; + $newapp['guid'] = hash('whirlpool',$newapp['name']); + + $installed = q("select id from app where app_id = '%s' and app_channel = %d limit 1", + dbesc($newapp['guid']), + intval($channel['channel_id']) + ); + if($installed) { + break; + } + + $newapp['system'] = 1; + if($term) { + $s = EMPTY_STR; + foreach($term as $t) { + if($s) { + $s .= ','; + } + $s .= $t['term']; + } + $newapp['categories'] = $s; + } + \Zotlabs\Lib\Apps::app_install($channel['channel_id'],$newapp); + } + } + } + } +} + +/** + * @brief Sync system apps. + * + * @param array $channel + * @param array $apps + */ +function sync_sysapps($channel, $apps) { + + if($channel && $apps) { + + // we do not currently sync system apps + + } +} + + + + + /** * @brief Import chatrooms. * @@ -1069,6 +1156,9 @@ function sync_files($channel, $files) { require_once('include/attach.php'); if($channel && $files) { + + $limit = service_class_fetch($channel['channel_id'], 'attach_upload_limit'); + foreach($files as $f) { if(! $f) continue; @@ -1189,6 +1279,17 @@ function sync_files($channel, $files) { } else { logger('sync_files attach does not exists: ' . print_r($att,true), LOGGER_DEBUG); + + if($limit !== false) { + $r = q("select sum(filesize) as total from attach where aid = %d ", + intval($channel['channel_account_id']) + ); + if(($r) && (($r[0]['total'] + $att['filesize']) > $limit)) { + logger('service class limit exceeded'); + continue; + } + } + create_table_from_array('attach',$att); } @@ -1245,6 +1346,7 @@ function sync_files($channel, $files) { logger('attachment store failed',LOGGER_NORMAL,LOG_ERR); } if($f['photo']) { + foreach($f['photo'] as $p) { unset($p['id']); $p['aid'] = $channel['channel_account_id']; @@ -1266,6 +1368,7 @@ function sync_files($channel, $files) { dbesc($p['resource_id']), intval($channel['channel_id']) ); + $update_xchan = $p['edited']; } // same for cover photos @@ -1285,19 +1388,20 @@ function sync_files($channel, $files) { else $p['content'] = (($p['content'])? base64_decode($p['content']) : ''); - if(intval($p['imgscale']) && (! $p['content'])) { + if(intval($p['imgscale']) && (! empty($p['content']))) { $time = datetime_convert(); - $parr = array('hash' => $channel['channel_hash'], + $parr = array( + 'hash' => $channel['channel_hash'], 'time' => $time, - 'resource' => $att['hash'], + 'resource' => $p['resource_id'], 'revision' => 0, 'signature' => base64url_encode(rsa_sign($channel['channel_hash'] . '.' . $time, $channel['channel_prvkey'])), - 'resolution' => $p['imgscale'] + 'resolution' => intval($p['imgscale']) ); - $stored_image = $newfname . '-' . intval($p['imgscale']); + $stored_image = $newfname . '-' . $p['imgscale']; $fp = fopen($stored_image,'w'); if(! $fp) { @@ -1306,7 +1410,6 @@ function sync_files($channel, $files) { } $redirects = 0; - $headers = []; $headers['Accept'] = 'application/x-zot+json' ; $headers['Sigtoken'] = random_string(); @@ -1314,8 +1417,17 @@ function sync_files($channel, $files) { $x = z_post_url($fetch_url,$parr,$redirects,[ 'filep' => $fp, 'headers' => $headers]); fclose($fp); - $p['content'] = file_get_contents($stored_image); - unlink($stored_image); + + // Override remote hub thumbnails storage settings + if(! boolval(get_config('system','filesystem_storage_thumbnails', 0))) { + $p['os_storage'] = 0; + $p['content'] = file_get_contents($stored_image); + @unlink($stored_image); + } + else { + $p['os_storage'] = 1; + $p['content'] = $stored_image; + } } if(!isset($p['display_path'])) @@ -1347,6 +1459,16 @@ function sync_files($channel, $files) { create_table_from_array('photo',$p, [ 'content' ] ); } } + + } + + // Set xchan photo date to prevent thumbnails fetch for clones on profile update packet recieve + if(isset($update_xchan)) { + + $x = q("UPDATE xchan SET xchan_photo_date = '%s' WHERE xchan_hash = '%s'", + dbescdate($update_xchan), + dbesc($channel['channel_hash']) + ); } \Zotlabs\Daemon\Master::Summon([ 'Thumbnail' , $att['hash'] ]); diff --git a/include/items.php b/include/items.php index 30e758add..95b696034 100755 --- a/include/items.php +++ b/include/items.php @@ -763,11 +763,11 @@ function get_item_elements($x,$allow_code = false) { // check the supplied signature against the supplied content. // Note that we will purify the content which could change it. - $r = q("select xchan_pubkey from xchan where xchan_hash = '%s' limit 1", + $r = q("select xchan_pubkey, xchan_network from xchan where xchan_hash = '%s' limit 1", dbesc($arr['author_xchan']) ); if($r) { - if($r[0]['xchan_pubkey']) { + if($r[0]['xchan_pubkey'] && $r[0]['xchan_network'] === 'zot') { if(rsa_verify($x['body'],base64url_decode($arr['sig']),$r[0]['xchan_pubkey'])) { $arr['item_verified'] = 1; } @@ -914,6 +914,16 @@ function import_author_xchan($x) { if(array_key_exists('network',$x) && $x['network'] === 'zot') return $y; + // perform zot6 discovery + + if($x['url']) { + $y = discover_by_webbie($x['url'],'zot6'); + + if($y) { + return $y; + } + } + if($x['network'] === 'rss') { $y = import_author_rss($x); } @@ -1920,11 +1930,21 @@ function item_store($arr, $allow_exec = false, $deliver = true) { if($parent_deleted) $arr['item_deleted'] = 1; - $r = q("SELECT id FROM item WHERE mid = '%s' AND uid = %d and revision = %d LIMIT 1", - dbesc($arr['mid']), - intval($arr['uid']), - intval($arr['revision']) - ); + if($arr['uuid']) { + $r = q("SELECT id FROM item WHERE uuid = '%s' AND uid = %d and revision = %d LIMIT 1", + dbesc($arr['uuid']), + intval($arr['uid']), + intval($arr['revision']) + ); + } + else { + $r = q("SELECT id FROM item WHERE mid = '%s' AND uid = %d and revision = %d LIMIT 1", + dbesc($arr['mid']), + intval($arr['uid']), + intval($arr['revision']) + ); + } + if($r) { logger('duplicate item ignored. ' . print_r($arr,true)); $ret['message'] = 'duplicate post.'; @@ -4258,7 +4278,7 @@ function items_fetch($arr,$channel = null,$observer_hash = null,$client_mode = C if($arr['mid']) $sql_options .= " and parent_mid = '" . dbesc($arr['mid']) . "' "; - $sql_extra = " AND item.parent IN ( SELECT parent FROM item WHERE item_thread_top = 1 $sql_options $item_normal ) "; + $sql_extra = " AND item.parent IN ( SELECT parent FROM item WHERE $item_uids and item_thread_top = 1 $sql_options $item_normal ) "; if($arr['since_id']) $sql_extra .= " and item.id > " . $since_id . " "; @@ -4688,7 +4708,7 @@ function sync_an_item($channel_id,$item_id) { if($r) { xchan_query($r); $sync_item = fetch_post_tags($r); - build_sync_packet($channel_d,array('item' => array(encode_item($sync_item[0],true)))); + build_sync_packet($channel_id, array('item' => array(encode_item($sync_item[0],true)))); } } diff --git a/include/js_strings.php b/include/js_strings.php index c053e5666..f01fa87ae 100644 --- a/include/js_strings.php +++ b/include/js_strings.php @@ -33,6 +33,7 @@ function js_strings() { '$name_empty' => t('A channel name is required.'), '$name_ok1' => t('This is a '), '$name_ok2' => t(' channel name'), + '$to_reply' => t('Back to reply'), // translatable prefix and suffix strings for jquery.timeago - // using the defaults set below if left untranslated, empty strings if diff --git a/include/message.php b/include/message.php index 037c59c60..2486beb83 100644 --- a/include/message.php +++ b/include/message.php @@ -220,8 +220,8 @@ function send_message($uid = 0, $recipient = '', $body = '', $subject = '', $rep foreach($images as $image) { if(! stristr($image,z_root() . '/photo/')) continue; - $image_uri = substr($image,strrpos($image,'/') + 1); - $image_uri = substr($image_uri,0, strpos($image_uri,'-')); + $image_uri = substr($image, strrpos($image, '/') + 1); + $image_uri = substr($image_uri, 0, strpos($image_uri, '.') - 2); $r = q("UPDATE photo SET allow_cid = '%s' WHERE resource_id = '%s' AND uid = %d and allow_cid = '%s'", dbesc('<' . $recipient . '>'), dbesc($image_uri), diff --git a/include/nav.php b/include/nav.php index 58e13dd93..3e1acd306 100644 --- a/include/nav.php +++ b/include/nav.php @@ -42,6 +42,10 @@ function nav($template = 'default') { require_once('include/conversation.php'); + $nav_apps = []; + $navbar_apps = []; + $channel_apps = []; + $channel_apps[] = channel_apps($is_owner, App::$profile['channel_address']); @@ -179,7 +183,6 @@ function nav($template = 'default') { $search_form_action = 'search'; } - $nav['search'] = ['search', t('Search'), "", t('Search site @name, !forum, #tag, ?docs, content'), $search_form_action]; /** @@ -378,16 +381,15 @@ function channel_apps($is_owner = false, $nickname = null) { if(App::$is_sys) return ''; - if(! get_pconfig($uid, 'system', 'channelapps','1')) - return ''; - $channel = App::get_channel(); if($channel && is_null($nickname)) $nickname = $channel['channel_address']; $uid = ((App::$profile['profile_uid']) ? App::$profile['profile_uid'] : local_channel()); - $account_id = ((App::$profile['profile_uid']) ? App::$profile['channel_account_id'] : App::$channel['channel_account_id']); + + if(! get_pconfig($uid, 'system', 'channelapps','1')) + return; if($uid == local_channel()) { return; diff --git a/include/network.php b/include/network.php index 8ac71011e..c754625cd 100644 --- a/include/network.php +++ b/include/network.php @@ -114,6 +114,13 @@ function z_fetch_url($url, $binary = false, $redirects = 0, $opts = array()) { @curl_setopt($ch, CURLOPT_TIMEOUT, (($curl_time !== false) ? $curl_time : 60)); } + if(x($opts,'connecttimeout') && intval($opts['connecttimeout'])) { + @curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, intval($opts['connecttimeout'])); + } + else { + $curl_contime = intval(@get_config('system','curl_connecttimeout')); + @curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, (($curl_contime !== false) ? $curl_contime : 30)); + } if(x($opts,'http_auth')) { // "username" . ':' . "password" @@ -867,13 +874,16 @@ function xml2array($contents, $namespaces = true, $get_attributes=1, $priority = } -function email_header_encode($in_str, $charset = 'UTF-8') { +function email_header_encode($in_str, $charset = 'UTF-8', $header = 'Subject') { + + $out_str = $in_str; $need_to_convert = false; for($x = 0; $x < strlen($in_str); $x ++) { if((ord($in_str[$x]) == 0) || ((ord($in_str[$x]) > 128))) { $need_to_convert = true; + break; } } @@ -885,11 +895,11 @@ function email_header_encode($in_str, $charset = 'UTF-8') { // define start delimimter, end delimiter and spacer $end = "?="; $start = "=?" . $charset . "?B?"; - $spacer = $end . "\r\n " . $start; + $spacer = $end . PHP_EOL . " " . $start; // determine length of encoded text within chunks // and ensure length is even - $length = 75 - strlen($start) - strlen($end); + $length = 75 - strlen($start) - strlen($end) - (strlen($header) + 2); /* [EDIT BY danbrown AT php DOT net: The following @@ -1789,8 +1799,8 @@ function z_mail($params) { $messageHeader = $params['additionalMailHeader'] . - "From: $fromName <{$params['fromEmail']}>\n" . - "Reply-To: $fromName <{$params['replyTo']}>\n" . + "From: $fromName <{$params['fromEmail']}>" . PHP_EOL . + "Reply-To: $fromName <{$params['replyTo']}>" . PHP_EOL . "Content-Type: text/plain; charset=UTF-8"; // send the message diff --git a/include/oembed.php b/include/oembed.php index 426197c5f..ee9e57c3f 100755 --- a/include/oembed.php +++ b/include/oembed.php @@ -1,6 +1,6 @@ '', + 'title' => t('View PDF'), + 'type' => 'pdf' + ]; + // set $txt to something so that we don't attempt to fetch what could be a lengthy pdf. + $txt = EMPTY_STR; + } + if(is_null($txt)) { $txt = ""; @@ -215,20 +226,10 @@ function oembed_fetch_url($embedurl){ // save in cache if(! get_config('system','oembed_cache_disable')) - Zlib\Cache::set('[' . App::$videowidth . '] ' . $furl, $txt); + Cache::set('[' . App::$videowidth . '] ' . $furl, $txt); } - if(strpos(strtolower($embedurl),'.pdf') !== false) { - $action = 'allow'; - $j = [ - 'html' => '', - 'title' => t('View PDF'), - 'type' => 'pdf' - ]; - - } - if(! $j) { $j = json_decode($txt,true); } diff --git a/include/permissions.php b/include/permissions.php index 115d96eca..501b2cc77 100644 --- a/include/permissions.php +++ b/include/permissions.php @@ -157,7 +157,7 @@ function get_all_perms($uid, $observer_xchan, $check_siteblock = true, $default_ // If we're still here, we have an observer, check the network. if($channel_perm & PERMS_NETWORK) { - if($x && $x[0]['xchan_network'] === 'zot') { + if($x && in_array($x[0]['xchan_network'],[ 'zot','zot6'])) { $ret[$perm_name] = true; continue; } @@ -192,7 +192,7 @@ function get_all_perms($uid, $observer_xchan, $check_siteblock = true, $default_ // They are in your address book, but haven't been approved - if($channel_perm & PERMS_PENDING) { + if($channel_perm & PERMS_PENDING && (! intval($x[0]['abook_pseudo']))) { $ret[$perm_name] = true; continue; } @@ -316,10 +316,19 @@ function perm_is_allowed($uid, $observer_xchan, $permission, $check_siteblock = if(! $x) { // not in address book and no guest token, see if they've got an xchan + $y = q("select xchan_network from xchan where xchan_hash = '%s' limit 1", dbesc($observer_xchan) ); if($y) { + + // This requires an explanation and the effects are subtle. + // The following line creates a fake connection, and this allows + // access tokens to have specific permissions even though they are + // not actual connections. + // The existence of this fake entry must be checked when dealing + // with connection related permissions. + $x = array(pseudo_abook($y[0])); } } @@ -327,7 +336,6 @@ function perm_is_allowed($uid, $observer_xchan, $permission, $check_siteblock = } $abperms = load_abconfig($uid,$observer_xchan,'my_perms'); } - // system is blocked to anybody who is not authenticated @@ -349,6 +357,7 @@ function perm_is_allowed($uid, $observer_xchan, $permission, $check_siteblock = return true; // If it's an unauthenticated observer, we only need to see if PERMS_PUBLIC is set + // We just did that. if(! $observer_xchan) { return false; @@ -357,7 +366,7 @@ function perm_is_allowed($uid, $observer_xchan, $permission, $check_siteblock = // If we're still here, we have an observer, check the network. if($channel_perm & PERMS_NETWORK) { - if (($x && $x[0]['xchan_network'] === 'zot') || ($y && $y[0]['xchan_network'] === 'zot')) + if ($x && in_array($x[0]['xchan_network'], ['zot','zot6'])) return true; } @@ -373,8 +382,7 @@ function perm_is_allowed($uid, $observer_xchan, $permission, $check_siteblock = return false; } - // From here on we require that the observer be a connection and - // handle whether we're allowing any, approved or specific ones + // From here on we require that the observer be a connection or pseudo connection if(! $x) { return false; @@ -382,7 +390,7 @@ function perm_is_allowed($uid, $observer_xchan, $permission, $check_siteblock = // They are in your address book, but haven't been approved - if($channel_perm & PERMS_PENDING) { + if($channel_perm & PERMS_PENDING && (! intval($x[0]['abook_pseudo']))) { return true; } diff --git a/include/photos.php b/include/photos.php index 44406e0b0..7ea2729ae 100644 --- a/include/photos.php +++ b/include/photos.php @@ -277,8 +277,7 @@ function photo_upload($channel, $observer, $args) { if(($width > 1024 || $height > 1024) && (! $errors)) $ph->scaleImage(1024); - $p['imgscale'] = 1; - $r1 = $ph->save($p); + $r1 = $ph->storeThumbnail($p, PHOTO_RES_1024); $link[1] = array( 'rel' => 'alternate', 'type' => 'text/html', @@ -292,8 +291,7 @@ function photo_upload($channel, $observer, $args) { if(($width > 640 || $height > 640) && (! $errors)) $ph->scaleImage(640); - $p['imgscale'] = 2; - $r2 = $ph->save($p); + $r2 = $ph->storeThumbnail($p, PHOTO_RES_640); $link[2] = array( 'rel' => 'alternate', 'type' => 'text/html', @@ -307,8 +305,7 @@ function photo_upload($channel, $observer, $args) { if(($width > 320 || $height > 320) && (! $errors)) $ph->scaleImage(320); - $p['imgscale'] = 3; - $r3 = $ph->save($p); + $r3 = $ph->storeThumbnail($p, PHOTO_RES_320); $link[3] = array( 'rel' => 'alternate', 'type' => 'text/html', diff --git a/include/security.php b/include/security.php index 493d34699..38cb72263 100644 --- a/include/security.php +++ b/include/security.php @@ -306,6 +306,7 @@ function change_channel($change_channel) { * * @return string additional SQL where statement */ + function permissions_sql($owner_id, $remote_observer = null, $table = '') { $local_channel = local_channel(); @@ -316,7 +317,7 @@ function permissions_sql($owner_id, $remote_observer = null, $table = '') { * default permissions - anonymous user */ - if($table) + if ($table) $table .= '.'; $sql = " AND {$table}allow_cid = '' @@ -329,38 +330,63 @@ function permissions_sql($owner_id, $remote_observer = null, $table = '') { * Profile owner - everything is visible */ - if(($local_channel) && ($local_channel == $owner_id)) { - $sql = ''; + if (($local_channel) && ($local_channel == $owner_id)) { + return EMPTY_STR; } /** - * Authenticated visitor. Unless pre-verified, - * check that the contact belongs to this $owner_id - * and load the groups the visitor belongs to. - * If pre-verified, the caller is expected to have already - * done this and passed the groups into this function. + * Authenticated visitor. */ else { + $observer = ((! is_null($remote_observer)) ? $remote_observer : get_observer_hash()); - if($observer) { - $groups = init_groups_visitor($observer); - $gs = '<<>>'; // should be impossible to match + if ($observer) { - if(is_array($groups) && count($groups)) { - foreach($groups as $g) - $gs .= '|<' . $g . '>'; + $sec = get_security_ids($owner_id,$observer); + + // always allow the channel owner, even if authenticated as a visitor + + if ($sec['channel_id']) { + foreach ($sec['channel_id'] as $ch) { + if ($observer === $ch) { + return EMPTY_STR; + } + } + } + + if (is_array($sec['allow_cid']) && count($sec['allow_cid'])) { + $ca = []; + foreach ($sec['allow_cid'] as $c) { + $ca[] = '<' . $c . '>'; + } + $cs = implode('|',$ca); } + else { + $cs = '<<>>'; // should be impossible to match + } + + if (is_array($sec['allow_gid']) && count($sec['allow_gid'])) { + $ga = []; + foreach ($sec['allow_gid'] as $g) { + $ga[] = '<' . $g . '>'; + } + $gs = implode('|',$ga); + } + else { + $gs = '<<>>'; // should be impossible to match + } + $regexop = db_getfunc('REGEXP'); $sql = sprintf( - " AND ( NOT ({$table}deny_cid like '%s' OR {$table}deny_gid $regexop '%s') - AND ( {$table}allow_cid like '%s' OR {$table}allow_gid $regexop '%s' OR ( {$table}allow_cid = '' AND {$table}allow_gid = '') ) + " AND ( NOT ({$table}deny_cid $regexop '%s' OR {$table}deny_gid $regexop '%s') + AND ( {$table}allow_cid $regexop '%s' OR {$table}allow_gid $regexop '%s' OR ( {$table}allow_cid = '' AND {$table}allow_gid = '') ) ) ", - dbesc(protect_sprintf( '%<' . $observer . '>%')), + dbesc($cs), dbesc($gs), - dbesc(protect_sprintf( '%<' . $observer . '>%')), + dbesc($cs), dbesc($gs) ); } @@ -377,6 +403,7 @@ function permissions_sql($owner_id, $remote_observer = null, $table = '') { * * @return string additional SQL where statement */ + function item_permissions_sql($owner_id, $remote_observer = null) { $local_channel = local_channel(); @@ -398,37 +425,59 @@ function item_permissions_sql($owner_id, $remote_observer = null) { } /** - * Authenticated visitor. Unless pre-verified, - * check that the contact belongs to this $owner_id - * and load the groups the visitor belongs to. - * If pre-verified, the caller is expected to have already - * done this and passed the groups into this function. + * Authenticated visitor. */ else { - $observer = (($remote_observer) ? $remote_observer : get_observer_hash()); - if($observer) { + $observer = (($remote_observer) ? $remote_observer : get_observer_hash()); - $s = scopes_sql($owner_id,$observer); + if($observer) { - $groups = init_groups_visitor($observer); + $scope = scopes_sql($owner_id,$observer); + $sec = get_security_ids($owner_id,$observer); - $gs = '<<>>'; // should be impossible to match + // always allow the channel owner, even if authenticated as a visitor - if(is_array($groups) && count($groups)) { - foreach($groups as $g) - $gs .= '|<' . $g . '>'; + if($sec['channel_id']) { + foreach($sec['channel_id'] as $ch) { + if($observer === $ch) { + return EMPTY_STR; + } + } + } + + if (is_array($sec['allow_cid']) && count($sec['allow_cid'])) { + $ca = []; + foreach ($sec['allow_cid'] as $c) { + $ca[] = '<' . $c . '>'; + } + $cs = implode('|',$ca); } + else { + $cs = '<<>>'; // should be impossible to match + } + + if (is_array($sec['allow_gid']) && count($sec['allow_gid'])) { + $ga = []; + foreach ($sec['allow_gid'] as $g) { + $ga[] = '<' . $g . '>'; + } + $gs = implode('|',$ga); + } + else { + $gs = '<<>>'; // should be impossible to match + } + $regexop = db_getfunc('REGEXP'); $sql = sprintf( - " AND (( NOT (deny_cid like '%s' OR deny_gid $regexop '%s') - AND ( allow_cid like '%s' OR allow_gid $regexop '%s' OR ( allow_cid = '' AND allow_gid = '' AND item_private = 0 )) - ) OR ( item_private = 1 $s )) + " AND (( NOT (deny_cid $regexop '%s' OR deny_gid $regexop '%s') + AND ( allow_cid $regexop '%s' OR allow_gid $regexop '%s' OR ( allow_cid = '' AND allow_gid = '' AND item_private = 0 )) + ) OR ( item_private = 1 $scope )) ", - dbesc(protect_sprintf( '%<' . $observer . '>%')), + dbesc($cs), dbesc($gs), - dbesc(protect_sprintf( '%<' . $observer . '>%')), + dbesc($cs), dbesc($gs) ); } @@ -465,40 +514,57 @@ function scopes_sql($uid,$observer) { } - - - - - /** * @param string $observer_hash * * @return string additional SQL where statement */ + function public_permissions_sql($observer_hash) { - $groups = init_groups_visitor($observer_hash); + $owner_id = 0; - $gs = '<<>>'; // should be impossible to match + if ($observer_hash) { + + $sec = get_security_ids($owner_id,$observer_hash); + + if (is_array($sec['allow_cid']) && count($sec['allow_cid'])) { + $ca = []; + foreach ($sec['allow_cid'] as $c) { + $ca[] = '<' . $c . '>'; + } + $cs = implode('|',$ca); + } + else { + $cs = '<<>>'; // should be impossible to match + } + + if (is_array($sec['allow_gid']) && count($sec['allow_gid'])) { + $ga = []; + foreach ($sec['allow_gid'] as $g) { + $ga[] = '<' . $g . '>'; + } + $gs = implode('|',$ga); + } + else { + $gs = '<<>>'; // should be impossible to match + } - if(is_array($groups) && count($groups)) { - foreach($groups as $g) - $gs .= '|<' . $g . '>'; - } - $sql = ''; - if($observer_hash) { $regexop = db_getfunc('REGEXP'); $sql = sprintf( - " OR (( NOT (deny_cid like '%s' OR deny_gid $regexop '%s') - AND ( allow_cid like '%s' OR allow_gid $regexop '%s' OR ( allow_cid = '' AND allow_gid = '' AND item_private = 0 ) ) - )) + " AND ( NOT (deny_cid $regexop '%s' OR deny_gid $regexop '%s') + AND ( allow_cid $regexop '%s' OR allow_gid $regexop '%s' OR ( allow_cid = '' AND allow_gid = '' AND item_private = 0) ) + ) ", - dbesc(protect_sprintf( '%<' . $observer_hash . '>%')), + dbesc($cs), dbesc($gs), - dbesc(protect_sprintf( '%<' . $observer_hash . '>%')), + dbesc($cs), dbesc($gs) ); } + else { + $sql = EMPTY_STR; + } return $sql; } @@ -510,7 +576,7 @@ function public_permissions_sql($observer_hash) { * In this implementation, a security token is reusable (if the user submits a form, goes back and resubmits the form, maybe with small changes; * or if the security token is used for ajax-calls that happen several times), but only valid for a certain amout of time (3hours). * The "typename" seperates the security tokens of different types of forms. This could be relevant in the following case: - * A security token is used to protekt a link from CSRF (e.g. the "delete this profile"-link). + * A security token is used to protect a link from CSRF (e.g. the "delete this profile"-link). * If the new page contains by any chance external elements, then the used security token is exposed by the referrer. * Actually, important actions should not be triggered by Links / GET-Requests at all, but somethimes they still are, * so this mechanism brings in some damage control (the attacker would be able to forge a request to a form of this type, but not to forms of other types). @@ -564,24 +630,40 @@ function check_form_security_token_ForbiddenOnErr($typename = '', $formname = 'f function init_groups_visitor($contact_id) { $groups = []; - // private profiles are treated as a virtual group - - $r = q("SELECT abook_profile from abook where abook_xchan = '%s' and abook_profile != '' ", + $x = q("select * from xchan where xchan_hash = '%s'", dbesc($contact_id) ); - if($r) { - foreach($r as $rv) { + + if (! $x) { + return $groups; + } + + // include xchans for all zot-like networks + + $xchans = q("select xchan_hash from xchan where xchan_hash = '%s' OR ( xchan_guid = '%s' AND xchan_pubkey = '%s' ) ", + dbesc($contact_id), + dbesc($x[0]['xchan_guid']), + dbesc($x[0]['xchan_pubkey']) + ); + + if($xchans) { + $hashes = ids_to_querystr($xchans,'xchan_hash',true); + } + + // private profiles are treated as a virtual group + + $r = q("SELECT abook_profile from abook where abook_xchan in ( " . protect_sprintf($hashes) . " ) and abook_profile != '' "); + if ($r) { + foreach ($r as $rv) { $groups[] = 'vp.' . $rv['abook_profile']; } } - // physical groups this channel is a member of + // physical groups this identity is a member of - $r = q("SELECT hash FROM pgrp left join pgrp_member on pgrp.id = pgrp_member.gid WHERE xchan = '%s' ", - dbesc($contact_id) - ); - if($r) { - foreach($r as $rr) + $r = q("SELECT hash FROM pgrp left join pgrp_member on pgrp.id = pgrp_member.gid WHERE xchan in ( " . protect_sprintf($hashes) . " ) "); + if ($r) { + foreach ($r as $rr) $groups[] = $rr['hash']; } return $groups; @@ -589,6 +671,70 @@ function init_groups_visitor($contact_id) { + +function get_security_ids($channel_id, $ob_hash) { + + $ret = [ + 'channel_id' => [], + 'allow_cid' => [], + 'allow_gid' => [] + ]; + + if($channel_id) { + $ch = q("select channel_hash, channel_portable_id from channel where channel_id = %d", + intval($channel_id) + ); + if($ch) { + $ret['channel_id'][] = $ch[0]['channel_hash']; + $ret['channel_id'][] = $ch[0]['channel_portable_id']; + } + } + + $groups = []; + + $x = q("select * from xchan where xchan_hash = '%s'", + dbesc($ob_hash) + ); + + if ($x) { + + // include xchans for all zot-like networks + + $xchans = q("select xchan_hash from xchan where xchan_hash = '%s' OR ( xchan_guid = '%s' AND xchan_pubkey = '%s' ) ", + dbesc($ob_hash), + dbesc($x[0]['xchan_guid']), + dbesc($x[0]['xchan_pubkey']) + ); + + if ($xchans) { + $ret['allow_cid'] = ids_to_array($xchans,'xchan_hash'); + $hashes = ids_to_querystr($xchans,'xchan_hash',true); + + // private profiles are treated as a virtual group + + $r = q("SELECT abook_profile from abook where abook_xchan in ( " . protect_sprintf($hashes) . " ) and abook_profile != '' "); + if($r) { + foreach ($r as $rv) { + $groups[] = 'vp.' . $rv['abook_profile']; + } + } + + // physical groups this identity is a member of + + $r = q("SELECT hash FROM pgrp left join pgrp_member on pgrp.id = pgrp_member.gid WHERE xchan in ( " . protect_sprintf($hashes) . " ) "); + if($r) { + foreach ($r as $rv) { + $groups[] = $rv['hash']; + } + } + $ret['allow_gid'] = $groups; + } + } + + return $ret; +} + + // This is used to determine which uid have posts which are visible to the logged in user (from the API) for the // public_timeline, and we can use this in a community page by making // $perms = (PERMS_NETWORK|PERMS_PUBLIC) unless logged in. diff --git a/include/taxonomy.php b/include/taxonomy.php index 46d95458c..b0304de5b 100644 --- a/include/taxonomy.php +++ b/include/taxonomy.php @@ -543,8 +543,6 @@ function article_catblock($uid,$count = 0,$authors = '',$owner = '', $flags = 0, function dir_tagblock($link,$r) { $o = ''; - $observer = get_observer_hash(); - if(! $r) $r = App::$data['directory_keywords']; diff --git a/include/text.php b/include/text.php index b017b038a..6a2a9e427 100644 --- a/include/text.php +++ b/include/text.php @@ -20,7 +20,7 @@ define('RANDOM_STRING_TEXT', 0x01 ); /** * @brief This is our template processor. * - * @param string|SmartyEngine $s the string requiring macro substitution, + * @param string SmartyEngine $s the string requiring macro substitution, * or an instance of SmartyEngine * @param array $r key value pairs (search => replace) * @@ -409,7 +409,8 @@ function autoname($len) { * @return string Escaped text. */ function xmlify($str) { - $buffer = ''; + + //$buffer = ''; if(is_array($str)) { @@ -418,7 +419,7 @@ function xmlify($str) { btlogger('xmlify called with array: ' . print_r($str,true), LOGGER_NORMAL, LOG_WARNING); } - +/* $len = mb_strlen($str); for($x = 0; $x < $len; $x ++) { $char = mb_substr($str,$x,1); @@ -452,6 +453,11 @@ function xmlify($str) { $buffer = trim($buffer); return($buffer); +*/ + $buffer = htmlspecialchars($str, ENT_QUOTES, "UTF-8"); + $buffer = trim($buffer); + return $buffer; + } /** @@ -464,9 +470,22 @@ function xmlify($str) { * @return string */ function unxmlify($s) { +/* $ret = str_replace('&', '&', $s); $ret = str_replace(array('<', '>', '"', '''), array('<', '>', '"', "'"), $ret); + return $ret; +*/ + + if(is_array($s)) { + + // allow to fall through so we ge a PHP error, as the log statement will + // probably get lost in the noise unless we're specifically looking for it. + + btlogger('unxmlify called with array: ' . print_r($s,true), LOGGER_NORMAL, LOG_WARNING); + } + + $ret = htmlspecialchars_decode($s, ENT_QUOTES); return $ret; } @@ -1682,7 +1701,12 @@ function prepare_body(&$item,$attach = false,$opts = false) { $s .= prepare_binary($item); } else { - $s .= prepare_text($item['body'],$item['mimetype'], $opts); + if($item['summary']) { + $s .= prepare_text('[summary]' . $item['summary'] . '[/summary]' . $item['body'],$item['mimetype'],$opts); + } + else { + $s .= prepare_text($item['body'],$item['mimetype'], $opts); + } } $event = (($item['obj_type'] === ACTIVITY_OBJ_EVENT) ? format_event_obj($item['obj']) : false); @@ -2545,15 +2569,6 @@ function design_tools() { */ function website_portation_tools() { - $channel = App::get_channel(); - $sys = false; - - if(App::$is_sys && is_site_admin()) { - require_once('include/channel.php'); - $channel = get_sys_channel(); - $sys = true; - } - return replace_macros(get_markup_template('website_portation_tools.tpl'), [ '$title' => t('Import'), '$import_label' => t('Import website...'), @@ -2721,7 +2736,6 @@ function handle_tag(&$body, &$str_tags, $profile_uid, $tag, $in_network = true) $name = substr($tag,(($exclusive) ? 2 : 1)); $newname = $name; // make a copy that we can mess with - $tagcid = 0; $r = null; @@ -2780,14 +2794,9 @@ function handle_tag(&$body, &$str_tags, $profile_uid, $tag, $in_network = true) } - - - - $fn_results = []; $access_tag = EMPTY_STR; - // $r is set if we found something if($r) { @@ -3081,13 +3090,12 @@ function item_url_replace($channel,&$item,$old,$new,$oldnick = '') { if($oldnick) json_url_replace('/' . $oldnick . '/' ,'/' . $channel['channel_address'] . '/' ,$item['target']); } + + $item['body'] = preg_replace("/(\[zrl=".preg_quote($old,'/')."\/(photos|gallery)\/".$channel['channel_address'].".+\]\[zmg=\d+x\d+\])".preg_quote($old,'/')."\/(.+\[\/zmg\])/", '${1}'.$new.'/${3}', $item['body']); + $item['body'] = preg_replace("/".preg_quote($old,'/')."\/(search|\w+\/".$channel['channel_address'].")/", $new.'/${1}', $item['body']); - $x = preg_replace("/".preg_quote($old,'/')."\/(search|\w+\/".$channel['channel_address'].")/", $new.'/${1}', $item['body']); - if($x) { - $item['body'] = $x; - $item['sig'] = base64url_encode(rsa_sign($item['body'],$channel['channel_prvkey'])); - $item['item_verified'] = 1; - } + $item['sig'] = base64url_encode(rsa_sign($item['body'],$channel['channel_prvkey'])); + $item['item_verified'] = 1; $item['plink'] = str_replace($old,$new,$item['plink']); if($oldnick) @@ -3241,7 +3249,7 @@ function flatten_array_recursive($arr) { $ret = array_merge($ret, $tmp); } } - elseif($a) { + elseif(isset($a)) { $ret[] = $a; } } diff --git a/include/xchan.php b/include/xchan.php index 4cbfb42c5..4fcdf9fce 100644 --- a/include/xchan.php +++ b/include/xchan.php @@ -1,6 +1,7 @@ $hubloc, - 'url' => \App::$query_string, + 'url' => App::$query_string, 'session' => $_SESSION ]; /** @@ -344,11 +356,61 @@ function owt_init($token) { */ call_hooks('magic_auth_success', $arr); - \App::set_observer($hubloc); + App::set_observer($hubloc); require_once('include/security.php'); - \App::set_groups(init_groups_visitor($_SESSION['visitor_id'])); + App::set_groups(init_groups_visitor($_SESSION['visitor_id'])); if(! get_config('system', 'hide_owa_greeting')) - info(sprintf( t('OpenWebAuth: %1$s welcomes %2$s'),\App::get_hostname(), $hubloc['xchan_name'])); + info(sprintf( t('OpenWebAuth: %1$s welcomes %2$s'),App::get_hostname(), $hubloc['xchan_name'])); logger('OpenWebAuth: auth success from ' . $hubloc['xchan_addr']); } + + +function observer_auth($ob_hash) { + + if($ob_hash === false) { + return; + } + + $r = q("select * from hubloc left join xchan on xchan_hash = hubloc_hash + where hubloc_addr = '%s' or hubloc_id_url = '%s' or hubloc_hash = '%s' order by hubloc_id desc", + dbesc($ob_hash), + dbesc($ob_hash), + dbesc($ob_hash) + ); + + if(! $r) { + // finger them if they can't be found. + $wf = discover_by_webbie($ob_hash); + if($wf) { + $r = q("select * from hubloc left join xchan on xchan_hash = hubloc_hash + where hubloc_addr = '%s' or hubloc_id_url = '%s' or hubloc_hash = '%s' order by hubloc_id desc", + dbesc($ob_hash), + dbesc($ob_hash), + dbesc($ob_hash) + ); + } + } + if(! $r) { + logger('unable to finger ' . $ob_hash); + return; + } + + // Note: this has no Libzot namespace so prefers zot over zot6 + + $hubloc = zot_record_preferred($r); + + $_SESSION['authenticated'] = 1; + + // normal visitor (remote_channel) login session credentials + $_SESSION['visitor_id'] = $hubloc['xchan_hash']; + $_SESSION['my_url'] = $hubloc['xchan_url']; + $_SESSION['my_address'] = $hubloc['hubloc_addr']; + $_SESSION['remote_hub'] = $hubloc['hubloc_url']; + $_SESSION['DNT'] = 1; + + App::set_observer($hubloc); + require_once('include/security.php'); + App::set_groups(init_groups_visitor($_SESSION['visitor_id'])); + +} diff --git a/include/zot.php b/include/zot.php index 3b089831b..b99eeb1ec 100644 --- a/include/zot.php +++ b/include/zot.php @@ -925,46 +925,62 @@ function import_xchan($arr, $ud_flags = UPDATE_FLAGS_UPDATED, $ud_arr = null) { $local = q("select channel_account_id, channel_id from channel where channel_hash = '%s' limit 1", dbesc($xchan_hash) ); + if($local) { + // @FIXME This should be removed in future when profile photo update by file sync procedure will be applied + // on most hubs in the network + // <--- $ph = z_fetch_url($arr['photo'], true); + if($ph['success']) { + + // Do not fetch already received thumbnails + $x = q("SELECT resource_id FROM photo WHERE uid = %d AND imgscale = %d AND filesize = %d LIMIT 1", + intval($local[0]['channel_id']), + intval(PHOTO_RES_PROFILE_300), + strlen($ph['body']) + ); - $hash = import_channel_photo($ph['body'], $arr['photo_mimetype'], $local[0]['channel_account_id'], $local[0]['channel_id']); + if($x) + $hash = $x[0]['resource_id']; + else + $hash = import_channel_photo($ph['body'], $arr['photo_mimetype'], $local[0]['channel_account_id'], $local[0]['channel_id']); + } + + if($hash) { + // unless proven otherwise + $is_default_profile = 1; - if($hash) { - // unless proven otherwise - $is_default_profile = 1; + $profile = q("select is_default from profile where aid = %d and uid = %d limit 1", + intval($local[0]['channel_account_id']), + intval($local[0]['channel_id']) + ); + if($profile) { + if(! intval($profile[0]['is_default'])) + $is_default_profile = 0; + } - $profile = q("select is_default from profile where aid = %d and uid = %d limit 1", + // If setting for the default profile, unset the profile photo flag from any other photos I own + if($is_default_profile) { + q("UPDATE photo SET photo_usage = %d WHERE photo_usage = %d AND resource_id != '%s' AND aid = %d AND uid = %d", + intval(PHOTO_NORMAL), + intval(PHOTO_PROFILE), + dbesc($hash), intval($local[0]['channel_account_id']), intval($local[0]['channel_id']) ); - if($profile) { - if(! intval($profile[0]['is_default'])) - $is_default_profile = 0; - } - - // If setting for the default profile, unset the profile photo flag from any other photos I own - if($is_default_profile) { - q("UPDATE photo SET photo_usage = %d WHERE photo_usage = %d AND resource_id != '%s' AND aid = %d AND uid = %d", - intval(PHOTO_NORMAL), - intval(PHOTO_PROFILE), - dbesc($hash), - intval($local[0]['channel_account_id']), - intval($local[0]['channel_id']) - ); - } } - - // reset the names in case they got messed up when we had a bug in this function - $photos = array( - z_root() . '/photo/profile/l/' . $local[0]['channel_id'], - z_root() . '/photo/profile/m/' . $local[0]['channel_id'], - z_root() . '/photo/profile/s/' . $local[0]['channel_id'], - $arr['photo_mimetype'], - false - ); } + // ---> + + // reset the names in case they got messed up when we had a bug in this function + $photos = array( + z_root() . '/photo/profile/l/' . $local[0]['channel_id'], + z_root() . '/photo/profile/m/' . $local[0]['channel_id'], + z_root() . '/photo/profile/s/' . $local[0]['channel_id'], + $arr['photo_mimetype'], + false + ); } else { $photos = import_xchan_photo($arr['photo'], $xchan_hash); @@ -1712,13 +1728,17 @@ function allowed_public_recips($msg) { $condensed_recips[] = $rr['hash']; $results = array(); - $r = q("select channel_hash as hash from channel left join abook on abook_channel = channel_id where abook_xchan = '%s' and channel_removed = 0 ", + $r = q("select channel_hash as hash, channel_id from channel left join abook on abook_channel = channel_id where abook_xchan = '%s' and channel_removed = 0 ", dbesc($hash) ); if($r) { - foreach($r as $rr) + foreach($r as $rr) { + $cfg = get_abconfig($rr['channel_id'],$rr['hash'],'their_perms','view_stream'); + if((! $cfg) && $scope !== 'any connections') + continue; if(in_array($rr['hash'],$condensed_recips)) $results[] = array('hash' => $rr['hash']); + } } return $results; } @@ -1840,7 +1860,7 @@ function process_delivery($sender, $arr, $deliveries, $relay, $public = false, $ intval($channel['channel_id']) ); if ($parent) { - $allowed = can_comment_on_post($d['hash'],$parent[0]); + $allowed = can_comment_on_post($sender['hash'],$parent[0]); } } @@ -3630,7 +3650,7 @@ function process_channel_sync_delivery($sender, $arr, $deliveries) { 'channel_r_storage', 'channel_r_pages', 'channel_w_stream', 'channel_w_wall', 'channel_w_comment', 'channel_w_mail', 'channel_w_like', 'channel_w_tagwall', 'channel_w_chat', 'channel_w_storage', 'channel_w_pages', 'channel_a_republish', - 'channel_a_delegate', 'channel_moved' + 'channel_a_delegate', 'channel_moved', 'channel_r_photos', 'channel_w_photos' ]; $clean = array(); @@ -5266,3 +5286,25 @@ function zot_reply_notify($data) { $ret['success'] = true; json_return_and_die($ret); } + + +function zot_record_preferred($arr, $check = 'hubloc_network') { + + if(! $arr) { + return $arr; + } + + foreach($arr as $v) { + if($v[$check] === 'zot') { + return $v; + } + } + foreach($arr as $v) { + if($v[$check] === 'zot6') { + return $v; + } + } + + return $arr[0]; + +} diff --git a/install/INSTALL.txt b/install/INSTALL.txt index fe2484d7a..0503ae2cc 100644 --- a/install/INSTALL.txt +++ b/install/INSTALL.txt @@ -175,7 +175,7 @@ encounter a database configuration which cannot be expressed on the setup form (for instance using MySQL with an unusual socket location); you can supply the PDO connection string as the database hostname. For instance - mysql:unix_socket=/my/special/socket_path + :/path/to/socket.file You should still fill in all other applicable form values as needed. diff --git a/install/database-w-defaults-v1131.diff b/install/database-w-defaults-v1131.diff deleted file mode 100644 index c8870f9c4..000000000 --- a/install/database-w-defaults-v1131.diff +++ /dev/null @@ -1,1914 +0,0 @@ -*** database.sql Mon Sep 8 20:28:00 2014 ---- database-w-defaults.sql Sun Nov 9 19:20:32 2014 -*************** -*** 1,15 **** -! SET SQL_MODE="NO_AUTO_VALUE_ON_ZERO"; -! -! /*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */; -! /*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */; -! /*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */; -! /*!40101 SET NAMES utf8 */; - - - CREATE TABLE IF NOT EXISTS `abook` ( - `abook_id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `abook_account` int(10) unsigned NOT NULL, -! `abook_channel` int(10) unsigned NOT NULL, - `abook_xchan` char(255) NOT NULL DEFAULT '', - `abook_my_perms` int(11) NOT NULL DEFAULT '0', - `abook_their_perms` int(11) NOT NULL DEFAULT '0', ---- 1,13 ---- -! -- -------------------------------------------------------- - -+ -- -+ -- Table structure for table `abook` -+ -- - - CREATE TABLE IF NOT EXISTS `abook` ( - `abook_id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `abook_account` int(10) unsigned NOT NULL DEFAULT '0', -! `abook_channel` int(10) unsigned NOT NULL DEFAULT '0', - `abook_xchan` char(255) NOT NULL DEFAULT '', - `abook_my_perms` int(11) NOT NULL DEFAULT '0', - `abook_their_perms` int(11) NOT NULL DEFAULT '0', -*************** -*** 37,42 **** ---- 35,46 ---- - KEY `abook_rating` (`abook_rating`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `account` -+ -- -+ - CREATE TABLE IF NOT EXISTS `account` ( - `account_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `account_parent` int(10) unsigned NOT NULL DEFAULT '0', -*************** -*** 70,79 **** - KEY `account_password_changed` (`account_password_changed`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `addon` ( - `id` int(11) NOT NULL AUTO_INCREMENT, -! `name` char(255) NOT NULL, -! `version` char(255) NOT NULL, - `installed` tinyint(1) NOT NULL DEFAULT '0', - `hidden` tinyint(1) NOT NULL DEFAULT '0', - `timestamp` bigint(20) NOT NULL DEFAULT '0', ---- 74,89 ---- - KEY `account_password_changed` (`account_password_changed`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `addon` -+ -- -+ - CREATE TABLE IF NOT EXISTS `addon` ( - `id` int(11) NOT NULL AUTO_INCREMENT, -! `name` char(255) NOT NULL DEFAULT '', -! `version` char(255) NOT NULL DEFAULT '', - `installed` tinyint(1) NOT NULL DEFAULT '0', - `hidden` tinyint(1) NOT NULL DEFAULT '0', - `timestamp` bigint(20) NOT NULL DEFAULT '0', -*************** -*** 84,89 **** ---- 94,104 ---- - KEY `installed` (`installed`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `app` -+ -- - - CREATE TABLE IF NOT EXISTS `app` ( - `id` int(11) NOT NULL AUTO_INCREMENT, -*************** -*** 108,115 **** - KEY `app_version` (`app_version`), - KEY `app_channel` (`app_channel`), - KEY `app_price` (`app_price`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - - CREATE TABLE IF NOT EXISTS `attach` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, ---- 123,135 ---- - KEY `app_version` (`app_version`), - KEY `app_channel` (`app_channel`), - KEY `app_price` (`app_price`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -! -! -- -------------------------------------------------------- - -+ -- -+ -- Table structure for table `attach` -+ -- - - CREATE TABLE IF NOT EXISTS `attach` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -*************** -*** 145,166 **** - KEY `creator` (`creator`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `auth_codes` ( -! `id` varchar(40) NOT NULL, -! `client_id` varchar(20) NOT NULL, -! `redirect_uri` varchar(200) NOT NULL, -! `expires` int(11) NOT NULL, -! `scope` varchar(250) NOT NULL, - PRIMARY KEY (`id`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `cache` ( -! `k` char(255) NOT NULL, - `v` text NOT NULL, -! `updated` datetime NOT NULL, - PRIMARY KEY (`k`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `channel` ( - `channel_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `channel_account_id` int(10) unsigned NOT NULL DEFAULT '0', ---- 165,204 ---- - KEY `creator` (`creator`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `auth_codes` -+ -- -+ - CREATE TABLE IF NOT EXISTS `auth_codes` ( -! `id` varchar(40) NOT NULL DEFAULT '', -! `client_id` varchar(20) NOT NULL DEFAULT '', -! `redirect_uri` varchar(200) NOT NULL DEFAULT '', -! `expires` int(11) NOT NULL DEFAULT '0', -! `scope` varchar(250) NOT NULL DEFAULT '', - PRIMARY KEY (`id`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `cache` -+ -- -+ - CREATE TABLE IF NOT EXISTS `cache` ( -! `k` char(255) NOT NULL DEFAULT '', - `v` text NOT NULL, -! `updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - PRIMARY KEY (`k`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `channel` -+ -- -+ - CREATE TABLE IF NOT EXISTS `channel` ( - `channel_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `channel_account_id` int(10) unsigned NOT NULL DEFAULT '0', -*************** -*** 189,212 **** - `channel_allow_gid` mediumtext NOT NULL, - `channel_deny_cid` mediumtext NOT NULL, - `channel_deny_gid` mediumtext NOT NULL, -! `channel_r_stream` int(10) unsigned NOT NULL DEFAULT '128', -! `channel_r_profile` int(10) unsigned NOT NULL DEFAULT '128', -! `channel_r_photos` int(10) unsigned NOT NULL DEFAULT '128', -! `channel_r_abook` int(10) unsigned NOT NULL DEFAULT '128', -! `channel_w_stream` int(10) unsigned NOT NULL DEFAULT '128', -! `channel_w_wall` int(10) unsigned NOT NULL DEFAULT '128', -! `channel_w_tagwall` int(10) unsigned NOT NULL DEFAULT '128', -! `channel_w_comment` int(10) unsigned NOT NULL DEFAULT '128', -! `channel_w_mail` int(10) unsigned NOT NULL DEFAULT '128', -! `channel_w_photos` int(10) unsigned NOT NULL DEFAULT '128', -! `channel_w_chat` int(10) unsigned NOT NULL DEFAULT '128', - `channel_a_delegate` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_r_storage` int(10) unsigned NOT NULL DEFAULT '128', -! `channel_w_storage` int(10) unsigned NOT NULL DEFAULT '128', -! `channel_r_pages` int(10) unsigned NOT NULL DEFAULT '128', -! `channel_w_pages` int(10) unsigned NOT NULL DEFAULT '128', -! `channel_a_republish` int(10) unsigned NOT NULL DEFAULT '128', -! `channel_w_like` int(10) unsigned NOT NULL DEFAULT '128', - PRIMARY KEY (`channel_id`), - UNIQUE KEY `channel_address_unique` (`channel_address`), - KEY `channel_account_id` (`channel_account_id`), ---- 227,250 ---- - `channel_allow_gid` mediumtext NOT NULL, - `channel_deny_cid` mediumtext NOT NULL, - `channel_deny_gid` mediumtext NOT NULL, -! `channel_r_stream` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_r_profile` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_r_photos` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_r_abook` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_w_stream` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_w_wall` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_w_tagwall` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_w_comment` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_w_mail` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_w_photos` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_w_chat` int(10) unsigned NOT NULL DEFAULT '0', - `channel_a_delegate` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_r_storage` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_w_storage` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_r_pages` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_w_pages` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_a_republish` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_w_like` int(10) unsigned NOT NULL DEFAULT '0', - PRIMARY KEY (`channel_id`), - UNIQUE KEY `channel_address_unique` (`channel_address`), - KEY `channel_account_id` (`channel_account_id`), -*************** -*** 241,250 **** - KEY `channel_w_pages` (`channel_w_pages`), - KEY `channel_deleted` (`channel_deleted`), - KEY `channel_a_republish` (`channel_a_republish`), -! KEY `channel_w_like` (`channel_w_like`), -! KEY `channel_dirdate` (`channel_dirdate`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `chat` ( - `chat_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `chat_room` int(10) unsigned NOT NULL DEFAULT '0', ---- 279,294 ---- - KEY `channel_w_pages` (`channel_w_pages`), - KEY `channel_deleted` (`channel_deleted`), - KEY `channel_a_republish` (`channel_a_republish`), -! KEY `channel_dirdate` (`channel_dirdate`), -! KEY `channel_w_like` (`channel_w_like`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `chat` -+ -- -+ - CREATE TABLE IF NOT EXISTS `chat` ( - `chat_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `chat_room` int(10) unsigned NOT NULL DEFAULT '0', -*************** -*** 255,275 **** - KEY `chat_room` (`chat_room`), - KEY `chat_xchan` (`chat_xchan`), - KEY `created` (`created`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `chatpresence` ( - `cp_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `cp_room` int(10) unsigned NOT NULL DEFAULT '0', - `cp_xchan` char(255) NOT NULL DEFAULT '', - `cp_last` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', -! `cp_status` char(255) NOT NULL, - `cp_client` char(128) NOT NULL DEFAULT '', - PRIMARY KEY (`cp_id`), - KEY `cp_room` (`cp_room`), - KEY `cp_xchan` (`cp_xchan`), - KEY `cp_last` (`cp_last`), - KEY `cp_status` (`cp_status`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `chatroom` ( - `cr_id` int(10) unsigned NOT NULL AUTO_INCREMENT, ---- 299,331 ---- - KEY `chat_room` (`chat_room`), - KEY `chat_xchan` (`chat_xchan`), - KEY `created` (`created`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -! -! -- -------------------------------------------------------- -! -! -- -! -- Table structure for table `chatpresence` -! -- - - CREATE TABLE IF NOT EXISTS `chatpresence` ( - `cp_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `cp_room` int(10) unsigned NOT NULL DEFAULT '0', - `cp_xchan` char(255) NOT NULL DEFAULT '', - `cp_last` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', -! `cp_status` char(255) NOT NULL DEFAULT '', - `cp_client` char(128) NOT NULL DEFAULT '', - PRIMARY KEY (`cp_id`), - KEY `cp_room` (`cp_room`), - KEY `cp_xchan` (`cp_xchan`), - KEY `cp_last` (`cp_last`), - KEY `cp_status` (`cp_status`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -! -! -- -------------------------------------------------------- -! -! -- -! -- Table structure for table `chatroom` -! -- - - CREATE TABLE IF NOT EXISTS `chatroom` ( - `cr_id` int(10) unsigned NOT NULL AUTO_INCREMENT, -*************** -*** 290,322 **** - KEY `cr_created` (`cr_created`), - KEY `cr_edited` (`cr_edited`), - KEY `cr_expire` (`cr_expire`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `clients` ( -! `client_id` varchar(20) NOT NULL, -! `pw` varchar(20) NOT NULL, -! `redirect_uri` varchar(200) NOT NULL, - `name` text, - `icon` text, - `uid` int(11) NOT NULL DEFAULT '0', - PRIMARY KEY (`client_id`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `config` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `cat` char(255) CHARACTER SET ascii NOT NULL, -! `k` char(255) CHARACTER SET ascii NOT NULL, - `v` text NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `access` (`cat`,`k`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `conv` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `guid` char(255) NOT NULL, - `recips` mediumtext NOT NULL, -! `uid` int(11) NOT NULL, -! `creator` char(255) NOT NULL, - `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `subject` mediumtext NOT NULL, ---- 346,396 ---- - KEY `cr_created` (`cr_created`), - KEY `cr_edited` (`cr_edited`), - KEY `cr_expire` (`cr_expire`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -! -! -- -------------------------------------------------------- -! -! -- -! -- Table structure for table `clients` -! -- - - CREATE TABLE IF NOT EXISTS `clients` ( -! `client_id` varchar(20) NOT NULL DEFAULT '', -! `pw` varchar(20) NOT NULL DEFAULT '', -! `redirect_uri` varchar(200) NOT NULL DEFAULT '', - `name` text, - `icon` text, - `uid` int(11) NOT NULL DEFAULT '0', - PRIMARY KEY (`client_id`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `config` -+ -- -+ - CREATE TABLE IF NOT EXISTS `config` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `cat` char(255) CHARACTER SET ascii NOT NULL DEFAULT '', -! `k` char(255) CHARACTER SET ascii NOT NULL DEFAULT '', - `v` text NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `access` (`cat`,`k`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `conv` -+ -- -+ - CREATE TABLE IF NOT EXISTS `conv` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `guid` char(255) NOT NULL DEFAULT '', - `recips` mediumtext NOT NULL, -! `uid` int(11) NOT NULL DEFAULT '0', -! `creator` char(255) NOT NULL DEFAULT '', - `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `subject` mediumtext NOT NULL, -*************** -*** 325,344 **** - KEY `updated` (`updated`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `event` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `aid` int(10) unsigned NOT NULL DEFAULT '0', -! `uid` int(11) NOT NULL, - `event_xchan` char(255) NOT NULL DEFAULT '', - `event_hash` char(255) NOT NULL DEFAULT '', -! `created` datetime NOT NULL, -! `edited` datetime NOT NULL, -! `start` datetime NOT NULL, -! `finish` datetime NOT NULL, - `summary` text NOT NULL, - `description` text NOT NULL, - `location` text NOT NULL, -! `type` char(255) NOT NULL, - `nofinish` tinyint(1) NOT NULL DEFAULT '0', - `adjust` tinyint(1) NOT NULL DEFAULT '1', - `ignore` tinyint(1) NOT NULL DEFAULT '0', ---- 399,424 ---- - KEY `updated` (`updated`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `event` -+ -- -+ - CREATE TABLE IF NOT EXISTS `event` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `aid` int(10) unsigned NOT NULL DEFAULT '0', -! `uid` int(11) NOT NULL DEFAULT '0', - `event_xchan` char(255) NOT NULL DEFAULT '', - `event_hash` char(255) NOT NULL DEFAULT '', -! `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', -! `edited` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', -! `start` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', -! `finish` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `summary` text NOT NULL, - `description` text NOT NULL, - `location` text NOT NULL, -! `type` char(255) NOT NULL DEFAULT '', - `nofinish` tinyint(1) NOT NULL DEFAULT '0', - `adjust` tinyint(1) NOT NULL DEFAULT '1', - `ignore` tinyint(1) NOT NULL DEFAULT '0', -*************** -*** 357,363 **** - KEY `aid` (`aid`), - KEY `event_hash` (`event_hash`), - KEY `event_xchan` (`event_xchan`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `fcontact` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, ---- 437,449 ---- - KEY `aid` (`aid`), - KEY `event_hash` (`event_hash`), - KEY `event_xchan` (`event_xchan`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -! -! -- -------------------------------------------------------- -! -! -- -! -- Table structure for table `fcontact` -! -- - - CREATE TABLE IF NOT EXISTS `fcontact` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -*************** -*** 381,386 **** ---- 467,478 ---- - KEY `network` (`network`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `ffinder` -+ -- -+ - CREATE TABLE IF NOT EXISTS `ffinder` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `uid` int(10) unsigned NOT NULL, -*************** -*** 392,427 **** - KEY `fid` (`fid`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `fserver` ( - `id` int(11) NOT NULL AUTO_INCREMENT, -! `server` char(255) NOT NULL, -! `posturl` char(255) NOT NULL, - `key` text NOT NULL, - PRIMARY KEY (`id`), - KEY `server` (`server`), - KEY `posturl` (`posturl`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `fsuggest` ( - `id` int(11) NOT NULL AUTO_INCREMENT, -! `uid` int(11) NOT NULL, -! `cid` int(11) NOT NULL, -! `name` char(255) NOT NULL, -! `url` char(255) NOT NULL, -! `request` char(255) NOT NULL, -! `photo` char(255) NOT NULL, - `note` text NOT NULL, -! `created` datetime NOT NULL, - PRIMARY KEY (`id`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `groups` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `hash` char(255) NOT NULL DEFAULT '', -! `uid` int(10) unsigned NOT NULL, - `visible` tinyint(1) NOT NULL DEFAULT '0', - `deleted` tinyint(1) NOT NULL DEFAULT '0', -! `name` char(255) NOT NULL, - PRIMARY KEY (`id`), - KEY `uid` (`uid`), - KEY `visible` (`visible`), ---- 484,537 ---- - KEY `fid` (`fid`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `fserver` -+ -- -+ - CREATE TABLE IF NOT EXISTS `fserver` ( - `id` int(11) NOT NULL AUTO_INCREMENT, -! `server` char(255) NOT NULL DEFAULT '', -! `posturl` char(255) NOT NULL DEFAULT '', - `key` text NOT NULL, - PRIMARY KEY (`id`), - KEY `server` (`server`), - KEY `posturl` (`posturl`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `fsuggest` -+ -- -+ - CREATE TABLE IF NOT EXISTS `fsuggest` ( - `id` int(11) NOT NULL AUTO_INCREMENT, -! `uid` int(11) NOT NULL DEFAULT '0', -! `cid` int(11) NOT NULL DEFAULT '0', -! `name` char(255) NOT NULL DEFAULT '', -! `url` char(255) NOT NULL DEFAULT '', -! `request` char(255) NOT NULL DEFAULT '', -! `photo` char(255) NOT NULL DEFAULT '', - `note` text NOT NULL, -! `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - PRIMARY KEY (`id`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `groups` -+ -- -+ - CREATE TABLE IF NOT EXISTS `groups` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `hash` char(255) NOT NULL DEFAULT '', -! `uid` int(10) unsigned NOT NULL DEFAULT '0', - `visible` tinyint(1) NOT NULL DEFAULT '0', - `deleted` tinyint(1) NOT NULL DEFAULT '0', -! `name` char(255) NOT NULL DEFAULT '', - PRIMARY KEY (`id`), - KEY `uid` (`uid`), - KEY `visible` (`visible`), -*************** -*** 429,438 **** - KEY `hash` (`hash`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `group_member` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `uid` int(10) unsigned NOT NULL, -! `gid` int(10) unsigned NOT NULL, - `xchan` char(255) NOT NULL DEFAULT '', - PRIMARY KEY (`id`), - KEY `uid` (`uid`), ---- 539,554 ---- - KEY `hash` (`hash`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `group_member` -+ -- -+ - CREATE TABLE IF NOT EXISTS `group_member` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `uid` int(10) unsigned NOT NULL DEFAULT '0', -! `gid` int(10) unsigned NOT NULL DEFAULT '0', - `xchan` char(255) NOT NULL DEFAULT '', - PRIMARY KEY (`id`), - KEY `uid` (`uid`), -*************** -*** 440,460 **** - KEY `xchan` (`xchan`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `hook` ( - `id` int(11) NOT NULL AUTO_INCREMENT, -! `hook` char(255) NOT NULL, -! `file` char(255) NOT NULL, -! `function` char(255) NOT NULL, - `priority` int(11) unsigned NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - KEY `hook` (`hook`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `hubloc` ( - `hubloc_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `hubloc_guid` char(255) NOT NULL DEFAULT '', - `hubloc_guid_sig` text NOT NULL, -! `hubloc_hash` char(255) NOT NULL, - `hubloc_addr` char(255) NOT NULL DEFAULT '', - `hubloc_network` char(32) NOT NULL DEFAULT '', - `hubloc_flags` int(10) unsigned NOT NULL DEFAULT '0', ---- 556,588 ---- - KEY `xchan` (`xchan`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `hook` -+ -- -+ - CREATE TABLE IF NOT EXISTS `hook` ( - `id` int(11) NOT NULL AUTO_INCREMENT, -! `hook` char(255) NOT NULL DEFAULT '', -! `file` char(255) NOT NULL DEFAULT '', -! `function` char(255) NOT NULL DEFAULT '', - `priority` int(11) unsigned NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - KEY `hook` (`hook`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `hubloc` -+ -- -+ - CREATE TABLE IF NOT EXISTS `hubloc` ( - `hubloc_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `hubloc_guid` char(255) NOT NULL DEFAULT '', - `hubloc_guid_sig` text NOT NULL, -! `hubloc_hash` char(255) NOT NULL DEFAULT '', - `hubloc_addr` char(255) NOT NULL DEFAULT '', - `hubloc_network` char(32) NOT NULL DEFAULT '', - `hubloc_flags` int(10) unsigned NOT NULL DEFAULT '0', -*************** -*** 474,493 **** - KEY `hubloc_connect` (`hubloc_connect`), - KEY `hubloc_host` (`hubloc_host`), - KEY `hubloc_addr` (`hubloc_addr`), -- KEY `hubloc_network` (`hubloc_network`), - KEY `hubloc_updated` (`hubloc_updated`), - KEY `hubloc_connected` (`hubloc_connected`), -! KEY `hubloc_status` (`hubloc_status`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `issue` ( - `issue_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `issue_created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `issue_updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', -! `issue_assigned` char(255) NOT NULL, -! `issue_priority` int(11) NOT NULL, -! `issue_status` int(11) NOT NULL, -! `issue_component` char(255) NOT NULL, - PRIMARY KEY (`issue_id`), - KEY `issue_created` (`issue_created`), - KEY `issue_updated` (`issue_updated`), ---- 602,627 ---- - KEY `hubloc_connect` (`hubloc_connect`), - KEY `hubloc_host` (`hubloc_host`), - KEY `hubloc_addr` (`hubloc_addr`), - KEY `hubloc_updated` (`hubloc_updated`), - KEY `hubloc_connected` (`hubloc_connected`), -! KEY `hubloc_status` (`hubloc_status`), -! KEY `hubloc_network` (`hubloc_network`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `issue` -+ -- -+ - CREATE TABLE IF NOT EXISTS `issue` ( - `issue_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `issue_created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `issue_updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', -! `issue_assigned` char(255) NOT NULL DEFAULT '', -! `issue_priority` int(11) NOT NULL DEFAULT '0', -! `issue_status` int(11) NOT NULL DEFAULT '0', -! `issue_component` char(255) NOT NULL DEFAULT '', - PRIMARY KEY (`issue_id`), - KEY `issue_created` (`issue_created`), - KEY `issue_updated` (`issue_updated`), -*************** -*** 497,502 **** ---- 631,642 ---- - KEY `issue_component` (`issue_component`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `item` -+ -- -+ - CREATE TABLE IF NOT EXISTS `item` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `mid` char(255) CHARACTER SET ascii NOT NULL DEFAULT '', -*************** -*** 516,549 **** - `author_xchan` char(255) NOT NULL DEFAULT '', - `source_xchan` char(255) NOT NULL DEFAULT '', - `mimetype` char(255) NOT NULL DEFAULT '', -! `title` text NOT NULL DEFAULT '', -! `body` mediumtext NOT NULL DEFAULT '', - `app` char(255) NOT NULL DEFAULT '', - `lang` char(64) NOT NULL DEFAULT '', - `revision` int(10) unsigned NOT NULL DEFAULT '0', - `verb` char(255) NOT NULL DEFAULT '', - `obj_type` char(255) NOT NULL DEFAULT '', -! `object` text NOT NULL DEFAULT '', - `tgt_type` char(255) NOT NULL DEFAULT '', -! `target` text NOT NULL DEFAULT '', - `layout_mid` char(255) NOT NULL DEFAULT '', -! `postopts` text NOT NULL DEFAULT '', -! `route` text NOT NULL DEFAULT '', - `llink` char(255) NOT NULL DEFAULT '', - `plink` char(255) NOT NULL DEFAULT '', - `resource_id` char(255) NOT NULL DEFAULT '', - `resource_type` char(16) NOT NULL DEFAULT '', -! `attach` mediumtext NOT NULL DEFAULT '', -! `sig` text NOT NULL DEFAULT '', -! `diaspora_meta` mediumtext NOT NULL DEFAULT '', - `location` char(255) NOT NULL DEFAULT '', - `coord` char(255) NOT NULL DEFAULT '', - `public_policy` char(255) NOT NULL DEFAULT '', - `comment_policy` char(255) NOT NULL DEFAULT '', -! `allow_cid` mediumtext NOT NULL DEFAULT '', -! `allow_gid` mediumtext NOT NULL DEFAULT '', -! `deny_cid` mediumtext NOT NULL DEFAULT '', -! `deny_gid` mediumtext NOT NULL DEFAULT '', - `item_restrict` int(11) NOT NULL DEFAULT '0', - `item_flags` int(11) NOT NULL DEFAULT '0', - `item_private` tinyint(4) NOT NULL DEFAULT '0', ---- 656,689 ---- - `author_xchan` char(255) NOT NULL DEFAULT '', - `source_xchan` char(255) NOT NULL DEFAULT '', - `mimetype` char(255) NOT NULL DEFAULT '', -! `title` text NOT NULL, -! `body` mediumtext NOT NULL, - `app` char(255) NOT NULL DEFAULT '', - `lang` char(64) NOT NULL DEFAULT '', - `revision` int(10) unsigned NOT NULL DEFAULT '0', - `verb` char(255) NOT NULL DEFAULT '', - `obj_type` char(255) NOT NULL DEFAULT '', -! `object` text NOT NULL, - `tgt_type` char(255) NOT NULL DEFAULT '', -! `target` text NOT NULL, - `layout_mid` char(255) NOT NULL DEFAULT '', -! `postopts` text NOT NULL, -! `route` text NOT NULL, - `llink` char(255) NOT NULL DEFAULT '', - `plink` char(255) NOT NULL DEFAULT '', - `resource_id` char(255) NOT NULL DEFAULT '', - `resource_type` char(16) NOT NULL DEFAULT '', -! `attach` mediumtext NOT NULL, -! `sig` text NOT NULL, -! `diaspora_meta` mediumtext NOT NULL, - `location` char(255) NOT NULL DEFAULT '', - `coord` char(255) NOT NULL DEFAULT '', - `public_policy` char(255) NOT NULL DEFAULT '', - `comment_policy` char(255) NOT NULL DEFAULT '', -! `allow_cid` mediumtext NOT NULL, -! `allow_gid` mediumtext NOT NULL, -! `deny_cid` mediumtext NOT NULL, -! `deny_gid` mediumtext NOT NULL, - `item_restrict` int(11) NOT NULL DEFAULT '0', - `item_flags` int(11) NOT NULL DEFAULT '0', - `item_private` tinyint(4) NOT NULL DEFAULT '0', -*************** -*** 555,562 **** - KEY `received` (`received`), - KEY `uid_commented` (`uid`,`commented`), - KEY `uid_created` (`uid`,`created`), -- KEY `changed` (`changed`), -- KEY `comments_closed` (`comments_closed`), - KEY `aid` (`aid`), - KEY `owner_xchan` (`owner_xchan`), - KEY `author_xchan` (`author_xchan`), ---- 695,700 ---- -*************** -*** 573,581 **** - KEY `mid` (`mid`), - KEY `parent_mid` (`parent_mid`), - KEY `uid_mid` (`mid`,`uid`), -- KEY `public_policy` (`public_policy`), - KEY `comment_policy` (`comment_policy`), - KEY `layout_mid` (`layout_mid`), - FULLTEXT KEY `title` (`title`), - FULLTEXT KEY `body` (`body`), - FULLTEXT KEY `allow_cid` (`allow_cid`), ---- 711,721 ---- - KEY `mid` (`mid`), - KEY `parent_mid` (`parent_mid`), - KEY `uid_mid` (`mid`,`uid`), - KEY `comment_policy` (`comment_policy`), - KEY `layout_mid` (`layout_mid`), -+ KEY `public_policy` (`public_policy`), -+ KEY `comments_closed` (`comments_closed`), -+ KEY `changed` (`changed`), - FULLTEXT KEY `title` (`title`), - FULLTEXT KEY `body` (`body`), - FULLTEXT KEY `allow_cid` (`allow_cid`), -*************** -*** 584,595 **** - FULLTEXT KEY `deny_gid` (`deny_gid`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `item_id` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `iid` int(11) NOT NULL, -! `uid` int(11) NOT NULL, -! `sid` char(255) NOT NULL, -! `service` char(255) NOT NULL, - PRIMARY KEY (`id`), - KEY `uid` (`uid`), - KEY `sid` (`sid`), ---- 724,741 ---- - FULLTEXT KEY `deny_gid` (`deny_gid`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `item_id` -+ -- -+ - CREATE TABLE IF NOT EXISTS `item_id` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `iid` int(11) NOT NULL DEFAULT '0', -! `uid` int(11) NOT NULL DEFAULT '0', -! `sid` char(255) NOT NULL DEFAULT '', -! `service` char(255) NOT NULL DEFAULT '', - PRIMARY KEY (`id`), - KEY `uid` (`uid`), - KEY `sid` (`sid`), -*************** -*** 597,621 **** - KEY `iid` (`iid`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `likes` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `channel_id` int(11) unsigned NOT NULL DEFAULT '0', - `liker` char(128) NOT NULL DEFAULT '', - `likee` char(128) NOT NULL DEFAULT '', -! `iid` int(10) unsigned NOT NULL DEFAULT '0', - `verb` char(255) NOT NULL DEFAULT '', - `target_type` char(255) NOT NULL DEFAULT '', - `target_id` char(128) NOT NULL DEFAULT '', - `target` mediumtext NOT NULL, - PRIMARY KEY (`id`), -- KEY `channel_id` (`channel_id`), - KEY `liker` (`liker`), - KEY `likee` (`likee`), - KEY `iid` (`iid`), - KEY `verb` (`verb`), - KEY `target_type` (`target_type`), - KEY `target_id` (`target_id`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `mail` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, ---- 743,779 ---- - KEY `iid` (`iid`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `likes` -+ -- -+ - CREATE TABLE IF NOT EXISTS `likes` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `channel_id` int(10) unsigned NOT NULL DEFAULT '0', - `liker` char(128) NOT NULL DEFAULT '', - `likee` char(128) NOT NULL DEFAULT '', -! `iid` int(11) unsigned NOT NULL DEFAULT '0', - `verb` char(255) NOT NULL DEFAULT '', - `target_type` char(255) NOT NULL DEFAULT '', - `target_id` char(128) NOT NULL DEFAULT '', - `target` mediumtext NOT NULL, - PRIMARY KEY (`id`), - KEY `liker` (`liker`), - KEY `likee` (`likee`), - KEY `iid` (`iid`), - KEY `verb` (`verb`), - KEY `target_type` (`target_type`), -+ KEY `channel_id` (`channel_id`), - KEY `target_id` (`target_id`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -! -! -- -------------------------------------------------------- -! -! -- -! -- Table structure for table `mail` -! -- - - CREATE TABLE IF NOT EXISTS `mail` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -*************** -*** 624,639 **** - `from_xchan` char(255) NOT NULL DEFAULT '', - `to_xchan` char(255) NOT NULL DEFAULT '', - `account_id` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_id` int(10) unsigned NOT NULL, - `title` text NOT NULL, - `body` mediumtext NOT NULL, - `attach` mediumtext NOT NULL, -! `mid` char(255) NOT NULL, -! `parent_mid` char(255) NOT NULL, - `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `expires` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - PRIMARY KEY (`id`), -- KEY `convid` (`convid`), - KEY `created` (`created`), - KEY `mail_flags` (`mail_flags`), - KEY `account_id` (`account_id`), ---- 782,796 ---- - `from_xchan` char(255) NOT NULL DEFAULT '', - `to_xchan` char(255) NOT NULL DEFAULT '', - `account_id` int(10) unsigned NOT NULL DEFAULT '0', -! `channel_id` int(10) unsigned NOT NULL DEFAULT '0', - `title` text NOT NULL, - `body` mediumtext NOT NULL, - `attach` mediumtext NOT NULL, -! `mid` char(255) NOT NULL DEFAULT '', -! `parent_mid` char(255) NOT NULL DEFAULT '', - `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `expires` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - PRIMARY KEY (`id`), - KEY `created` (`created`), - KEY `mail_flags` (`mail_flags`), - KEY `account_id` (`account_id`), -*************** -*** 642,659 **** - KEY `to_xchan` (`to_xchan`), - KEY `mid` (`mid`), - KEY `parent_mid` (`parent_mid`), -! KEY `expires` (`expires`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `manage` ( - `id` int(11) NOT NULL AUTO_INCREMENT, -! `uid` int(11) NOT NULL, - `xchan` char(255) NOT NULL DEFAULT '', - PRIMARY KEY (`id`), - KEY `uid` (`uid`), - KEY `xchan` (`xchan`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `menu` ( - `menu_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `menu_channel_id` int(10) unsigned NOT NULL DEFAULT '0', ---- 799,829 ---- - KEY `to_xchan` (`to_xchan`), - KEY `mid` (`mid`), - KEY `parent_mid` (`parent_mid`), -! KEY `expires` (`expires`), -! KEY `convid` (`convid`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `manage` -+ -- -+ - CREATE TABLE IF NOT EXISTS `manage` ( - `id` int(11) NOT NULL AUTO_INCREMENT, -! `uid` int(11) NOT NULL DEFAULT '0', - `xchan` char(255) NOT NULL DEFAULT '', - PRIMARY KEY (`id`), - KEY `uid` (`uid`), - KEY `xchan` (`xchan`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `menu` -+ -- -+ - CREATE TABLE IF NOT EXISTS `menu` ( - `menu_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `menu_channel_id` int(10) unsigned NOT NULL DEFAULT '0', -*************** -*** 666,671 **** ---- 836,847 ---- - KEY `menu_flags` (`menu_flags`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `menu_item` -+ -- -+ - CREATE TABLE IF NOT EXISTS `menu_item` ( - `mitem_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `mitem_link` char(255) NOT NULL DEFAULT '', -*************** -*** 675,681 **** - `allow_gid` mediumtext NOT NULL, - `deny_cid` mediumtext NOT NULL, - `deny_gid` mediumtext NOT NULL, -! `mitem_channel_id` int(10) unsigned NOT NULL, - `mitem_menu_id` int(10) unsigned NOT NULL DEFAULT '0', - `mitem_order` int(11) NOT NULL DEFAULT '0', - PRIMARY KEY (`mitem_id`), ---- 851,857 ---- - `allow_gid` mediumtext NOT NULL, - `deny_cid` mediumtext NOT NULL, - `deny_gid` mediumtext NOT NULL, -! `mitem_channel_id` int(10) unsigned NOT NULL DEFAULT '0', - `mitem_menu_id` int(10) unsigned NOT NULL DEFAULT '0', - `mitem_order` int(11) NOT NULL DEFAULT '0', - PRIMARY KEY (`mitem_id`), -*************** -*** 684,705 **** - KEY `mitem_flags` (`mitem_flags`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `notify` ( - `id` int(11) NOT NULL AUTO_INCREMENT, -! `hash` char(64) NOT NULL, -! `name` char(255) NOT NULL, -! `url` char(255) NOT NULL, -! `photo` char(255) NOT NULL, -! `date` datetime NOT NULL, - `msg` mediumtext NOT NULL, -! `aid` int(11) NOT NULL, -! `uid` int(11) NOT NULL, -! `link` char(255) NOT NULL, - `parent` char(255) NOT NULL DEFAULT '', - `seen` tinyint(1) NOT NULL DEFAULT '0', -! `type` int(11) NOT NULL, -! `verb` char(255) NOT NULL, -! `otype` char(16) NOT NULL, - PRIMARY KEY (`id`), - KEY `type` (`type`), - KEY `seen` (`seen`), ---- 860,887 ---- - KEY `mitem_flags` (`mitem_flags`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `notify` -+ -- -+ - CREATE TABLE IF NOT EXISTS `notify` ( - `id` int(11) NOT NULL AUTO_INCREMENT, -! `hash` char(64) NOT NULL DEFAULT '', -! `name` char(255) NOT NULL DEFAULT '', -! `url` char(255) NOT NULL DEFAULT '', -! `photo` char(255) NOT NULL DEFAULT '', -! `date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `msg` mediumtext NOT NULL, -! `aid` int(11) NOT NULL DEFAULT '0', -! `uid` int(11) NOT NULL DEFAULT '0', -! `link` char(255) NOT NULL DEFAULT '', - `parent` char(255) NOT NULL DEFAULT '', - `seen` tinyint(1) NOT NULL DEFAULT '0', -! `type` int(11) NOT NULL DEFAULT '0', -! `verb` char(255) NOT NULL DEFAULT '', -! `otype` char(16) NOT NULL DEFAULT '', - PRIMARY KEY (`id`), - KEY `type` (`type`), - KEY `seen` (`seen`), -*************** -*** 712,717 **** ---- 894,905 ---- - KEY `aid` (`aid`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `obj` -+ -- -+ - CREATE TABLE IF NOT EXISTS `obj` ( - `obj_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `obj_page` char(64) NOT NULL DEFAULT '', -*************** -*** 731,736 **** ---- 919,930 ---- - KEY `obj_obj` (`obj_obj`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `outq` -+ -- -+ - CREATE TABLE IF NOT EXISTS `outq` ( - `outq_hash` char(255) NOT NULL, - `outq_account` int(10) unsigned NOT NULL DEFAULT '0', -*************** -*** 753,786 **** - KEY `outq_delivered` (`outq_delivered`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `pconfig` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `uid` int(11) NOT NULL DEFAULT '0', -! `cat` char(255) CHARACTER SET ascii NOT NULL, -! `k` char(255) CHARACTER SET ascii NOT NULL, - `v` mediumtext NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `access` (`uid`,`cat`,`k`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `photo` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `aid` int(10) unsigned NOT NULL DEFAULT '0', -! `uid` int(10) unsigned NOT NULL, - `xchan` char(255) NOT NULL DEFAULT '', -! `resource_id` char(255) NOT NULL, -! `created` datetime NOT NULL, -! `edited` datetime NOT NULL, -! `title` char(255) NOT NULL, - `description` text NOT NULL, -! `album` char(255) NOT NULL, -! `filename` char(255) NOT NULL, - `type` char(128) NOT NULL DEFAULT 'image/jpeg', -! `height` smallint(6) NOT NULL, -! `width` smallint(6) NOT NULL, - `size` int(10) unsigned NOT NULL DEFAULT '0', - `data` mediumblob NOT NULL, -! `scale` tinyint(3) NOT NULL, - `profile` tinyint(1) NOT NULL DEFAULT '0', - `photo_flags` int(10) unsigned NOT NULL DEFAULT '0', - `allow_cid` mediumtext NOT NULL, ---- 947,992 ---- - KEY `outq_delivered` (`outq_delivered`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `pconfig` -+ -- -+ - CREATE TABLE IF NOT EXISTS `pconfig` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `uid` int(11) NOT NULL DEFAULT '0', -! `cat` char(255) CHARACTER SET ascii NOT NULL DEFAULT '', -! `k` char(255) CHARACTER SET ascii NOT NULL DEFAULT '', - `v` mediumtext NOT NULL, - PRIMARY KEY (`id`), - UNIQUE KEY `access` (`uid`,`cat`,`k`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `photo` -+ -- -+ - CREATE TABLE IF NOT EXISTS `photo` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `aid` int(10) unsigned NOT NULL DEFAULT '0', -! `uid` int(10) unsigned NOT NULL DEFAULT '0', - `xchan` char(255) NOT NULL DEFAULT '', -! `resource_id` char(255) NOT NULL DEFAULT '', -! `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', -! `edited` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', -! `title` char(255) NOT NULL DEFAULT '', - `description` text NOT NULL, -! `album` char(255) NOT NULL DEFAULT '', -! `filename` char(255) NOT NULL DEFAULT '', - `type` char(128) NOT NULL DEFAULT 'image/jpeg', -! `height` smallint(6) NOT NULL DEFAULT '0', -! `width` smallint(6) NOT NULL DEFAULT '0', - `size` int(10) unsigned NOT NULL DEFAULT '0', - `data` mediumblob NOT NULL, -! `scale` tinyint(3) NOT NULL DEFAULT '0', - `profile` tinyint(1) NOT NULL DEFAULT '0', - `photo_flags` int(10) unsigned NOT NULL DEFAULT '0', - `allow_cid` mediumtext NOT NULL, -*************** -*** 800,805 **** ---- 1006,1017 ---- - KEY `resource_id` (`resource_id`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `poll` -+ -- -+ - CREATE TABLE IF NOT EXISTS `poll` ( - `poll_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `poll_channel` int(10) unsigned NOT NULL DEFAULT '0', -*************** -*** 812,817 **** ---- 1024,1035 ---- - KEY `poll_votes` (`poll_votes`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `poll_elm` -+ -- -+ - CREATE TABLE IF NOT EXISTS `poll_elm` ( - `pelm_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `pelm_poll` int(10) unsigned NOT NULL DEFAULT '0', -*************** -*** 823,838 **** - KEY `pelm_result` (`pelm_result`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `profdef` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `field_name` char(255) NOT NULL DEFAULT '', - `field_type` char(16) NOT NULL DEFAULT '', - `field_desc` char(255) NOT NULL DEFAULT '', - `field_help` char(255) NOT NULL DEFAULT '', -! `field_inputs` mediumtext NOT NULL DEFAULT '', - PRIMARY KEY (`id`), - KEY `field_name` (`field_name`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `profext` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, ---- 1041,1068 ---- - KEY `pelm_result` (`pelm_result`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `profdef` -+ -- -+ - CREATE TABLE IF NOT EXISTS `profdef` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `field_name` char(255) NOT NULL DEFAULT '', - `field_type` char(16) NOT NULL DEFAULT '', - `field_desc` char(255) NOT NULL DEFAULT '', - `field_help` char(255) NOT NULL DEFAULT '', -! `field_inputs` mediumtext NOT NULL, - PRIMARY KEY (`id`), - KEY `field_name` (`field_name`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -! -! -- -------------------------------------------------------- -! -! -- -! -- Table structure for table `profext` -! -- - - CREATE TABLE IF NOT EXISTS `profext` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -*************** -*** 844,882 **** - KEY `channel_id` (`channel_id`), - KEY `hash` (`hash`), - KEY `k` (`k`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `profile` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `profile_guid` char(64) NOT NULL DEFAULT '', - `aid` int(10) unsigned NOT NULL DEFAULT '0', -! `uid` int(11) NOT NULL, -! `profile_name` char(255) NOT NULL, - `is_default` tinyint(1) NOT NULL DEFAULT '0', - `hide_friends` tinyint(1) NOT NULL DEFAULT '0', -! `name` char(255) NOT NULL, -! `pdesc` char(255) NOT NULL, - `chandesc` text NOT NULL, - `dob` char(32) NOT NULL DEFAULT '0000-00-00', - `dob_tz` char(255) NOT NULL DEFAULT 'UTC', -! `address` char(255) NOT NULL, -! `locality` char(255) NOT NULL, -! `region` char(255) NOT NULL, -! `postal_code` char(32) NOT NULL, -! `country_name` char(255) NOT NULL, -! `hometown` char(255) NOT NULL, -! `gender` char(32) NOT NULL, -! `marital` char(255) NOT NULL, - `with` text NOT NULL, - `howlong` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', -! `sexual` char(255) NOT NULL, -! `politic` char(255) NOT NULL, -! `religion` char(255) NOT NULL, - `keywords` text NOT NULL, - `likes` text NOT NULL, - `dislikes` text NOT NULL, - `about` text NOT NULL, -! `summary` char(255) NOT NULL, - `music` text NOT NULL, - `book` text NOT NULL, - `tv` text NOT NULL, ---- 1074,1118 ---- - KEY `channel_id` (`channel_id`), - KEY `hash` (`hash`), - KEY `k` (`k`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -! -! -- -------------------------------------------------------- -! -! -- -! -- Table structure for table `profile` -! -- - - CREATE TABLE IF NOT EXISTS `profile` ( - `id` int(11) NOT NULL AUTO_INCREMENT, - `profile_guid` char(64) NOT NULL DEFAULT '', - `aid` int(10) unsigned NOT NULL DEFAULT '0', -! `uid` int(11) NOT NULL DEFAULT '0', -! `profile_name` char(255) NOT NULL DEFAULT '', - `is_default` tinyint(1) NOT NULL DEFAULT '0', - `hide_friends` tinyint(1) NOT NULL DEFAULT '0', -! `name` char(255) NOT NULL DEFAULT '', -! `pdesc` char(255) NOT NULL DEFAULT '', - `chandesc` text NOT NULL, - `dob` char(32) NOT NULL DEFAULT '0000-00-00', - `dob_tz` char(255) NOT NULL DEFAULT 'UTC', -! `address` char(255) NOT NULL DEFAULT '', -! `locality` char(255) NOT NULL DEFAULT '', -! `region` char(255) NOT NULL DEFAULT '', -! `postal_code` char(32) NOT NULL DEFAULT '', -! `country_name` char(255) NOT NULL DEFAULT '', -! `hometown` char(255) NOT NULL DEFAULT '', -! `gender` char(32) NOT NULL DEFAULT '', -! `marital` char(255) NOT NULL DEFAULT '', - `with` text NOT NULL, - `howlong` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', -! `sexual` char(255) NOT NULL DEFAULT '', -! `politic` char(255) NOT NULL DEFAULT '', -! `religion` char(255) NOT NULL DEFAULT '', - `keywords` text NOT NULL, - `likes` text NOT NULL, - `dislikes` text NOT NULL, - `about` text NOT NULL, -! `summary` char(255) NOT NULL DEFAULT '', - `music` text NOT NULL, - `book` text NOT NULL, - `tv` text NOT NULL, -*************** -*** 887,895 **** - `education` text NOT NULL, - `contact` text NOT NULL, - `channels` text NOT NULL, -! `homepage` char(255) NOT NULL, -! `photo` char(255) NOT NULL, -! `thumb` char(255) NOT NULL, - `publish` tinyint(1) NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - UNIQUE KEY `guid` (`profile_guid`,`uid`), ---- 1123,1131 ---- - `education` text NOT NULL, - `contact` text NOT NULL, - `channels` text NOT NULL, -! `homepage` char(255) NOT NULL DEFAULT '', -! `photo` char(255) NOT NULL DEFAULT '', -! `thumb` char(255) NOT NULL DEFAULT '', - `publish` tinyint(1) NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - UNIQUE KEY `guid` (`profile_guid`,`uid`), -*************** -*** 908,920 **** - KEY `profile_guid` (`profile_guid`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `profile_check` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `uid` int(10) unsigned NOT NULL, - `cid` int(10) unsigned NOT NULL DEFAULT '0', -! `dfrn_id` char(255) NOT NULL, -! `sec` char(255) NOT NULL, -! `expire` int(11) NOT NULL, - PRIMARY KEY (`id`), - KEY `uid` (`uid`), - KEY `cid` (`cid`), ---- 1144,1162 ---- - KEY `profile_guid` (`profile_guid`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `profile_check` -+ -- -+ - CREATE TABLE IF NOT EXISTS `profile_check` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `uid` int(10) unsigned NOT NULL DEFAULT '0', - `cid` int(10) unsigned NOT NULL DEFAULT '0', -! `dfrn_id` char(255) NOT NULL DEFAULT '', -! `sec` char(255) NOT NULL DEFAULT '', -! `expire` int(11) NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - KEY `uid` (`uid`), - KEY `cid` (`cid`), -*************** -*** 923,951 **** - KEY `expire` (`expire`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `register` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `hash` char(255) NOT NULL, -! `created` datetime NOT NULL, -! `uid` int(10) unsigned NOT NULL, -! `password` char(255) NOT NULL, -! `language` char(16) NOT NULL, - PRIMARY KEY (`id`), - KEY `hash` (`hash`), - KEY `created` (`created`), - KEY `uid` (`uid`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `session` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, -! `sid` char(255) NOT NULL, - `data` text NOT NULL, -! `expire` bigint(20) unsigned NOT NULL, - PRIMARY KEY (`id`), - KEY `sid` (`sid`), - KEY `expire` (`expire`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `shares` ( - `share_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `share_type` int(11) NOT NULL DEFAULT '0', ---- 1165,1211 ---- - KEY `expire` (`expire`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `register` -+ -- -+ - CREATE TABLE IF NOT EXISTS `register` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `hash` char(255) NOT NULL DEFAULT '', -! `created` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', -! `uid` int(10) unsigned NOT NULL DEFAULT '0', -! `password` char(255) NOT NULL DEFAULT '', -! `language` char(16) NOT NULL DEFAULT '', - PRIMARY KEY (`id`), - KEY `hash` (`hash`), - KEY `created` (`created`), - KEY `uid` (`uid`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -! -! -- -------------------------------------------------------- -! -! -- -! -- Table structure for table `session` -! -- - - CREATE TABLE IF NOT EXISTS `session` ( - `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, -! `sid` char(255) NOT NULL DEFAULT '', - `data` text NOT NULL, -! `expire` bigint(20) unsigned NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - KEY `sid` (`sid`), - KEY `expire` (`expire`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `shares` -+ -- -+ - CREATE TABLE IF NOT EXISTS `shares` ( - `share_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `share_type` int(11) NOT NULL DEFAULT '0', -*************** -*** 957,974 **** - KEY `share_xchan` (`share_xchan`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `sign` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `iid` int(10) unsigned NOT NULL DEFAULT '0', - `retract_iid` int(10) unsigned NOT NULL DEFAULT '0', - `signed_text` mediumtext NOT NULL, - `signature` text NOT NULL, -! `signer` char(255) NOT NULL, - PRIMARY KEY (`id`), - KEY `iid` (`iid`), - KEY `retract_iid` (`retract_iid`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `site` ( - `site_url` char(255) NOT NULL, - `site_access` int(11) NOT NULL DEFAULT '0', ---- 1217,1246 ---- - KEY `share_xchan` (`share_xchan`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `sign` -+ -- -+ - CREATE TABLE IF NOT EXISTS `sign` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `iid` int(10) unsigned NOT NULL DEFAULT '0', - `retract_iid` int(10) unsigned NOT NULL DEFAULT '0', - `signed_text` mediumtext NOT NULL, - `signature` text NOT NULL, -! `signer` char(255) NOT NULL DEFAULT '', - PRIMARY KEY (`id`), - KEY `iid` (`iid`), - KEY `retract_iid` (`retract_iid`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `site` -+ -- -+ - CREATE TABLE IF NOT EXISTS `site` ( - `site_url` char(255) NOT NULL, - `site_access` int(11) NOT NULL DEFAULT '0', -*************** -*** 988,996 **** ---- 1260,1275 ---- - KEY `site_register` (`site_register`), - KEY `site_access` (`site_access`), - KEY `site_sellpage` (`site_sellpage`), -+ KEY `site_pull` (`site_pull`), - KEY `site_realm` (`site_realm`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `source` -+ -- -+ - CREATE TABLE IF NOT EXISTS `source` ( - `src_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `src_channel_id` int(10) unsigned NOT NULL DEFAULT '0', -*************** -*** 1003,1014 **** - KEY `src_xchan` (`src_xchan`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `spam` ( - `id` int(11) NOT NULL AUTO_INCREMENT, -! `uid` int(11) NOT NULL, - `spam` int(11) NOT NULL DEFAULT '0', - `ham` int(11) NOT NULL DEFAULT '0', -! `term` char(255) NOT NULL, - `date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - PRIMARY KEY (`id`), - KEY `uid` (`uid`), ---- 1282,1299 ---- - KEY `src_xchan` (`src_xchan`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `spam` -+ -- -+ - CREATE TABLE IF NOT EXISTS `spam` ( - `id` int(11) NOT NULL AUTO_INCREMENT, -! `uid` int(11) NOT NULL DEFAULT '0', - `spam` int(11) NOT NULL DEFAULT '0', - `ham` int(11) NOT NULL DEFAULT '0', -! `term` char(255) NOT NULL DEFAULT '', - `date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - PRIMARY KEY (`id`), - KEY `uid` (`uid`), -*************** -*** 1017,1041 **** - KEY `term` (`term`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `sys_perms` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `cat` char(255) NOT NULL, -! `k` char(255) NOT NULL, - `v` mediumtext NOT NULL, -! `public_perm` tinyint(1) unsigned NOT NULL, - PRIMARY KEY (`id`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `term` ( - `tid` int(10) unsigned NOT NULL AUTO_INCREMENT, - `aid` int(10) unsigned NOT NULL DEFAULT '0', - `uid` int(10) unsigned NOT NULL DEFAULT '0', -! `oid` int(10) unsigned NOT NULL, -! `otype` tinyint(3) unsigned NOT NULL, -! `type` tinyint(3) unsigned NOT NULL, -! `term` char(255) NOT NULL, -! `url` char(255) NOT NULL, -! `imgurl` char(255) NOT NULL, - `term_hash` char(255) NOT NULL DEFAULT '', - `parent_hash` char(255) NOT NULL DEFAULT '', - PRIMARY KEY (`tid`), ---- 1302,1338 ---- - KEY `term` (`term`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `sys_perms` -+ -- -+ - CREATE TABLE IF NOT EXISTS `sys_perms` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `cat` char(255) NOT NULL DEFAULT '', -! `k` char(255) NOT NULL DEFAULT '', - `v` mediumtext NOT NULL, -! `public_perm` tinyint(1) unsigned NOT NULL DEFAULT '0', - PRIMARY KEY (`id`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `term` -+ -- -+ - CREATE TABLE IF NOT EXISTS `term` ( - `tid` int(10) unsigned NOT NULL AUTO_INCREMENT, - `aid` int(10) unsigned NOT NULL DEFAULT '0', - `uid` int(10) unsigned NOT NULL DEFAULT '0', -! `oid` int(10) unsigned NOT NULL DEFAULT '0', -! `otype` tinyint(3) unsigned NOT NULL DEFAULT '0', -! `type` tinyint(3) unsigned NOT NULL DEFAULT '0', -! `term` char(255) NOT NULL DEFAULT '', -! `url` char(255) NOT NULL DEFAULT '', -! `imgurl` char(255) NOT NULL DEFAULT '', - `term_hash` char(255) NOT NULL DEFAULT '', - `parent_hash` char(255) NOT NULL DEFAULT '', - PRIMARY KEY (`tid`), -*************** -*** 1050,1071 **** - KEY `parent_hash` (`parent_hash`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `tokens` ( -! `id` varchar(40) NOT NULL, - `secret` text NOT NULL, -! `client_id` varchar(20) NOT NULL, -! `expires` bigint(20) unsigned NOT NULL, -! `scope` varchar(200) NOT NULL, -! `uid` int(11) NOT NULL, - PRIMARY KEY (`id`), - KEY `client_id` (`client_id`), - KEY `expires` (`expires`), - KEY `uid` (`uid`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `updates` ( - `ud_id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `ud_hash` char(128) NOT NULL, - `ud_guid` char(255) NOT NULL DEFAULT '', - `ud_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `ud_last` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', ---- 1347,1380 ---- - KEY `parent_hash` (`parent_hash`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `tokens` -+ -- -+ - CREATE TABLE IF NOT EXISTS `tokens` ( -! `id` varchar(40) NOT NULL DEFAULT '', - `secret` text NOT NULL, -! `client_id` varchar(20) NOT NULL DEFAULT '', -! `expires` bigint(20) unsigned NOT NULL DEFAULT '0', -! `scope` varchar(200) NOT NULL DEFAULT '', -! `uid` int(11) NOT NULL DEFAULT '0', - PRIMARY KEY (`id`), - KEY `client_id` (`client_id`), - KEY `expires` (`expires`), - KEY `uid` (`uid`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `updates` -+ -- -+ - CREATE TABLE IF NOT EXISTS `updates` ( - `ud_id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `ud_hash` char(128) NOT NULL DEFAULT '', - `ud_guid` char(255) NOT NULL DEFAULT '', - `ud_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', - `ud_last` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', -*************** -*** 1080,1085 **** ---- 1389,1400 ---- - KEY `ud_last` (`ud_last`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `verify` -+ -- -+ - CREATE TABLE IF NOT EXISTS `verify` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `channel` int(10) unsigned NOT NULL DEFAULT '0', -*************** -*** 1095,1100 **** ---- 1410,1421 ---- - KEY `created` (`created`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `vote` -+ -- -+ - CREATE TABLE IF NOT EXISTS `vote` ( - `vote_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `vote_poll` int(11) NOT NULL DEFAULT '0', -*************** -*** 1107,1112 **** ---- 1428,1439 ---- - KEY `vote_element` (`vote_element`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `xchan` -+ -- -+ - CREATE TABLE IF NOT EXISTS `xchan` ( - `xchan_hash` char(255) NOT NULL, - `xchan_guid` char(255) NOT NULL DEFAULT '', -*************** -*** 1139,1144 **** ---- 1466,1477 ---- - KEY `xchan_follow` (`xchan_follow`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `xchat` -+ -- -+ - CREATE TABLE IF NOT EXISTS `xchat` ( - `xchat_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `xchat_url` char(255) NOT NULL DEFAULT '', -*************** -*** 1150,1162 **** - KEY `xchat_desc` (`xchat_desc`), - KEY `xchat_xchan` (`xchat_xchan`), - KEY `xchat_edited` (`xchat_edited`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `xconfig` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `xchan` char(255) NOT NULL, -! `cat` char(255) NOT NULL, -! `k` char(255) NOT NULL, - `v` mediumtext NOT NULL, - PRIMARY KEY (`id`), - KEY `xchan` (`xchan`), ---- 1483,1501 ---- - KEY `xchat_desc` (`xchat_desc`), - KEY `xchat_xchan` (`xchat_xchan`), - KEY `xchat_edited` (`xchat_edited`) -! ) ENGINE=MyISAM DEFAULT CHARSET=utf8; -! -! -- -------------------------------------------------------- -! -! -- -! -- Table structure for table `xconfig` -! -- - - CREATE TABLE IF NOT EXISTS `xconfig` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `xchan` char(255) NOT NULL DEFAULT '', -! `cat` char(255) NOT NULL DEFAULT '', -! `k` char(255) NOT NULL DEFAULT '', - `v` mediumtext NOT NULL, - PRIMARY KEY (`id`), - KEY `xchan` (`xchan`), -*************** -*** 1164,1169 **** ---- 1503,1514 ---- - KEY `k` (`k`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `xign` -+ -- -+ - CREATE TABLE IF NOT EXISTS `xign` ( - `id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `uid` int(11) NOT NULL DEFAULT '0', -*************** -*** 1173,1178 **** ---- 1518,1529 ---- - KEY `xchan` (`xchan`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `xlink` -+ -- -+ - CREATE TABLE IF NOT EXISTS `xlink` ( - `xlink_id` int(10) unsigned NOT NULL AUTO_INCREMENT, - `xlink_xchan` char(255) NOT NULL DEFAULT '', -*************** -*** 1186,1191 **** ---- 1537,1548 ---- - KEY `xlink_rating` (`xlink_rating`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `xprof` -+ -- -+ - CREATE TABLE IF NOT EXISTS `xprof` ( - `xprof_hash` char(255) NOT NULL, - `xprof_age` tinyint(3) unsigned NOT NULL DEFAULT '0', -*************** -*** 1216,1224 **** - KEY `xprof_hometown` (`xprof_hometown`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - - CREATE TABLE IF NOT EXISTS `xtag` ( - `xtag_id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `xtag_hash` char(255) NOT NULL, - `xtag_term` char(255) NOT NULL DEFAULT '', - `xtag_flags` int(11) NOT NULL DEFAULT '0', - PRIMARY KEY (`xtag_id`), ---- 1573,1587 ---- - KEY `xprof_hometown` (`xprof_hometown`) - ) ENGINE=MyISAM DEFAULT CHARSET=utf8; - -+ -- -------------------------------------------------------- -+ -+ -- -+ -- Table structure for table `xtag` -+ -- -+ - CREATE TABLE IF NOT EXISTS `xtag` ( - `xtag_id` int(10) unsigned NOT NULL AUTO_INCREMENT, -! `xtag_hash` char(255) NOT NULL DEFAULT '', - `xtag_term` char(255) NOT NULL DEFAULT '', - `xtag_flags` int(11) NOT NULL DEFAULT '0', - PRIMARY KEY (`xtag_id`), diff --git a/install/htconfig.sample.php b/install/htconfig.sample.php index 07725e3f4..509942530 100755 --- a/install/htconfig.sample.php +++ b/install/htconfig.sample.php @@ -12,7 +12,7 @@ // Then set the following for your MySQL installation -$db_host = 'your.mysqlhost.com'; // Use 'localhost' if you aren't using a remote server +$db_host = 'your.mysqlhost.com'; // Use 'localhost' or ':/path/to/socket.file' if you aren't using a remote server $db_port = 0; // leave 0 for default or set your port $db_user = 'mysqlusername'; $db_pass = 'mysqlpassword'; diff --git a/install/schema_mysql.sql b/install/schema_mysql.sql index 49a2c476a..e7550455f 100644 --- a/install/schema_mysql.sql +++ b/install/schema_mysql.sql @@ -269,24 +269,6 @@ CREATE TABLE IF NOT EXISTS `channel` ( `channel_allow_gid` mediumtext NOT NULL, `channel_deny_cid` mediumtext NOT NULL, `channel_deny_gid` mediumtext NOT NULL, - `channel_r_stream` int(10) unsigned NOT NULL DEFAULT 0 , - `channel_r_profile` int(10) unsigned NOT NULL DEFAULT 0 , - `channel_r_photos` int(10) unsigned NOT NULL DEFAULT 0 , - `channel_r_abook` int(10) unsigned NOT NULL DEFAULT 0 , - `channel_w_stream` int(10) unsigned NOT NULL DEFAULT 0 , - `channel_w_wall` int(10) unsigned NOT NULL DEFAULT 0 , - `channel_w_tagwall` int(10) unsigned NOT NULL DEFAULT 0 , - `channel_w_comment` int(10) unsigned NOT NULL DEFAULT 0 , - `channel_w_mail` int(10) unsigned NOT NULL DEFAULT 0 , - `channel_w_photos` int(10) unsigned NOT NULL DEFAULT 0 , - `channel_w_chat` int(10) unsigned NOT NULL DEFAULT 0 , - `channel_a_delegate` int(10) unsigned NOT NULL DEFAULT 0 , - `channel_r_storage` int(10) unsigned NOT NULL DEFAULT 0 , - `channel_w_storage` int(10) unsigned NOT NULL DEFAULT 0 , - `channel_r_pages` int(10) unsigned NOT NULL DEFAULT 0 , - `channel_w_pages` int(10) unsigned NOT NULL DEFAULT 0 , - `channel_a_republish` int(10) unsigned NOT NULL DEFAULT 0 , - `channel_w_like` int(10) unsigned NOT NULL DEFAULT 0 , `channel_removed` tinyint(1) NOT NULL DEFAULT 0 , `channel_system` tinyint(1) NOT NULL DEFAULT 0 , `channel_moved` char(191) NOT NULL DEFAULT '', @@ -683,7 +665,7 @@ CREATE TABLE IF NOT EXISTS `item` ( KEY `mimetype` (`mimetype`), KEY `mid` (`mid`), KEY `parent_mid` (`parent_mid`), - KEY `uid_mid` (`mid`,`uid`), + KEY `uid_mid` (`uid`,`mid`), KEY `comment_policy` (`comment_policy`), KEY `layout_mid` (`layout_mid`), KEY `public_policy` (`public_policy`), @@ -691,19 +673,16 @@ CREATE TABLE IF NOT EXISTS `item` ( KEY `changed` (`changed`), KEY `item_origin` (`item_origin`), KEY `item_wall` (`item_wall`), - KEY `item_unseen` (`item_unseen`), KEY `item_uplink` (`item_uplink`), - KEY `item_notshown` (`item_notshown`), KEY `item_nsfw` (`item_nsfw`), - KEY `item_relay` (`item_relay`), KEY `item_mentionsme` (`item_mentionsme`), KEY `item_nocomment` (`item_nocomment`), KEY `item_obscured` (`item_obscured`), - KEY `item_verified` (`item_verified`), KEY `item_rss` (`item_rss`), KEY `item_consensus` (`item_consensus`), KEY `item_deleted_pending_remove_changed` (`item_deleted`, `item_pending_remove`, `changed`), - KEY `item_pending_remove_changed` (`item_pending_remove`, `changed`) + KEY `item_pending_remove_changed` (`item_pending_remove`, `changed`), + KEY `thr_parent` (`thr_parent`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS `item_id` ( @@ -1345,7 +1324,8 @@ CREATE TABLE IF NOT EXISTS `xchan` ( KEY `xchan_selfcensored` (`xchan_selfcensored`), KEY `xchan_system` (`xchan_system`), KEY `xchan_pubforum` (`xchan_pubforum`), - KEY `xchan_deleted` (`xchan_deleted`) + KEY `xchan_deleted` (`xchan_deleted`), + KEY `xchan_photo_m` (`xchan_photo_m`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS `xchat` ( diff --git a/install/schema_postgres.sql b/install/schema_postgres.sql index 1b6cab51b..c1c222b37 100644 --- a/install/schema_postgres.sql +++ b/install/schema_postgres.sql @@ -627,7 +627,6 @@ CREATE TABLE "item" ( "item_search_vector" tsvector, PRIMARY KEY ("id") ); -create index "item_uid" on item ("uid"); create index "item_uuid" on item ("uuid"); create index "item_parent" on item ("parent"); create index "item_created" on item ("created"); @@ -638,51 +637,46 @@ create index "item_uid_created" on item ("uid","created"); create index "item_uid_unseen" on item ("uid","item_unseen"); create index "item_changed" on item ("changed"); create index "item_comments_closed" on item ("comments_closed"); -create index "item_aid" on item ("aid"); create index "item_owner_xchan" on item ("owner_xchan"); create index "item_author_xchan" on item ("author_xchan"); create index "item_resource_id" on item ("resource_id"); create index "item_resource_type" on item ("resource_type"); -create index "item_restrict" on item ("item_restrict"); -create index "item_flags" on item ("item_flags"); create index "item_commented" on item ("commented"); create index "item_verb" on item ("verb"); create index "item_obj_type" on item ("obj_type"); -create index "item_private" on item ("item_private"); create index "item_llink" on item ("llink"); create index "item_expires" on item ("expires"); create index "item_revision" on item ("revision"); create index "item_mimetype" on item ("mimetype"); create index "item_mid" on item ("mid"); create index "item_parent_mid" on item ("parent_mid"); -create index "item_uid_mid" on item ("mid","uid"); +create index "item_uid_mid" on item ("uid","mid"); create index "item_public_policy" on item ("public_policy"); create index "item_comment_policy" on item ("comment_policy"); create index "item_layout_mid" on item ("layout_mid"); -create index "item_unseen" on item ("item_unseen"); create index "item_wall" on item ("item_wall"); create index "item_origin" on item ("item_origin"); -create index "item_starred" on item ("item_starred"); create index "item_uplink" on item ("item_uplink"); create index "item_consensus" on item ("item_consensus"); -create index "item_thread_top" on item ("item_thread_top"); -create index "item_notshown" on item ("item_notshown"); create index "item_nsfw" on item ("item_nsfw"); -create index "item_relay" on item ("item_relay"); create index "item_mentionsme" on item ("item_mentionsme"); create index "item_nocomment" on item ("item_nocomment"); create index "item_obscured" on item ("item_obscured"); -create index "item_verified" on item ("item_verified"); -create index "item_retained" on item ("item_retained"); create index "item_rss" on item ("item_rss"); -create index "item_deleted" on item ("item_deleted"); -create index "item_type" on item ("item_type"); -create index "item_hidden" on item ("item_hidden"); -create index "item_unpublished" on item ("item_unpublished"); -create index "item_delayed" on item ("item_delayed"); -create index "item_pending_remove" on item ("item_pending_remove"); -create index "item_blocked" on item ("item_blocked"); +create index "item_thr_parent" on item ("thr_parent"); + +create index "item_uid_item_type" on item ("uid", "item_type"); +create index "item_uid_item_thread_top" on item ("uid", "item_thread_top"); +create index "item_uid_item_blocked" on item ("uid", "item_blocked"); +create index "item_uid_item_wall" on item ("uid", "item_wall"); +create index "item_uid_item_starred" on item ("uid", "item_starred"); +create index "item_uid_item_retained" on item ("uid", "item_retained"); +create index "item_uid_item_private" on item ("uid", "item_private"); +create index "item_uid_resource_type" on item ("uid", "resource_type"); +create index "item_item_deleted_item_pending_remove_changed" on item ("item_deleted", "item_pending_remove", "changed"); +create index "item_item_pending_remove_changed" on item ("item_pending_remove", "changed"); + -- fulltext indexes create index "item_search_idx" on item USING gist("item_search_vector"); create index "item_allow_cid" on item ("allow_cid"); @@ -1318,6 +1312,7 @@ create index "xchan_selfcensored" on xchan ("xchan_selfcensored"); create index "xchan_system" on xchan ("xchan_system"); create index "xchan_pubforum" on xchan ("xchan_pubforum"); create index "xchan_deleted" on xchan ("xchan_deleted"); +create index "xchan_photo_m" on xchan ("xchan_photo_m"); CREATE TABLE "xchat" ( "xchat_id" serial NOT NULL, diff --git a/library/fullcalendar/CHANGELOG.txt b/library/fullcalendar.old/CHANGELOG.txt similarity index 100% rename from library/fullcalendar/CHANGELOG.txt rename to library/fullcalendar.old/CHANGELOG.txt diff --git a/library/fullcalendar/CONTRIBUTING.txt b/library/fullcalendar.old/CONTRIBUTING.txt similarity index 100% rename from library/fullcalendar/CONTRIBUTING.txt rename to library/fullcalendar.old/CONTRIBUTING.txt diff --git a/library/fullcalendar.old/LICENSE.txt b/library/fullcalendar.old/LICENSE.txt new file mode 100644 index 000000000..eafaf97ec --- /dev/null +++ b/library/fullcalendar.old/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright (c) 2015 Adam Shaw + +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. diff --git a/library/fullcalendar/fullcalendar.css b/library/fullcalendar.old/fullcalendar.css similarity index 100% rename from library/fullcalendar/fullcalendar.css rename to library/fullcalendar.old/fullcalendar.css diff --git a/library/fullcalendar/fullcalendar.js b/library/fullcalendar.old/fullcalendar.js similarity index 100% rename from library/fullcalendar/fullcalendar.js rename to library/fullcalendar.old/fullcalendar.js diff --git a/library/fullcalendar/fullcalendar.min.css b/library/fullcalendar.old/fullcalendar.min.css similarity index 100% rename from library/fullcalendar/fullcalendar.min.css rename to library/fullcalendar.old/fullcalendar.min.css diff --git a/library/fullcalendar/fullcalendar.min.js b/library/fullcalendar.old/fullcalendar.min.js similarity index 100% rename from library/fullcalendar/fullcalendar.min.js rename to library/fullcalendar.old/fullcalendar.min.js diff --git a/library/fullcalendar/fullcalendar.print.css b/library/fullcalendar.old/fullcalendar.print.css similarity index 100% rename from library/fullcalendar/fullcalendar.print.css rename to library/fullcalendar.old/fullcalendar.print.css diff --git a/library/fullcalendar/fullcalendar.print.min.css b/library/fullcalendar.old/fullcalendar.print.min.css similarity index 100% rename from library/fullcalendar/fullcalendar.print.min.css rename to library/fullcalendar.old/fullcalendar.print.min.css diff --git a/library/fullcalendar/gcal.js b/library/fullcalendar.old/gcal.js similarity index 100% rename from library/fullcalendar/gcal.js rename to library/fullcalendar.old/gcal.js diff --git a/library/fullcalendar/gcal.min.js b/library/fullcalendar.old/gcal.min.js similarity index 100% rename from library/fullcalendar/gcal.min.js rename to library/fullcalendar.old/gcal.min.js diff --git a/library/fullcalendar/locale-all.js b/library/fullcalendar.old/locale-all.js similarity index 100% rename from library/fullcalendar/locale-all.js rename to library/fullcalendar.old/locale-all.js diff --git a/library/fullcalendar/CHANGELOG.md b/library/fullcalendar/CHANGELOG.md new file mode 100644 index 000000000..2687cd8e7 --- /dev/null +++ b/library/fullcalendar/CHANGELOG.md @@ -0,0 +1,1377 @@ + +v4.0.2 (2019-04-03) +------------------- + +Bugfixes: +- eventAllow and constraints not respected when dragging event between calendars +- viewSkeletonRender now in typedefs (#4589) +- invalid draggedEvent properties in eventAllow for external dnd (#4575) +- forceEventDuration not working with external dnd (#4597) +- rrule displaying time when allDay is true (#4576) +- rrule events not displaying at interval start (#4596) +- prev button not initially working when starting on 31st of a month (#4595) +- clicking X in popover generating a dayClick (#4584) +- locale file used as single script tag not affecting calendar locale (#4581) +- header "today" button not translated for pt and pt-br (#4591) +- fa locale typo (#4582) + + +v4.0.1 (2019-03-18) +------------------- + +Read about all the changes in v4: +https://fullcalendar.io/docs/upgrading-from-v3 + +Obscure breaking changes from v3->v4 not mentioned elsewhere: +- `touchMouseIgnoreWait` moved to `(packageRoot).config.touchMouseIgnoreWait` +- `dataAttrPrefix` moved to `(packageRoot).config.dataAttrPrefix` + +Advancements since latest prerelease: +- New styling for buttons and icons in header. New styling for events. +- Bugfixes: #4539, #4503, #4534, #4505, #4477, #4467, #4454, #4458, #4483, + #4517, #4506, #4435, #4498, #4497, #4446, #4432, #4530 + +NOTE: version "4.0.0" was skipped because of an NPM publishing error + + +v3.10.0 (2019-01-10) +-------------------- + +POTENTIALLY BREAKING CHANGE: +The jquery and moment packages have been moved to peerDependencies. If you are using +NPM to install fullcalendar, you'll need to explicitly add jquery and moment as +dependencies of your project. NPM will not install them automatically. (#4136, #4233) + +New Features: +- events from a Google Calendar event source will receive extended props (#4123) +- export more classes and util functions (#4124) +- new locales: zh-hk (#4266), be (#4274) + +Bugfixes: +- not accepting dayClicks/selects because of overflow-x:hidden on html/body (#3615) +- event end time not displayed when duration is one slot, in agenda view (#3049) +- switching views before event fetch resolves, JS error (#3689) +- single-day allDay event not showing when time is specified (#3854) +- prev button doesn't work when previous days are hidden by hiddenDays and dayCount + is greater than dateIncrement (#4202) +- calendar locale not used in all moments objects (#4174) +- background event background color does not completely fill cells in Chrome (#4145) +- provide a delta for eventResize when resizing from start (#4135) +- IE11 memory leak from not removing handler correctly (#4311) +- make touchstart handlers passive (#4087) +- fixed typescript definition for: eventAllow (#4243), selectAllow (#4319) +- fixed locales: de (#4197, #4371), hu (#4203), tr (#4312), ja (#4329) + + +v3.9.0 (2018-03-04) +------------------- + +- Bootstrap 4 support (#4032, #4065, thx @GeekJosh) +- add OptionsInput to the fullcalendar.d.ts exports (#4040, #4006) +- columnHeaderFormat/columnHeaderHtml/columnHeaderText in .d.ts file (#4061, #4085) +- list-view auto-height not working (#3346, #4071, thx @WhatTheBuild) +- bump momentjs minimum version to 2.20.1, for locale fixes (#4014) +- swedish week header translation fix (#4082) +- dutch year translation (#4069) + + +v3.8.2 (2018-01-30) +------------------- + +Bugfixes: +- Fix TypeScript definitions file with strictNullChecks (#4035) + + +v3.8.1 (2018-01-28) +------------------- + +Bugfixes: +- TypeScript definition file not compatible with noImplicitAny (#4017) +- ES6 classes are not supported for grid class (#3437) +- day numbers in month view should be localized (#3339) +- select helper is resizable, causes js error (#3764) +- selecting over existing select helper causes js error (#4031) +- eventOrder doesn't work on custom fields (#3950) +- aria label on button icons (#4023) +- dynamic option changes to select/overlap/allow doesn't cause rerender + +Locales: +- added Georgian (#3994) +- added Bosnian (#4029) + + +v3.8.0 (2017-12-18) +------------------- + +- new settings for month/agenda/basic views (#3078): + - `columnHeaderFormat` (renamed from `columnFormat`) + - `columnHeaderText` + - `columnHeaderHtml` +- TypeScript definition file (fullcalendar.d.ts) included in npm package (#3889) +- codebase using SASS, though not taking advantage of it yet (#3463) +- codebase fully ported to TypeScript / Webpack +- Afrikaans locale fix (#3862) + + +v3.7.0 (2017-11-13) +------------------- + +Bugfixes: +- `render` method does not re-adjust calendar dimension (#3893) +- when custom view navigates completely into hidden weekends, JS error ([scheduler-375]) + +Other: +- in themes.html demo, fixed broken Bootswatch themes (#3917) +- moved JavaScript codebase over to TypeScript + (same external API; embedded typedefs coming soon) + +[scheduler-375]: https://github.com/fullcalendar/fullcalendar-scheduler/issues/375 + + +v3.6.2 (2017-10-23) +------------------- + +Bugfixes: +- Google Calendar event sources not calling `loading` callback (#3884) +- `eventDataTransform` w/ eventConstraint shouldn't be called during event resizing (#3859) +- `navLinks` would go to the previously navigated date (#3869) +- `nowIndicator` arrow would repeatedly render (#3872) +- fc-content-skeleton DOM element would repeatedly render on navigation in agenda view + + +v3.6.1 (2017-10-11) +------------------- + +Bugfixes: +- JSON feed event sources always requesting current page (#3865) +- multi-day events appearing multiple times in more+ popover (#3856) + + +v3.6.0 (2017-10-10) +------------------- + +Features: +- `agendaEventMinHeight` for guaranteeing height (#961, #3788) thx @Stafie +- `columnHeader` can be set to `false` to hide headings (#3438, #3787) thx @caseyjhol +- export all View classes (#2851, #3831) +- `updateEvent`, update complex attributes (#2864) +- Albanian locale (#3847) thx @alensaqe + +Bugfixes: +- objects used as non-standard Event properties ignored by `updateEvent` (#3839) +- listDay error if event goes over period (#3843) +- `validDays` with `hiddenDays`, js error when no days active (#3846) +- json feed Event Source object no longer has `url` property (#3845) +- `updateEvent`, allDay to timed, when no end, wrong end date (#3144) +- `removeEvents` by `_id` stopped working (#3828) +- correct `this` context in FuncEventSource (#3848) thx @declspec +- js event not received in unselect callback when selecting another cell (#3832) + +Incompatibilities: +- The `viewRender` callback might now be fired AFTER events have been rendered + to the DOM. However, the eventRender/eventAfterRender/eventAfterAllRender callbacks + will always be fired after `viewRender`, just as before. +- The internal `Grid` class (accessed via `$.fullCalendar.Grid`) has been removed. + For monkeypatching, use DayGrid/TimeGrid directly. + + +v3.5.1 (2017-09-06) +------------------- + +- fixed loading trigger not firing (#3810) +- fixed overaggressively fetching events, on option changes (#3820) +- fixed event object `date` property being discarded (tho still parsed) (#3819) +- fixed event object `_id` property being discarded (#3811) + + +v3.5.0 (2017-08-30) +------------------- + +Features: +- Bootstrap 3 theme support (#2334, #3566) + - via `themeSystem: 'bootstrap3'` (the `theme` option is deprecated) + - new `bootstrapGlyphicons` option + - jQuery UI "Cupertino" theme no longer included in zip archive + - improved theme switcher on demo page (#1436) + (big thanks to @joankaradimov) +- 25% event rendering performance improvement across the board (#2524) +- console message for unknown method/calendar (#3253) +- Serbian cyrilic/latin (#3656) +- available via Packagist (#2999, #3617) + +Bugfixes: +- slot time label invisible when minTime starts out of alignment (#2786) +- bug with inverse-background event rendering when out of range (#3652) +- wrongly disabled prev/next when current date outside of validRange (#3686, #3651) +- updateEvent, error when changing allDay from false to true (#3518) +- updateEvent doesn't support ID changes (#2928) +- Promise then method doesn't forward result (#3744) +- Korean typo (#3693) +- fixed switching from any view to listview, eventAfterRender isn't called (#3751) + +Incompatibilities: +- Event Objects obtained from clientEvents or various callbacks are no longer + references to internally used objects. Rather, they are static object copies. +- `clientEvents` method no longer returns events in same order as received. + Do not depend on order. + + +v3.4.0 (2017-04-27) +------------------- + +- composer.json for Composer (PHP package manager) (#3617) +- fix toISOString for locales with non-trivial postformatting (#3619) +- fix for nested inverse-background events (#3609) +- Estonian locale (#3600) +- fixed Latvian localization (#3525) +- internal refactor of async systems + + +v3.3.1 (2017-04-01) +------------------- + +Bugfixes: +- stale calendar title when navigate away from then back to the a view (#3604) +- js error when gotoDate immediately after calendar initialization (#3598) +- agenda view scrollbars causes misalignment in jquery 3.2.1 (#3612) +- navigation bug when trying to navigate to a day of another week (#3610) +- dateIncrement not working when duration and dateIncrement have different units + + +v3.3.0 (2017-03-23) +------------------- + +Features: +- `visibleRange` - complete control over view's date range (#2847, #3105, #3245) +- `validRange` - restrict date range (#429) +- `changeView` - pass in a date or visibleRange as second param (#3366) +- `dateIncrement` - customize prev/next jump (#2710) +- `dateAlignment` - custom view alignment, like start-of-week (#3113) +- `dayCount` - force a fixed number-of-days, even with hiddenDays (#2753) +- `showNonCurrentDates` - option to hide day cells for prev/next months (#437) +- can define a defaultView with a duration/visibleRange/dayCount with needing + to create a custom view in the `views` object. Known as a "Generic View". + +Behavior Changes: +- when custom view is specified with duration `{days:7}`, + it will no longer align with the start of the week. (#2847) +- when `gotoDate` is called on a custom view with a duration of multiple days, + the view will always shift to begin with the given date. (#3515) + +Bugfixes: +- event rendering when excessive `minTime`/`maxTime` (#2530) +- event dragging not shown when excessive `minTime`/`maxTime` (#3055) +- excessive `minTime`/`maxTime` not reflected in event fetching (#3514) + - when minTime is negative, or maxTime beyond 24 hours, when event data is requested + via a function or a feed, the given data params will have time parts. +- external event dragging via touchpunch broken (#3544) +- can't make an immediate new selection after existing selection, with mouse. + introduced in v3.2.0 (#3558) + + +v3.2.0 (2017-02-14) +------------------- + +Features: +- `selectMinDistance`, threshold before a mouse selection begins (#2428) + +Bugfixes: +- iOS 10, unwanted scrolling while dragging events/selection (#3403) +- dayClick triggered when swiping on touch devices (#3332) +- dayClick not functioning on Firefix mobile (#3450) +- title computed incorrectly for views with no weekends (#2884) +- unwanted scrollbars in month-view when non-integer width (#3453, #3444) +- incorrect date formatting for locales with non-standlone month/day names (#3478) +- date formatting, incorrect omission of trailing period for certain locales (#2504, #3486) +- formatRange should collapse same week numbers (#3467) +- Taiwanese locale updated (#3426) +- Finnish noEventsMessage updated (#3476) +- Croatian (hr) buttonText is blank (#3270) +- JSON feed PHP example, date range math bug (#3485) + + +v3.1.0 (2016-12-05) +------------------- + +- experimental support for implicitly batched ("debounced") event rendering (#2938) + - `eventRenderWait` (off by default) +- new `footer` option, similar to header toolbar (#654, #3299) +- event rendering batch methods (#3351): + - `renderEvents` + - `updateEvents` +- more granular touch settings (#3377): + - `eventLongPressDelay` + - `selectLongPressDelay` +- eventDestroy not called when removing the popover (#3416, #3419) +- print stylesheet and gcal extension now offered as minified (#3415) +- fc-today in agenda header cells (#3361, #3365) +- height-related options in tandem with other options (#3327, #3384) +- Kazakh locale (#3394) +- Afrikaans locale (#3390) +- internal refactor related to timing of rendering and firing handlers. + calls to rerender the current date-range and events from within handlers + might not execute immediately. instead, will execute after handler finishes. + + +v3.0.1 (2016-09-26) +------------------- + +Bugfixes: +- list view rendering event times incorrectly (#3334) +- list view rendering events/days out of order (#3347) +- events with no title rendering as "undefined" +- add .fc scope to table print styles (#3343) +- "display no events" text fix for German (#3354) + + +v3.0.0 (2016-09-04) +------------------- + +Features: +- List View (#560) + - new views: `listDay`, `listWeek`, `listMonth`, `listYear`, and simply `list` + - `listDayFormat` + - `listDayAltFormat` + - `noEventsMessage` +- Clickable day/week numbers for easier navigation (#424) + - `navLinks` + - `navLinkDayClick` + - `navLinkWeekClick` +- Programmatically allow/disallow user interactions: + - `eventAllow` (#2740) + - `selectAllow` (#2511) +- Option to display week numbers in cells (#3024) + - `weekNumbersWithinDays` (set to `true` to activate) +- When week calc is ISO, default first day-of-week to Monday (#3255) +- Macedonian locale (#2739) +- Malay locale + +Breaking Changes: +- IE8 support dropped +- jQuery: minimum support raised to v2.0.0 +- MomentJS: minimum support raised to v2.9.0 +- `lang` option renamed to `locale` +- dist files have been renamed to be more consistent with MomentJS: + - `lang/` -> `locale/` + - `lang-all.js` -> `locale-all.js` +- behavior of moment methods no longer affected by ambiguousness: + - `isSame` + - `isBefore` + - `isAfter` +- View-Option-Hashes no longer supported (deprecated in 2.2.4) +- removed `weekMode` setting +- removed `axisFormat` setting +- DOM structure of month/basic-view day cell numbers changed + +Bugfixes: +- `$.fullCalendar.version` incorrect (#3292) + +Build System: +- using gulp instead of grunt (faster) +- using npm internally for dependencies instead of bower +- changed repo directory structure + + +v2.9.1 (2016-07-31) +------------------- + +- multiple definitions for businessHours (#2686) +- businessHours for single day doesn't display weekends (#2944) +- height/contentHeight can accept a function or 'parent' for dynamic value (#3271) +- fix +more popover clipped by overflow (#3232) +- fix +more popover positioned incorrectly when scrolled (#3137) +- Norwegian Nynorsk translation (#3246) +- fix isAnimating JS error (#3285) + + +v2.9.0 (2016-07-10) +------------------- + +- Setters for (almost) all options (#564). + See [docs](http://fullcalendar.io/docs/utilities/dynamic_options/) for more info. +- Travis CI improvements (#3266) + + +v2.8.0 (2016-06-19) +------------------- + +- getEventSources method (#3103, #2433) +- getEventSourceById method (#3223) +- refetchEventSources method (#3103, #1328, #254) +- removeEventSources method (#3165, #948) +- prevent flicker when refetchEvents is called (#3123, #2558) +- fix for removing event sources that share same URL (#3209) +- jQuery 3 support (#3197, #3124) +- Travis CI integration (#3218) +- EditorConfig for promoting consistent code style (#141) +- use en dash when formatting ranges (#3077) +- height:auto always shows scrollbars in month view on FF (#3202) +- new languages: + - Basque (#2992) + - Galician (#194) + - Luxembourgish (#2979) + + +v2.7.3 (2016-06-02) +------------------- + +internal enhancements that plugins can benefit from: +- EventEmitter not correctly working with stopListeningTo +- normalizeEvent hook for manipulating event data + + +v2.7.2 (2016-05-20) +------------------- + +- fixed desktops/laptops with touch support not accepting mouse events for + dayClick/dragging/resizing (#3154, #3149) +- fixed dayClick incorrectly triggered on touch scroll (#3152) +- fixed touch event dragging wrongfully beginning upon scrolling document (#3160) +- fixed minified JS still contained comments +- UI change: mouse users must hover over an event to reveal its resizers + + +v2.7.1 (2016-05-01) +------------------- + +- dayClick not firing on touch devices (#3138) +- icons for prev/next not working in MS Edge (#2852) +- fix bad languages troubles with firewalls (#3133, #3132) +- update all dev dependencies (#3145, #3010, #2901, #251) +- git-ignore npm debug logs (#3011) +- misc automated test updates (#3139, #3147) +- Google Calendar htmlLink not always defined (#2844) + + +v2.7.0 (2016-04-23) +------------------- + +touch device support (#994): + - smoother scrolling + - interactions initiated via "long press": + - event drag-n-drop + - event resize + - time-range selecting + - `longPressDelay` + + +v2.6.1 (2016-02-17) +------------------- + +- make `nowIndicator` positioning refresh on window resize + + +v2.6.0 (2016-01-07) +------------------- + +- current time indicator (#414) +- bundled with most recent version of moment (2.11.0) +- UMD wrapper around lang files now handles commonjs (#2918) +- fix bug where external event dragging would not respect eventOverlap +- fix bug where external event dropping would not render the whole-day highlight + + +v2.5.0 (2015-11-30) +------------------- + +- internal timezone refactor. fixes #2396, #2900, #2945, #2711 +- internal "grid" system refactor. improved API for plugins. + + +v2.4.0 (2015-08-16) +------------------- + +- add new buttons to the header via `customButtons` ([225]) +- control stacking order of events via `eventOrder` ([364]) +- control frequency of slot text via `slotLabelInterval` ([946]) +- `displayEventTime` ([1904]) +- `on` and `off` methods ([1910]) +- renamed `axisFormat` to `slotLabelFormat` + +[225]: https://code.google.com/p/fullcalendar/issues/detail?id=225 +[364]: https://code.google.com/p/fullcalendar/issues/detail?id=364 +[946]: https://code.google.com/p/fullcalendar/issues/detail?id=946 +[1904]: https://code.google.com/p/fullcalendar/issues/detail?id=1904 +[1910]: https://code.google.com/p/fullcalendar/issues/detail?id=1910 + + +v2.3.2 (2015-06-14) +------------------- + +- minor code adjustment in preparation for plugins + + +v2.3.1 (2015-03-08) +------------------- + +- Fix week view column title for en-gb ([PR220]) +- Publish to NPM ([2447]) +- Detangle bower from npm package ([PR179]) + +[PR220]: https://github.com/arshaw/fullcalendar/pull/220 +[2447]: https://code.google.com/p/fullcalendar/issues/detail?id=2447 +[PR179]: https://github.com/arshaw/fullcalendar/pull/179 + + +v2.3.0 (2015-02-21) +------------------- + +- internal refactoring in preparation for other views +- businessHours now renders on whole-days in addition to timed areas +- events in "more" popover not sorted by time ([2385]) +- avoid using moment's deprecated zone method ([2443]) +- destroying the calendar sometimes causes all window resize handlers to be unbound ([2432]) +- multiple calendars on one page, can't accept external elements after navigating ([2433]) +- accept external events from jqui sortable ([1698]) +- external jqui drop processed before reverting ([1661]) +- IE8 fix: month view renders incorrectly ([2428]) +- IE8 fix: eventLimit:true wouldn't activate "more" link ([2330]) +- IE8 fix: dragging an event with an href +- IE8 fix: invisible element while dragging agenda view events +- IE8 fix: erratic external element dragging + +[2385]: https://code.google.com/p/fullcalendar/issues/detail?id=2385 +[2443]: https://code.google.com/p/fullcalendar/issues/detail?id=2443 +[2432]: https://code.google.com/p/fullcalendar/issues/detail?id=2432 +[2433]: https://code.google.com/p/fullcalendar/issues/detail?id=2433 +[1698]: https://code.google.com/p/fullcalendar/issues/detail?id=1698 +[1661]: https://code.google.com/p/fullcalendar/issues/detail?id=1661 +[2428]: https://code.google.com/p/fullcalendar/issues/detail?id=2428 +[2330]: https://code.google.com/p/fullcalendar/issues/detail?id=2330 + + +v2.2.7 (2015-02-10) +------------------- + +- view.title wasn't defined in viewRender callback ([2407]) +- FullCalendar versions >= 2.2.5 brokenness with Moment versions <= 2.8.3 ([2417]) +- Support Bokmal Norwegian language specifically ([2427]) + +[2407]: https://code.google.com/p/fullcalendar/issues/detail?id=2407 +[2417]: https://code.google.com/p/fullcalendar/issues/detail?id=2417 +[2427]: https://code.google.com/p/fullcalendar/issues/detail?id=2427 + + +v2.2.6 (2015-01-11) +------------------- + +- Compatibility with Moment v2.9. Was breaking GCal plugin ([2408]) +- View object's `title` property mistakenly omitted ([2407]) +- Single-day views with hiddens days could cause prev/next misbehavior ([2406]) +- Don't let the current date ever be a hidden day (solves [2395]) +- Hebrew locale ([2157]) + +[2408]: https://code.google.com/p/fullcalendar/issues/detail?id=2408 +[2407]: https://code.google.com/p/fullcalendar/issues/detail?id=2407 +[2406]: https://code.google.com/p/fullcalendar/issues/detail?id=2406 +[2395]: https://code.google.com/p/fullcalendar/issues/detail?id=2395 +[2157]: https://code.google.com/p/fullcalendar/issues/detail?id=2157 + + +v2.2.5 (2014-12-30) +------------------- + +- `buttonText` specified for custom views via the `views` option + - bugfix: wrong default value, couldn't override default + - feature: default value taken from locale + + +v2.2.4 (2014-12-29) +------------------- + +- Arbitrary durations for basic/agenda views with the `views` option ([692]) +- Specify view-specific options using the `views` option. fixes [2283] +- Deprecate view-option-hashes +- Formalize and expose View API ([1055]) +- updateEvent method, more intuitive behavior. fixes [2194] + +[692]: https://code.google.com/p/fullcalendar/issues/detail?id=692 +[2283]: https://code.google.com/p/fullcalendar/issues/detail?id=2283 +[1055]: https://code.google.com/p/fullcalendar/issues/detail?id=1055 +[2194]: https://code.google.com/p/fullcalendar/issues/detail?id=2194 + + +v2.2.3 (2014-11-26) +------------------- + +- removeEventSource with Google Calendar object source, would not remove ([2368]) +- Events with invalid end dates are still accepted and rendered ([2350], [2237], [2296]) +- Bug when rendering business hours and navigating away from original view ([2365]) +- Links to Google Calendar events will use current timezone ([2122]) +- Google Calendar plugin works with timezone names that have spaces +- Google Calendar plugin accepts person email addresses as calendar IDs +- Internally use numeric sort instead of alphanumeric sort ([2370]) + +[2368]: https://code.google.com/p/fullcalendar/issues/detail?id=2368 +[2350]: https://code.google.com/p/fullcalendar/issues/detail?id=2350 +[2237]: https://code.google.com/p/fullcalendar/issues/detail?id=2237 +[2296]: https://code.google.com/p/fullcalendar/issues/detail?id=2296 +[2365]: https://code.google.com/p/fullcalendar/issues/detail?id=2365 +[2122]: https://code.google.com/p/fullcalendar/issues/detail?id=2122 +[2370]: https://code.google.com/p/fullcalendar/issues/detail?id=2370 + + +v2.2.2 (2014-11-19) +------------------- + +- Fixes to Google Calendar API V3 code + - wouldn't recognize a lone-string Google Calendar ID if periods before the @ symbol + - removeEventSource wouldn't work when given a Google Calendar ID + + +v2.2.1 (2014-11-19) +------------------- + +- Migrate Google Calendar plugin to use V3 of the API ([1526]) + +[1526]: https://code.google.com/p/fullcalendar/issues/detail?id=1526 + + +v2.2.0 (2014-11-14) +------------------- + +- Background events. Event object's `rendering` property ([144], [1286]) +- `businessHours` option ([144]) +- Controlling where events can be dragged/resized and selections can go ([396], [1286], [2253]) + - `eventOverlap`, `selectOverlap`, and similar + - `eventConstraint`, `selectConstraint`, and similar +- Improvements to dragging and dropping external events ([2004]) + - Associating with real event data. used with `eventReceive` + - Associating a `duration` +- Performance boost for moment creation + - Be aware, FullCalendar-specific methods now attached directly to global moment.fn + - Helps with [issue 2259][2259] +- Reintroduced forgotten `dropAccept` option ([2312]) + +[144]: https://code.google.com/p/fullcalendar/issues/detail?id=144 +[396]: https://code.google.com/p/fullcalendar/issues/detail?id=396 +[1286]: https://code.google.com/p/fullcalendar/issues/detail?id=1286 +[2004]: https://code.google.com/p/fullcalendar/issues/detail?id=2004 +[2253]: https://code.google.com/p/fullcalendar/issues/detail?id=2253 +[2259]: https://code.google.com/p/fullcalendar/issues/detail?id=2259 +[2312]: https://code.google.com/p/fullcalendar/issues/detail?id=2312 + + +v2.1.1 (2014-08-29) +------------------- + +- removeEventSource not working with array ([2203]) +- mouseout not triggered after mouseover+updateEvent ([829]) +- agenda event's render with no href, not clickable ([2263]) + +[2203]: https://code.google.com/p/fullcalendar/issues/detail?id=2203 +[829]: https://code.google.com/p/fullcalendar/issues/detail?id=829 +[2263]: https://code.google.com/p/fullcalendar/issues/detail?id=2263 + + +v2.1.0 (2014-08-25) +------------------- + +Large code refactor with better OOP, better code reuse, and more comments. +**No more reliance on jQuery UI** for event dragging, resizing, or anything else. + +Significant changes to HTML/CSS skeleton: +- Leverages tables for liquid rendering of days and events. No costly manual repositioning ([809]) +- **Backwards-incompatibilities**: + - **Many classNames have changed. Custom CSS will likely need to be adjusted.** + - IE7 definitely not supported anymore + - In `eventRender` callback, `element` will not be attached to DOM yet + - Events are styled to be one line by default ([1992]). Can be undone through custom CSS, + but not recommended (might get gaps [like this][111] in certain situations). + +A "more..." link when there are too many events on a day ([304]). Works with month and basic views +as well as the all-day section of the agenda views. New options: +- `eventLimit`. a number or `true` +- `eventLimitClick`. the `"popover`" value will reveal all events in a raised panel (the default) +- `eventLimitText` +- `dayPopoverFormat` + +Changes related to height and scrollbars: +- `aspectRatio`/`height`/`contentHeight` values will be honored *no matter what* + - If too many events causing too much vertical space, scrollbars will be used ([728]). + This is default behavior for month view (**backwards-incompatibility**) + - If too few slots in agenda view, view will stretch to be the correct height ([2196]) +- `'auto'` value for `height`/`contentHeight` options. If content is too tall, the view will + vertically stretch to accomodate and no scrollbars will be used ([521]). +- Tall weeks in month view will borrow height from other weeks ([243]) +- Automatically scroll the view then dragging/resizing an event ([1025], [2078]) +- New `fixedWeekCount` option to determines the number of weeks in month view + - Supersedes `weekMode` (**deprecated**). Instead, use a combination of `fixedWeekCount` and + one of the height options, possibly with an `'auto'` value + +Much nicer, glitch-free rendering of calendar *for printers* ([35]). Things you might not expect: +- Buttons will become hidden +- Agenda views display a flat list of events where the time slots would be + +Other issues resolved along the way: +- Space on right side of agenda events configurable through CSS ([204]) +- Problem with window resize ([259]) +- Events sorting stays consistent across weeks ([510]) +- Agenda's columns misaligned on wide screens ([511]) +- Run `selectHelper` through `eventRender` callbacks ([629]) +- Keyboard access, tabbing ([637]) +- Run resizing events through `eventRender` ([714]) +- Resize an event to a different day in agenda views ([736]) +- Allow selection across days in agenda views ([778]) +- Mouseenter delegated event not working on event elements ([936]) +- Agenda event dragging, snapping to different columns is erratic ([1101]) +- Android browser cuts off Day view at 8 PM with no scroll bar ([1203]) +- Don't fire `eventMouseover`/`eventMouseout` while dragging/resizing ([1297]) +- Customize the resize handle text ("=") ([1326]) +- If agenda event is too short, don't overwrite `.fc-event-time` ([1700]) +- Zooming calendar causes events to misalign ([1996]) +- Event destroy callback on event removal ([2017]) +- Agenda views, when RTL, should have axis on right ([2132]) +- Make header buttons more accessibile ([2151]) +- daySelectionMousedown should interpret OSX ctrl+click as a right mouse click ([2169]) +- Best way to display time text on multi-day events *with times* ([2172]) +- Eliminate table use for header layout ([2186]) +- Event delegation used for event-related callbacks (like `eventClick`). Speedier. + +[35]: https://code.google.com/p/fullcalendar/issues/detail?id=35 +[204]: https://code.google.com/p/fullcalendar/issues/detail?id=204 +[243]: https://code.google.com/p/fullcalendar/issues/detail?id=243 +[259]: https://code.google.com/p/fullcalendar/issues/detail?id=259 +[304]: https://code.google.com/p/fullcalendar/issues/detail?id=304 +[510]: https://code.google.com/p/fullcalendar/issues/detail?id=510 +[511]: https://code.google.com/p/fullcalendar/issues/detail?id=511 +[521]: https://code.google.com/p/fullcalendar/issues/detail?id=521 +[629]: https://code.google.com/p/fullcalendar/issues/detail?id=629 +[637]: https://code.google.com/p/fullcalendar/issues/detail?id=637 +[714]: https://code.google.com/p/fullcalendar/issues/detail?id=714 +[728]: https://code.google.com/p/fullcalendar/issues/detail?id=728 +[736]: https://code.google.com/p/fullcalendar/issues/detail?id=736 +[778]: https://code.google.com/p/fullcalendar/issues/detail?id=778 +[809]: https://code.google.com/p/fullcalendar/issues/detail?id=809 +[936]: https://code.google.com/p/fullcalendar/issues/detail?id=936 +[1025]: https://code.google.com/p/fullcalendar/issues/detail?id=1025 +[1101]: https://code.google.com/p/fullcalendar/issues/detail?id=1101 +[1203]: https://code.google.com/p/fullcalendar/issues/detail?id=1203 +[1297]: https://code.google.com/p/fullcalendar/issues/detail?id=1297 +[1326]: https://code.google.com/p/fullcalendar/issues/detail?id=1326 +[1700]: https://code.google.com/p/fullcalendar/issues/detail?id=1700 +[1992]: https://code.google.com/p/fullcalendar/issues/detail?id=1992 +[1996]: https://code.google.com/p/fullcalendar/issues/detail?id=1996 +[2017]: https://code.google.com/p/fullcalendar/issues/detail?id=2017 +[2078]: https://code.google.com/p/fullcalendar/issues/detail?id=2078 +[2132]: https://code.google.com/p/fullcalendar/issues/detail?id=2132 +[2151]: https://code.google.com/p/fullcalendar/issues/detail?id=2151 +[2169]: https://code.google.com/p/fullcalendar/issues/detail?id=2169 +[2172]: https://code.google.com/p/fullcalendar/issues/detail?id=2172 +[2186]: https://code.google.com/p/fullcalendar/issues/detail?id=2186 +[2196]: https://code.google.com/p/fullcalendar/issues/detail?id=2196 +[111]: https://code.google.com/p/fullcalendar/issues/detail?id=111 + + +v2.0.3 (2014-08-15) +------------------- + +- moment-2.8.1 compatibility ([2221]) +- relative path in bower.json ([PR 117]) +- upgraded jquery-ui and misc dev dependencies + +[2221]: https://code.google.com/p/fullcalendar/issues/detail?id=2221 +[PR 117]: https://github.com/arshaw/fullcalendar/pull/177 + + +v2.0.2 (2014-06-24) +------------------- + +- bug with persisting addEventSource calls ([2191]) +- bug with persisting removeEvents calls with an array source ([2187]) +- bug with removeEvents method when called with 0 removes all events ([2082]) + +[2191]: https://code.google.com/p/fullcalendar/issues/detail?id=2191 +[2187]: https://code.google.com/p/fullcalendar/issues/detail?id=2187 +[2082]: https://code.google.com/p/fullcalendar/issues/detail?id=2082 + + +v2.0.1 (2014-06-15) +------------------- + +- `delta` parameters reintroduced in `eventDrop` and `eventResize` handlers ([2156]) + - **Note**: this changes the argument order for `revertFunc` +- wrongfully triggering a windowResize when resizing an agenda view event ([1116]) +- `this` values in event drag-n-drop/resize handlers consistently the DOM node ([1177]) +- `displayEventEnd` - v2 workaround to force display of an end time ([2090]) +- don't modify passed-in eventSource items ([954]) +- destroy method now removes fc-ltr class ([2033]) +- weeks of last/next month still visible when weekends are hidden ([2095]) +- fixed memory leak when destroying calendar with selectable/droppable ([2137]) +- Icelandic language ([2180]) +- Bahasa Indonesia language ([PR 172]) + +[1116]: https://code.google.com/p/fullcalendar/issues/detail?id=1116 +[1177]: https://code.google.com/p/fullcalendar/issues/detail?id=1177 +[2090]: https://code.google.com/p/fullcalendar/issues/detail?id=2090 +[954]: https://code.google.com/p/fullcalendar/issues/detail?id=954 +[2033]: https://code.google.com/p/fullcalendar/issues/detail?id=2033 +[2095]: https://code.google.com/p/fullcalendar/issues/detail?id=2095 +[2137]: https://code.google.com/p/fullcalendar/issues/detail?id=2137 +[2156]: https://code.google.com/p/fullcalendar/issues/detail?id=2156 +[2180]: https://code.google.com/p/fullcalendar/issues/detail?id=2180 +[PR 172]: https://github.com/arshaw/fullcalendar/pull/172 + + +v2.0.0 (2014-06-01) +------------------- + +Internationalization support, timezone support, and [MomentJS] integration. Extensive changes, many +of which are backwards incompatible. + +[Full list of changes][Upgrading-to-v2] | [Affected Issues][Date-Milestone] + +An automated testing framework has been set up ([Karma] + [Jasmine]) and tests have been written +which cover about half of FullCalendar's functionality. Special thanks to @incre-d, @vidbina, and +@sirrocco for the help. + +In addition, the main development repo has been repurposed to also include the built distributable +JS/CSS for the project and will serve as the new [Bower] endpoint. + +[MomentJS]: http://momentjs.com/ +[Upgrading-to-v2]: http://arshaw.com/fullcalendar/wiki/Upgrading-to-v2/ +[Date-Milestone]: https://code.google.com/p/fullcalendar/issues/list?can=1&q=milestone%3Ddate +[Karma]: http://karma-runner.github.io/ +[Jasmine]: http://jasmine.github.io/ +[Bower]: http://bower.io/ + + +v1.6.4 (2013-09-01) +------------------- + +- better algorithm for positioning timed agenda events ([1115]) +- `slotEventOverlap` option to tweak timed agenda event overlapping ([218]) +- selection bug when slot height is customized ([1035]) +- supply view argument in `loading` callback ([1018]) +- fixed week number not displaying in agenda views ([1951]) +- fixed fullCalendar not initializing with no options ([1356]) +- NPM's `package.json`, no more warnings or errors ([1762]) +- building the bower component should output `bower.json` instead of `component.json` ([PR 125]) +- use bower internally for fetching new versions of jQuery and jQuery UI + +[1115]: https://code.google.com/p/fullcalendar/issues/detail?id=1115 +[218]: https://code.google.com/p/fullcalendar/issues/detail?id=218 +[1035]: https://code.google.com/p/fullcalendar/issues/detail?id=1035 +[1018]: https://code.google.com/p/fullcalendar/issues/detail?id=1018 +[1951]: https://code.google.com/p/fullcalendar/issues/detail?id=1951 +[1356]: https://code.google.com/p/fullcalendar/issues/detail?id=1356 +[1762]: https://code.google.com/p/fullcalendar/issues/detail?id=1762 +[PR 125]: https://github.com/arshaw/fullcalendar/pull/125 + + +v1.6.3 (2013-08-10) +------------------- + +- `viewRender` callback ([PR 15]) +- `viewDestroy` callback ([PR 15]) +- `eventDestroy` callback ([PR 111]) +- `handleWindowResize` option ([PR 54]) +- `eventStartEditable`/`startEditable` options ([PR 49]) +- `eventDurationEditable`/`durationEditable` options ([PR 49]) +- specify function for `$.ajax` `data` parameter for JSON event sources ([PR 59]) +- fixed bug with agenda event dropping in wrong column ([PR 55]) +- easier event element z-index customization ([PR 58]) +- classNames on past/future days ([PR 88]) +- allow `null`/`undefined` event titles ([PR 84]) +- small optimize for agenda event rendering ([PR 56]) +- deprecated: + - `viewDisplay` + - `disableDragging` + - `disableResizing` +- bundled with latest jQuery (1.10.2) and jQuery UI (1.10.3) + +[PR 15]: https://github.com/arshaw/fullcalendar/pull/15 +[PR 111]: https://github.com/arshaw/fullcalendar/pull/111 +[PR 54]: https://github.com/arshaw/fullcalendar/pull/54 +[PR 49]: https://github.com/arshaw/fullcalendar/pull/49 +[PR 59]: https://github.com/arshaw/fullcalendar/pull/59 +[PR 55]: https://github.com/arshaw/fullcalendar/pull/55 +[PR 58]: https://github.com/arshaw/fullcalendar/pull/58 +[PR 88]: https://github.com/arshaw/fullcalendar/pull/88 +[PR 84]: https://github.com/arshaw/fullcalendar/pull/84 +[PR 56]: https://github.com/arshaw/fullcalendar/pull/56 + + +v1.6.2 (2013-07-18) +------------------- + +- `hiddenDays` option ([686]) +- bugfix: when `eventRender` returns `false`, incorrect stacking of events ([762]) +- bugfix: couldn't change `event.backgroundImage` when calling `updateEvent` (thx @stephenharris) + +[686]: https://code.google.com/p/fullcalendar/issues/detail?id=686 +[762]: https://code.google.com/p/fullcalendar/issues/detail?id=762 + + +v1.6.1 (2013-04-14) +------------------- + +- fixed event inner content overflow bug ([1783]) +- fixed table header className bug [1772] +- removed text-shadow on events (better for general use, thx @tkrotoff) + +[1783]: https://code.google.com/p/fullcalendar/issues/detail?id=1783 +[1772]: https://code.google.com/p/fullcalendar/issues/detail?id=1772 + + +v1.6.0 (2013-03-18) +------------------- + +- visual facelift, with bootstrap-inspired buttons and colors +- simplified HTML/CSS for events and buttons +- `dayRender`, for modifying a day cell ([191], thx @althaus) +- week numbers on side of calendar ([295]) + - `weekNumber` + - `weekNumberCalculation` + - `weekNumberTitle` + - `W` formatting variable +- finer snapping granularity for agenda view events ([495], thx @ms-doodle-com) +- `eventAfterAllRender` ([753], thx @pdrakeweb) +- `eventDataTransform` (thx @joeyspo) +- `data-date` attributes on cells (thx @Jae) +- expose `$.fullCalendar.dateFormatters` +- when clicking fast on buttons, prevent text selection +- bundled with latest jQuery (1.9.1) and jQuery UI (1.10.2) +- Grunt/Lumbar build system for internal development +- build for Bower package manager +- build for jQuery plugin site + +[191]: https://code.google.com/p/fullcalendar/issues/detail?id=191 +[295]: https://code.google.com/p/fullcalendar/issues/detail?id=295 +[495]: https://code.google.com/p/fullcalendar/issues/detail?id=495 +[753]: https://code.google.com/p/fullcalendar/issues/detail?id=753 + + +v1.5.4 (2012-09-05) +------------------- + +- made compatible with jQuery 1.8.* (thx @archaeron) +- bundled with jQuery 1.8.1 and jQuery UI 1.8.23 + + +v1.5.3 (2012-02-06) +------------------- + +- fixed dragging issue with jQuery UI 1.8.16 ([1168]) +- bundled with jQuery 1.7.1 and jQuery UI 1.8.17 + +[1168]: https://code.google.com/p/fullcalendar/issues/detail?id=1168 + + +v1.5.2 (2011-08-21) +------------------- + +- correctly process UTC "Z" ISO8601 date strings ([750]) + +[750]: https://code.google.com/p/fullcalendar/issues/detail?id=750 + + +v1.5.1 (2011-04-09) +------------------- + +- more flexible ISO8601 date parsing ([814]) +- more flexible parsing of UNIX timestamps ([826]) +- FullCalendar now buildable from source on a Mac ([795]) +- FullCalendar QA'd in FF4 ([883]) +- upgraded to jQuery 1.5.2 (which supports IE9) and jQuery UI 1.8.11 + +[814]: https://code.google.com/p/fullcalendar/issues/detail?id=814 +[826]: https://code.google.com/p/fullcalendar/issues/detail?id=826 +[795]: https://code.google.com/p/fullcalendar/issues/detail?id=795 +[883]: https://code.google.com/p/fullcalendar/issues/detail?id=883 + + +v1.5 (2011-03-19) +----------------- + +- slicker default styling for buttons +- reworked a lot of the calendar's HTML and accompanying CSS (solves [327] and [395]) +- more printer-friendly (fullcalendar-print.css) +- fullcalendar now inherits styles from jquery-ui themes differently. + styles for buttons are distinct from styles for calendar cells. + (solves [299]) +- can now color events through FullCalendar options and Event-Object properties ([117]) + THIS IS NOW THE PREFERRED METHOD OF COLORING EVENTS (as opposed to using className and CSS) + - FullCalendar options: + - eventColor (changes both background and border) + - eventBackgroundColor + - eventBorderColor + - eventTextColor + - Event-Object options: + - color (changes both background and border) + - backgroundColor + - borderColor + - textColor +- can now specify an event source as an *object* with a `url` property (json feed) or + an `events` property (function or array) with additional properties that will + be applied to the entire event source: + - color (changes both background and border) + - backgroudColor + - borderColor + - textColor + - className + - editable + - allDayDefault + - ignoreTimezone + - startParam (for a feed) + - endParam (for a feed) + - ANY OF THE JQUERY $.ajax OPTIONS + allows for easily changing from GET to POST and sending additional parameters ([386]) + allows for easily attaching ajax handlers such as `error` ([754]) + allows for turning caching on ([355]) +- Google Calendar feeds are now specified differently: + - specify a simple string of your feed's URL + - specify an *object* with a `url` property of your feed's URL. + you can include any of the new Event-Source options in this object. + - the old `$.fullCalendar.gcalFeed` method still works +- no more IE7 SSL popup ([504]) +- remove `cacheParam` - use json event source `cache` option instead +- latest jquery/jquery-ui + +[327]: https://code.google.com/p/fullcalendar/issues/detail?id=327 +[395]: https://code.google.com/p/fullcalendar/issues/detail?id=395 +[299]: https://code.google.com/p/fullcalendar/issues/detail?id=299 +[117]: https://code.google.com/p/fullcalendar/issues/detail?id=117 +[386]: https://code.google.com/p/fullcalendar/issues/detail?id=386 +[754]: https://code.google.com/p/fullcalendar/issues/detail?id=754 +[355]: https://code.google.com/p/fullcalendar/issues/detail?id=355 +[504]: https://code.google.com/p/fullcalendar/issues/detail?id=504 + + +v1.4.11 (2011-02-22) +-------------------- + +- fixed rerenderEvents bug ([790]) +- fixed bug with faulty dragging of events from all-day slot in agenda views +- bundled with jquery 1.5 and jquery-ui 1.8.9 + +[790]: https://code.google.com/p/fullcalendar/issues/detail?id=790 + + +v1.4.10 (2011-01-02) +-------------------- + +- fixed bug with resizing event to different week in 5-day month view ([740]) +- fixed bug with events not sticking after a removeEvents call ([757]) +- fixed bug with underlying parseTime method, and other uses of parseInt ([688]) + +[740]: https://code.google.com/p/fullcalendar/issues/detail?id=740 +[757]: https://code.google.com/p/fullcalendar/issues/detail?id=757 +[688]: https://code.google.com/p/fullcalendar/issues/detail?id=688 + + +v1.4.9 (2010-11-16) +------------------- + +- new algorithm for vertically stacking events ([111]) +- resizing an event to a different week ([306]) +- bug: some events not rendered with consecutive calls to addEventSource ([679]) + +[111]: https://code.google.com/p/fullcalendar/issues/detail?id=111 +[306]: https://code.google.com/p/fullcalendar/issues/detail?id=306 +[679]: https://code.google.com/p/fullcalendar/issues/detail?id=679 + + +v1.4.8 (2010-10-16) +------------------- + +- ignoreTimezone option (set to `false` to process UTC offsets in ISO8601 dates) +- bugfixes + - event refetching not being called under certain conditions ([417], [554]) + - event refetching being called multiple times under certain conditions ([586], [616]) + - selection cannot be triggered by right mouse button ([558]) + - agenda view left axis sized incorrectly ([465]) + - IE js error when calendar is too narrow ([517]) + - agenda view looks strange when no scrollbars ([235]) + - improved parsing of ISO8601 dates with UTC offsets +- $.fullCalendar.version +- an internal refactor of the code, for easier future development and modularity + +[417]: https://code.google.com/p/fullcalendar/issues/detail?id=417 +[554]: https://code.google.com/p/fullcalendar/issues/detail?id=554 +[586]: https://code.google.com/p/fullcalendar/issues/detail?id=586 +[616]: https://code.google.com/p/fullcalendar/issues/detail?id=616 +[558]: https://code.google.com/p/fullcalendar/issues/detail?id=558 +[465]: https://code.google.com/p/fullcalendar/issues/detail?id=465 +[517]: https://code.google.com/p/fullcalendar/issues/detail?id=517 +[235]: https://code.google.com/p/fullcalendar/issues/detail?id=235 + + +v1.4.7 (2010-07-05) +------------------- + +- "dropping" external objects onto the calendar + - droppable (boolean, to turn on/off) + - dropAccept (to filter which events the calendar will accept) + - drop (trigger) +- selectable options can now be specified with a View Option Hash +- bugfixes + - dragged & reverted events having wrong time text ([406]) + - bug rendering events that have an endtime with seconds, but no hours/minutes ([477]) + - gotoDate date overflow bug ([429]) + - wrong date reported when clicking on edge of last column in agenda views [412] +- support newlines in event titles +- select/unselect callbacks now passes native js event + +[406]: https://code.google.com/p/fullcalendar/issues/detail?id=406 +[477]: https://code.google.com/p/fullcalendar/issues/detail?id=477 +[429]: https://code.google.com/p/fullcalendar/issues/detail?id=429 +[412]: https://code.google.com/p/fullcalendar/issues/detail?id=412 + + +v1.4.6 (2010-05-31) +------------------- + +- "selecting" days or timeslots + - options: selectable, selectHelper, unselectAuto, unselectCancel + - callbacks: select, unselect + - methods: select, unselect +- when dragging an event, the highlighting reflects the duration of the event +- code compressing by Google Closure Compiler +- bundled with jQuery 1.4.2 and jQuery UI 1.8.1 + + +v1.4.5 (2010-02-21) +------------------- + +- lazyFetching option, which can force the calendar to fetch events on every view/date change +- scroll state of agenda views are preserved when switching back to view +- bugfixes + - calling methods on an uninitialized fullcalendar throws error + - IE6/7 bug where an entire view becomes invisible ([320]) + - error when rendering a hidden calendar (in jquery ui tabs for example) in IE ([340]) + - interconnected bugs related to calendar resizing and scrollbars + - when switching views or clicking prev/next, calendar would "blink" ([333]) + - liquid-width calendar's events shifted (depending on initial height of browser) ([341]) + - more robust underlying algorithm for calendar resizing + +[320]: https://code.google.com/p/fullcalendar/issues/detail?id=320 +[340]: https://code.google.com/p/fullcalendar/issues/detail?id=340 +[333]: https://code.google.com/p/fullcalendar/issues/detail?id=333 +[341]: https://code.google.com/p/fullcalendar/issues/detail?id=341 + + +v1.4.4 (2010-02-03) +------------------- + +- optimized event rendering in all views (events render in 1/10 the time) +- gotoDate() does not force the calendar to unnecessarily rerender +- render() method now correctly readjusts height + + +v1.4.3 (2009-12-22) +------------------- + +- added destroy method +- Google Calendar event pages respect currentTimezone +- caching now handled by jQuery's ajax +- protection from setting aspectRatio to zero +- bugfixes + - parseISO8601 and DST caused certain events to display day before + - button positioning problem in IE6 + - ajax event source removed after recently being added, events still displayed + - event not displayed when end is an empty string + - dynamically setting calendar height when no events have been fetched, throws error + + +v1.4.2 (2009-12-02) +------------------- + +- eventAfterRender trigger +- getDate & getView methods +- height & contentHeight options (explicitly sets the pixel height) +- minTime & maxTime options (restricts shown hours in agenda view) +- getters [for all options] and setters [for height, contentHeight, and aspectRatio ONLY! stay tuned..] +- render method now readjusts calendar's size +- bugfixes + - lightbox scripts that use iframes (like fancybox) + - day-of-week classNames were off when firstDay=1 + - guaranteed space on right side of agenda events (even when stacked) + - accepts ISO8601 dates with a space (instead of 'T') + + +v1.4.1 (2009-10-31) +------------------- + +- can exclude weekends with new 'weekends' option +- gcal feed 'currentTimezone' option +- bugfixes + - year/month/date option sometimes wouldn't set correctly (depending on current date) + - daylight savings issue caused agenda views to start at 1am (for BST users) +- cleanup of gcal.js code + + +v1.4 (2009-10-19) +----------------- + +- agendaWeek and agendaDay views +- added some options for agenda views: + - allDaySlot + - allDayText + - firstHour + - slotMinutes + - defaultEventMinutes + - axisFormat +- modified some existing options/triggers to work with agenda views: + - dragOpacity and timeFormat can now accept a "View Hash" (a new concept) + - dayClick now has an allDay parameter + - eventDrop now has an an allDay parameter + (this will affect those who use revertFunc, adjust parameter list) +- added 'prevYear' and 'nextYear' for buttons in header +- minor change for theme users, ui-state-hover not applied to active/inactive buttons +- added event-color-changing example in docs +- better defaults for right-to-left themed button icons + + +v1.3.2 (2009-10-13) +------------------- + +- Bugfixes (please upgrade from 1.3.1!) + - squashed potential infinite loop when addMonths and addDays + is called with an invalid date + - $.fullCalendar.parseDate() now correctly parses IETF format + - when switching views, the 'today' button sticks inactive, fixed +- gotoDate now can accept a single Date argument +- documentation for changes in 1.3.1 and 1.3.2 now on website + + +v1.3.1 (2009-09-30) +------------------- + +- Important Bugfixes (please upgrade from 1.3!) + - When current date was late in the month, for long months, and prev/next buttons + were clicked in month-view, some months would be skipped/repeated + - In certain time zones, daylight savings time would cause certain days + to be misnumbered in month-view +- Subtle change in way week interval is chosen when switching from month to basicWeek/basicDay view +- Added 'allDayDefault' option +- Added 'changeView' and 'render' methods + + +v1.3 (2009-09-21) +----------------- + +- different 'views': month/basicWeek/basicDay +- more flexible 'header' system for buttons +- themable by jQuery UI themes +- resizable events (require jQuery UI resizable plugin) +- rescoped & rewritten CSS, enhanced default look +- cleaner css & rendering techniques for right-to-left +- reworked options & API to support multiple views / be consistent with jQuery UI +- refactoring of entire codebase + - broken into different JS & CSS files, assembled w/ build scripts + - new test suite for new features, uses firebug-lite +- refactored docs +- Options + - + date + - + defaultView + - + aspectRatio + - + disableResizing + - + monthNames (use instead of $.fullCalendar.monthNames) + - + monthNamesShort (use instead of $.fullCalendar.monthAbbrevs) + - + dayNames (use instead of $.fullCalendar.dayNames) + - + dayNamesShort (use instead of $.fullCalendar.dayAbbrevs) + - + theme + - + buttonText + - + buttonIcons + - x draggable -> editable/disableDragging + - x fixedWeeks -> weekMode + - x abbrevDayHeadings -> columnFormat + - x buttons/title -> header + - x eventDragOpacity -> dragOpacity + - x eventRevertDuration -> dragRevertDuration + - x weekStart -> firstDay + - x rightToLeft -> isRTL + - x showTime (use 'allDay' CalEvent property instead) +- Triggered Actions + - + eventResizeStart + - + eventResizeStop + - + eventResize + - x monthDisplay -> viewDisplay + - x resize -> windowResize + - 'eventDrop' params changed, can revert if ajax cuts out +- CalEvent Properties + - x showTime -> allDay + - x draggable -> editable + - 'end' is now INCLUSIVE when allDay=true + - 'url' now produces a real tag, more native clicking/tab behavior +- Methods: + - + renderEvent + - x prevMonth -> prev + - x nextMonth -> next + - x prevYear/nextYear -> moveDate + - x refresh -> rerenderEvents/refetchEvents + - x removeEvent -> removeEvents + - x getEventsByID -> clientEvents +- Utilities: + - 'formatDate' format string completely changed (inspired by jQuery UI datepicker + datejs) + - 'formatDates' added to support date-ranges +- Google Calendar Options: + - x draggable -> editable +- Bugfixes + - gcal extension fetched 25 results max, now fetches all + + +v1.2.1 (2009-06-29) +------------------- + +- bugfixes + - allows and corrects invalid end dates for events + - doesn't throw an error in IE while rendering when display:none + - fixed 'loading' callback when used w/ multiple addEventSource calls + - gcal className can now be an array + + +v1.2 (2009-05-31) +----------------- + +- expanded API + - 'className' CalEvent attribute + - 'source' CalEvent attribute + - dynamically get/add/remove/update events of current month + - locale improvements: change month/day name text + - better date formatting ($.fullCalendar.formatDate) + - multiple 'event sources' allowed + - dynamically add/remove event sources +- options for prevYear and nextYear buttons +- docs have been reworked (include addition of Google Calendar docs) +- changed behavior of parseDate for number strings + (now interpets as unix timestamp, not MS times) +- bugfixes + - rightToLeft month start bug + - off-by-one errors with month formatting commands + - events from previous months sticking when clicking prev/next quickly +- Google Calendar API changed to work w/ multiple event sources + - can also provide 'className' and 'draggable' options +- date utilties moved from $ to $.fullCalendar +- more documentation in source code +- minified version of fullcalendar.js +- test suit (available from svn) +- top buttons now use `'); + buttonEl.addEventListener('click', buttonClick); + groupChildren.push(buttonEl); + } + } + }); + if (groupChildren.length > 1) { + groupEl = document.createElement('div'); + var buttonGroupClassName = theme.getClass('buttonGroup'); + if (isOnlyButtons && buttonGroupClassName) { + groupEl.classList.add(buttonGroupClassName); + } + appendToElement(groupEl, groupChildren); + sectionEl.appendChild(groupEl); + } + else { + appendToElement(sectionEl, groupChildren); // 1 or 0 children + } + }); + } + return sectionEl; + }; + Toolbar.prototype.updateToday = function (isTodayEnabled) { + this.toggleButtonEnabled('today', isTodayEnabled); + }; + Toolbar.prototype.updatePrev = function (isPrevEnabled) { + this.toggleButtonEnabled('prev', isPrevEnabled); + }; + Toolbar.prototype.updateNext = function (isNextEnabled) { + this.toggleButtonEnabled('next', isNextEnabled); + }; + Toolbar.prototype.updateTitle = function (text) { + findElements(this.el, 'h2').forEach(function (titleEl) { + titleEl.innerText = text; + }); + }; + Toolbar.prototype.updateActiveButton = function (buttonName) { + var className = this.theme.getClass('buttonActive'); + findElements(this.el, 'button').forEach(function (buttonEl) { + if (buttonName && buttonEl.classList.contains('fc-' + buttonName + '-button')) { + buttonEl.classList.add(className); + } + else { + buttonEl.classList.remove(className); + } + }); + }; + Toolbar.prototype.toggleButtonEnabled = function (buttonName, bool) { + findElements(this.el, '.fc-' + buttonName + '-button').forEach(function (buttonEl) { + buttonEl.disabled = !bool; + }); + }; + return Toolbar; + }(Component)); + + var CalendarComponent = /** @class */ (function (_super) { + __extends(CalendarComponent, _super); + function CalendarComponent(context, el) { + var _this = _super.call(this, context) || this; + _this._renderToolbars = memoizeRendering(_this.renderToolbars); + _this.buildViewPropTransformers = memoize(buildViewPropTransformers); + _this.el = el; + prependToElement(el, _this.contentEl = createElement('div', { className: 'fc-view-container' })); + var calendar = _this.calendar; + for (var _i = 0, _a = calendar.pluginSystem.hooks.viewContainerModifiers; _i < _a.length; _i++) { + var modifyViewContainer = _a[_i]; + modifyViewContainer(_this.contentEl, calendar); + } + _this.toggleElClassNames(true); + _this.computeTitle = memoize(computeTitle); + _this.parseBusinessHours = memoize(function (input) { + return parseBusinessHours(input, _this.calendar); + }); + return _this; + } + CalendarComponent.prototype.destroy = function () { + if (this.header) { + this.header.destroy(); + } + if (this.footer) { + this.footer.destroy(); + } + if (this.view) { + this.view.destroy(); + } + removeElement(this.contentEl); + this.toggleElClassNames(false); + _super.prototype.destroy.call(this); + }; + CalendarComponent.prototype.toggleElClassNames = function (bool) { + var classList = this.el.classList; + var dirClassName = 'fc-' + this.opt('dir'); + var themeClassName = this.theme.getClass('widget'); + if (bool) { + classList.add('fc'); + classList.add(dirClassName); + classList.add(themeClassName); + } + else { + classList.remove('fc'); + classList.remove(dirClassName); + classList.remove(themeClassName); + } + }; + CalendarComponent.prototype.render = function (props) { + this.freezeHeight(); + var title = this.computeTitle(props.dateProfile, props.viewSpec.options); + this._renderToolbars(props.viewSpec, props.dateProfile, props.currentDate, props.dateProfileGenerator, title); + this.renderView(props, title); + this.updateSize(); + this.thawHeight(); + }; + CalendarComponent.prototype.renderToolbars = function (viewSpec, dateProfile, currentDate, dateProfileGenerator, title) { + var headerLayout = this.opt('header'); + var footerLayout = this.opt('footer'); + var now = this.calendar.getNow(); + var todayInfo = dateProfileGenerator.build(now); + var prevInfo = dateProfileGenerator.buildPrev(dateProfile, currentDate); + var nextInfo = dateProfileGenerator.buildNext(dateProfile, currentDate); + var toolbarProps = { + title: title, + activeButton: viewSpec.type, + isTodayEnabled: todayInfo.isValid && !rangeContainsMarker(dateProfile.currentRange, now), + isPrevEnabled: prevInfo.isValid, + isNextEnabled: nextInfo.isValid + }; + if (headerLayout) { + if (!this.header) { + this.header = new Toolbar(this.context, 'fc-header-toolbar'); + prependToElement(this.el, this.header.el); + } + this.header.receiveProps(__assign({ layout: headerLayout }, toolbarProps)); + } + else if (this.header) { + this.header.destroy(); + this.header = null; + } + if (footerLayout) { + if (!this.footer) { + this.footer = new Toolbar(this.context, 'fc-footer-toolbar'); + appendToElement(this.el, this.footer.el); + } + this.footer.receiveProps(__assign({ layout: footerLayout }, toolbarProps)); + } + else if (this.footer) { + this.footer.destroy(); + this.footer = null; + } + }; + CalendarComponent.prototype.renderView = function (props, title) { + var view = this.view; + var viewSpec = props.viewSpec, dateProfileGenerator = props.dateProfileGenerator; + if (!view || view.viewSpec !== viewSpec) { + if (view) { + view.destroy(); + } + view = this.view = new viewSpec['class']({ + calendar: this.calendar, + view: null, + dateEnv: this.dateEnv, + theme: this.theme, + options: viewSpec.options + }, viewSpec, dateProfileGenerator, this.contentEl); + } + else { + view.addScroll(view.queryScroll()); + } + view.title = title; // for the API + var viewProps = { + dateProfile: props.dateProfile, + businessHours: this.parseBusinessHours(viewSpec.options.businessHours), + eventStore: props.eventStore, + eventUiBases: props.eventUiBases, + dateSelection: props.dateSelection, + eventSelection: props.eventSelection, + eventDrag: props.eventDrag, + eventResize: props.eventResize + }; + var transformers = this.buildViewPropTransformers(this.calendar.pluginSystem.hooks.viewPropsTransformers); + for (var _i = 0, transformers_1 = transformers; _i < transformers_1.length; _i++) { + var transformer = transformers_1[_i]; + __assign(viewProps, transformer.transform(viewProps, viewSpec, props, view)); + } + view.receiveProps(viewProps); + }; + // Sizing + // ----------------------------------------------------------------------------------------------------------------- + CalendarComponent.prototype.updateSize = function (isResize) { + if (isResize === void 0) { isResize = false; } + var view = this.view; + if (isResize) { + view.addScroll(view.queryScroll()); + } + if (isResize || this.isHeightAuto == null) { + this.computeHeightVars(); + } + view.updateSize(isResize, this.viewHeight, this.isHeightAuto); + view.updateNowIndicator(); // we need to guarantee this will run after updateSize + view.popScroll(isResize); + }; + CalendarComponent.prototype.computeHeightVars = function () { + var calendar = this.calendar; // yuck. need to handle dynamic options + var heightInput = calendar.opt('height'); + var contentHeightInput = calendar.opt('contentHeight'); + this.isHeightAuto = heightInput === 'auto' || contentHeightInput === 'auto'; + if (typeof contentHeightInput === 'number') { // exists and not 'auto' + this.viewHeight = contentHeightInput; + } + else if (typeof contentHeightInput === 'function') { // exists and is a function + this.viewHeight = contentHeightInput(); + } + else if (typeof heightInput === 'number') { // exists and not 'auto' + this.viewHeight = heightInput - this.queryToolbarsHeight(); + } + else if (typeof heightInput === 'function') { // exists and is a function + this.viewHeight = heightInput() - this.queryToolbarsHeight(); + } + else if (heightInput === 'parent') { // set to height of parent element + this.viewHeight = this.el.parentNode.offsetHeight - this.queryToolbarsHeight(); + } + else { + this.viewHeight = Math.round(this.contentEl.offsetWidth / + Math.max(calendar.opt('aspectRatio'), .5)); + } + }; + CalendarComponent.prototype.queryToolbarsHeight = function () { + var height = 0; + if (this.header) { + height += computeHeightAndMargins(this.header.el); + } + if (this.footer) { + height += computeHeightAndMargins(this.footer.el); + } + return height; + }; + // Height "Freezing" + // ----------------------------------------------------------------------------------------------------------------- + CalendarComponent.prototype.freezeHeight = function () { + applyStyle(this.el, { + height: this.el.offsetHeight, + overflow: 'hidden' + }); + }; + CalendarComponent.prototype.thawHeight = function () { + applyStyle(this.el, { + height: '', + overflow: '' + }); + }; + return CalendarComponent; + }(Component)); + // Title and Date Formatting + // ----------------------------------------------------------------------------------------------------------------- + // Computes what the title at the top of the calendar should be for this view + function computeTitle(dateProfile, viewOptions) { + var range; + // for views that span a large unit of time, show the proper interval, ignoring stray days before and after + if (/^(year|month)$/.test(dateProfile.currentRangeUnit)) { + range = dateProfile.currentRange; + } + else { // for day units or smaller, use the actual day range + range = dateProfile.activeRange; + } + return this.dateEnv.formatRange(range.start, range.end, createFormatter(viewOptions.titleFormat || computeTitleFormat(dateProfile), viewOptions.titleRangeSeparator), { isEndExclusive: dateProfile.isRangeAllDay }); + } + // Generates the format string that should be used to generate the title for the current date range. + // Attempts to compute the most appropriate format if not explicitly specified with `titleFormat`. + function computeTitleFormat(dateProfile) { + var currentRangeUnit = dateProfile.currentRangeUnit; + if (currentRangeUnit === 'year') { + return { year: 'numeric' }; + } + else if (currentRangeUnit === 'month') { + return { year: 'numeric', month: 'long' }; // like "September 2014" + } + else { + var days = diffWholeDays(dateProfile.currentRange.start, dateProfile.currentRange.end); + if (days !== null && days > 1) { + // multi-day range. shorter, like "Sep 9 - 10 2014" + return { year: 'numeric', month: 'short', day: 'numeric' }; + } + else { + // one day. longer, like "September 9 2014" + return { year: 'numeric', month: 'long', day: 'numeric' }; + } + } + } + // Plugin + // ----------------------------------------------------------------------------------------------------------------- + function buildViewPropTransformers(theClasses) { + return theClasses.map(function (theClass) { + return new theClass(); + }); + } + + var Interaction = /** @class */ (function () { + function Interaction(settings) { + this.component = settings.component; + } + Interaction.prototype.destroy = function () { + }; + return Interaction; + }()); + function parseInteractionSettings(component, input) { + return { + component: component, + el: input.el, + useEventCenter: input.useEventCenter != null ? input.useEventCenter : true + }; + } + function interactionSettingsToStore(settings) { + var _a; + return _a = {}, + _a[settings.component.uid] = settings, + _a; + } + // global state + var interactionSettingsStore = {}; + + /* + Detects when the user clicks on an event within a DateComponent + */ + var EventClicking = /** @class */ (function (_super) { + __extends(EventClicking, _super); + function EventClicking(settings) { + var _this = _super.call(this, settings) || this; + _this.handleSegClick = function (ev, segEl) { + var component = _this.component; + var seg = getElSeg(segEl); + if (seg && // might be the
surrounding the more link + component.isValidSegDownEl(ev.target)) { + // our way to simulate a link click for elements that can't be tags + // grab before trigger fired in case trigger trashes DOM thru rerendering + var hasUrlContainer = elementClosest(ev.target, '.fc-has-url'); + var url = hasUrlContainer ? hasUrlContainer.querySelector('a[href]').href : ''; + component.publiclyTrigger('eventClick', [ + { + el: segEl, + event: new EventApi(component.calendar, seg.eventRange.def, seg.eventRange.instance), + jsEvent: ev, + view: component.view + } + ]); + if (url && !ev.defaultPrevented) { + window.location.href = url; + } + } + }; + var component = settings.component; + _this.destroy = listenBySelector(component.el, 'click', component.fgSegSelector + ',' + component.bgSegSelector, _this.handleSegClick); + return _this; + } + return EventClicking; + }(Interaction)); + + /* + Triggers events and adds/removes core classNames when the user's pointer + enters/leaves event-elements of a component. + */ + var EventHovering = /** @class */ (function (_super) { + __extends(EventHovering, _super); + function EventHovering(settings) { + var _this = _super.call(this, settings) || this; + // for simulating an eventMouseLeave when the event el is destroyed while mouse is over it + _this.handleEventElRemove = function (el) { + if (el === _this.currentSegEl) { + _this.handleSegLeave(null, _this.currentSegEl); + } + }; + _this.handleSegEnter = function (ev, segEl) { + if (getElSeg(segEl)) { // TODO: better way to make sure not hovering over more+ link or its wrapper + segEl.classList.add('fc-allow-mouse-resize'); + _this.currentSegEl = segEl; + _this.triggerEvent('eventMouseEnter', ev, segEl); + } + }; + _this.handleSegLeave = function (ev, segEl) { + if (_this.currentSegEl) { + segEl.classList.remove('fc-allow-mouse-resize'); + _this.currentSegEl = null; + _this.triggerEvent('eventMouseLeave', ev, segEl); + } + }; + var component = settings.component; + _this.removeHoverListeners = listenToHoverBySelector(component.el, component.fgSegSelector + ',' + component.bgSegSelector, _this.handleSegEnter, _this.handleSegLeave); + component.calendar.on('eventElRemove', _this.handleEventElRemove); + return _this; + } + EventHovering.prototype.destroy = function () { + this.removeHoverListeners(); + this.component.calendar.off('eventElRemove', this.handleEventElRemove); + }; + EventHovering.prototype.triggerEvent = function (publicEvName, ev, segEl) { + var component = this.component; + var seg = getElSeg(segEl); + if (!ev || component.isValidSegDownEl(ev.target)) { + component.publiclyTrigger(publicEvName, [ + { + el: segEl, + event: new EventApi(this.component.calendar, seg.eventRange.def, seg.eventRange.instance), + jsEvent: ev, + view: component.view + } + ]); + } + }; + return EventHovering; + }(Interaction)); + + var StandardTheme = /** @class */ (function (_super) { + __extends(StandardTheme, _super); + function StandardTheme() { + return _super !== null && _super.apply(this, arguments) || this; + } + return StandardTheme; + }(Theme)); + StandardTheme.prototype.classes = { + widget: 'fc-unthemed', + widgetHeader: 'fc-widget-header', + widgetContent: 'fc-widget-content', + buttonGroup: 'fc-button-group', + button: 'fc-button fc-button-primary', + buttonActive: 'fc-button-active', + popoverHeader: 'fc-widget-header', + popoverContent: 'fc-widget-content', + // day grid + headerRow: 'fc-widget-header', + dayRow: 'fc-widget-content', + // list view + listView: 'fc-widget-content' + }; + StandardTheme.prototype.baseIconClass = 'fc-icon'; + StandardTheme.prototype.iconClasses = { + close: 'fc-icon-x', + prev: 'fc-icon-chevron-left', + next: 'fc-icon-chevron-right', + prevYear: 'fc-icon-chevrons-left', + nextYear: 'fc-icon-chevrons-right' + }; + StandardTheme.prototype.iconOverrideOption = 'buttonIcons'; + StandardTheme.prototype.iconOverrideCustomButtonOption = 'icon'; + StandardTheme.prototype.iconOverridePrefix = 'fc-icon-'; + + var Calendar = /** @class */ (function () { + function Calendar(el, overrides) { + var _this = this; + this.parseRawLocales = memoize(parseRawLocales); + this.buildLocale = memoize(buildLocale); + this.buildDateEnv = memoize(buildDateEnv); + this.buildTheme = memoize(buildTheme); + this.buildEventUiSingleBase = memoize(this._buildEventUiSingleBase); + this.buildSelectionConfig = memoize(this._buildSelectionConfig); + this.buildEventUiBySource = memoizeOutput(buildEventUiBySource, isObjectsSimilar); + this.buildEventUiBases = memoize(buildEventUiBases); + this.interactionsStore = {}; + this.actionQueue = []; + this.isReducing = false; + // isDisplaying: boolean = false // installed in DOM? accepting renders? + this.needsRerender = false; // needs a render? + this.needsFullRerender = false; + this.isRendering = false; // currently in the executeRender function? + this.renderingPauseDepth = 0; + this.buildDelayedRerender = memoize(buildDelayedRerender); + this.afterSizingTriggers = {}; + this.isViewUpdated = false; + this.isDatesUpdated = false; + this.isEventsUpdated = false; + this.el = el; + this.optionsManager = new OptionsManager(overrides || {}); + this.pluginSystem = new PluginSystem(); + // only do once. don't do in handleOptions. because can't remove plugins + this.addPluginInputs(this.optionsManager.computed.plugins || []); + this.handleOptions(this.optionsManager.computed); + this.publiclyTrigger('_init'); // for tests + this.hydrate(); + this.calendarInteractions = this.pluginSystem.hooks.calendarInteractions + .map(function (calendarInteractionClass) { + return new calendarInteractionClass(_this); + }); + } + Calendar.prototype.addPluginInputs = function (pluginInputs) { + var pluginDefs = refinePluginDefs(pluginInputs); + for (var _i = 0, pluginDefs_1 = pluginDefs; _i < pluginDefs_1.length; _i++) { + var pluginDef = pluginDefs_1[_i]; + this.pluginSystem.add(pluginDef); + } + }; + Object.defineProperty(Calendar.prototype, "view", { + // public API + get: function () { + return this.component ? this.component.view : null; + }, + enumerable: true, + configurable: true + }); + // Public API for rendering + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.render = function () { + if (!this.component) { + this.renderableEventStore = createEmptyEventStore(); + this.bindHandlers(); + this.executeRender(); + } + else { + this.requestRerender(true); + } + }; + Calendar.prototype.destroy = function () { + if (this.component) { + this.unbindHandlers(); + this.component.destroy(); // don't null-out. in case API needs access + this.component = null; // umm ??? + for (var _i = 0, _a = this.calendarInteractions; _i < _a.length; _i++) { + var interaction = _a[_i]; + interaction.destroy(); + } + this.publiclyTrigger('_destroyed'); + } + }; + // Handlers + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.bindHandlers = function () { + var _this = this; + // event delegation for nav links + this.removeNavLinkListener = listenBySelector(this.el, 'click', 'a[data-goto]', function (ev, anchorEl) { + var gotoOptions = anchorEl.getAttribute('data-goto'); + gotoOptions = gotoOptions ? JSON.parse(gotoOptions) : {}; + var dateEnv = _this.dateEnv; + var dateMarker = dateEnv.createMarker(gotoOptions.date); + var viewType = gotoOptions.type; + // property like "navLinkDayClick". might be a string or a function + var customAction = _this.viewOpt('navLink' + capitaliseFirstLetter(viewType) + 'Click'); + if (typeof customAction === 'function') { + customAction(dateEnv.toDate(dateMarker), ev); + } + else { + if (typeof customAction === 'string') { + viewType = customAction; + } + _this.zoomTo(dateMarker, viewType); + } + }); + if (this.opt('handleWindowResize')) { + window.addEventListener('resize', this.windowResizeProxy = debounce(// prevents rapid calls + this.windowResize.bind(this), this.opt('windowResizeDelay'))); + } + }; + Calendar.prototype.unbindHandlers = function () { + this.removeNavLinkListener(); + if (this.windowResizeProxy) { + window.removeEventListener('resize', this.windowResizeProxy); + this.windowResizeProxy = null; + } + }; + // Dispatcher + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.hydrate = function () { + var _this = this; + this.state = this.buildInitialState(); + var rawSources = this.opt('eventSources') || []; + var singleRawSource = this.opt('events'); + var sources = []; // parsed + if (singleRawSource) { + rawSources.unshift(singleRawSource); + } + for (var _i = 0, rawSources_1 = rawSources; _i < rawSources_1.length; _i++) { + var rawSource = rawSources_1[_i]; + var source = parseEventSource(rawSource, this); + if (source) { + sources.push(source); + } + } + this.batchRendering(function () { + _this.dispatch({ type: 'INIT' }); // pass in sources here? + _this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: sources }); + _this.dispatch({ + type: 'SET_VIEW_TYPE', + viewType: _this.opt('defaultView') || _this.pluginSystem.hooks.defaultView + }); + }); + }; + Calendar.prototype.buildInitialState = function () { + return { + viewType: null, + loadingLevel: 0, + eventSourceLoadingLevel: 0, + currentDate: this.getInitialDate(), + dateProfile: null, + eventSources: {}, + eventStore: createEmptyEventStore(), + dateSelection: null, + eventSelection: '', + eventDrag: null, + eventResize: null + }; + }; + Calendar.prototype.dispatch = function (action) { + this.actionQueue.push(action); + if (!this.isReducing) { + this.isReducing = true; + var oldState = this.state; + while (this.actionQueue.length) { + this.state = this.reduce(this.state, this.actionQueue.shift(), this); + } + var newState = this.state; + this.isReducing = false; + if (!oldState.loadingLevel && newState.loadingLevel) { + this.publiclyTrigger('loading', [true]); + } + else if (oldState.loadingLevel && !newState.loadingLevel) { + this.publiclyTrigger('loading', [false]); + } + var view = this.component && this.component.view; + if (oldState.eventStore !== newState.eventStore || this.needsFullRerender) { + if (oldState.eventStore) { + this.isEventsUpdated = true; + } + } + if (oldState.dateProfile !== newState.dateProfile || this.needsFullRerender) { + if (oldState.dateProfile && view) { // why would view be null!? + this.publiclyTrigger('datesDestroy', [ + { + view: view, + el: view.el + } + ]); + } + this.isDatesUpdated = true; + } + if (oldState.viewType !== newState.viewType || this.needsFullRerender) { + if (oldState.viewType && view) { // why would view be null!? + this.publiclyTrigger('viewSkeletonDestroy', [ + { + view: view, + el: view.el + } + ]); + } + this.isViewUpdated = true; + } + this.requestRerender(); + } + }; + Calendar.prototype.reduce = function (state, action, calendar) { + return reduce(state, action, calendar); + }; + // Render Queue + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.requestRerender = function (needsFull) { + if (needsFull === void 0) { needsFull = false; } + this.needsRerender = true; + this.needsFullRerender = this.needsFullRerender || needsFull; + this.delayedRerender(); // will call a debounced-version of tryRerender + }; + Calendar.prototype.tryRerender = function () { + if (this.component && // must be accepting renders + this.needsRerender && // indicates that a rerender was requested + !this.renderingPauseDepth && // not paused + !this.isRendering // not currently in the render loop + ) { + this.executeRender(); + } + }; + Calendar.prototype.batchRendering = function (func) { + this.renderingPauseDepth++; + func(); + this.renderingPauseDepth--; + if (this.needsRerender) { + this.requestRerender(); + } + }; + // Rendering + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.executeRender = function () { + var needsFullRerender = this.needsFullRerender; // save before clearing + // clear these BEFORE the render so that new values will accumulate during render + this.needsRerender = false; + this.needsFullRerender = false; + this.isRendering = true; + this.renderComponent(needsFullRerender); + this.isRendering = false; + // received a rerender request while rendering + if (this.needsRerender) { + this.delayedRerender(); + } + }; + /* + don't call this directly. use executeRender instead + */ + Calendar.prototype.renderComponent = function (needsFull) { + var _a = this, state = _a.state, component = _a.component; + var viewType = state.viewType; + var viewSpec = this.viewSpecs[viewType]; + var savedScroll = (needsFull && component) ? component.view.queryScroll() : null; + if (!viewSpec) { + throw new Error("View type \"" + viewType + "\" is not valid"); + } + // if event sources are still loading and progressive rendering hasn't been enabled, + // keep rendering the last fully loaded set of events + var renderableEventStore = this.renderableEventStore = + (state.eventSourceLoadingLevel && !this.opt('progressiveEventRendering')) ? + this.renderableEventStore : + state.eventStore; + var eventUiSingleBase = this.buildEventUiSingleBase(viewSpec.options); + var eventUiBySource = this.buildEventUiBySource(state.eventSources); + var eventUiBases = this.eventUiBases = this.buildEventUiBases(renderableEventStore.defs, eventUiSingleBase, eventUiBySource); + if (needsFull || !component) { + if (component) { + component.freezeHeight(); // next component will unfreeze it + component.destroy(); + } + component = this.component = new CalendarComponent({ + calendar: this, + view: null, + dateEnv: this.dateEnv, + theme: this.theme, + options: this.optionsManager.computed + }, this.el); + } + component.receiveProps(__assign({}, state, { viewSpec: viewSpec, dateProfile: state.dateProfile, dateProfileGenerator: this.dateProfileGenerators[viewType], eventStore: renderableEventStore, eventUiBases: eventUiBases, dateSelection: state.dateSelection, eventSelection: state.eventSelection, eventDrag: state.eventDrag, eventResize: state.eventResize })); + if (savedScroll) { + component.view.applyScroll(savedScroll, false); + } + if (this.isViewUpdated) { + this.isViewUpdated = false; + this.publiclyTrigger('viewSkeletonRender', [ + { + view: component.view, + el: component.view.el + } + ]); + } + if (this.isDatesUpdated) { + this.isDatesUpdated = false; + this.publiclyTrigger('datesRender', [ + { + view: component.view, + el: component.view.el + } + ]); + } + if (this.isEventsUpdated) { + this.isEventsUpdated = false; + } + this.releaseAfterSizingTriggers(); + }; + // Options + // ----------------------------------------------------------------------------------------------------------------- + /* + Not meant for public API + */ + Calendar.prototype.resetOptions = function (options) { + var _this = this; + var changeHandlers = this.pluginSystem.hooks.optionChangeHandlers; + var oldOptions = this.optionsManager.overrides; + var oldNormalOptions = {}; + var normalOptions = {}; + var specialOptions = {}; + for (var name_1 in oldOptions) { + if (!changeHandlers[name_1]) { + oldNormalOptions[name_1] = oldOptions[name_1]; + } + } + for (var name_2 in options) { + if (changeHandlers[name_2]) { + specialOptions[name_2] = options[name_2]; + } + else { + normalOptions[name_2] = options[name_2]; + } + } + this.batchRendering(function () { + if (anyKeysRemoved(oldNormalOptions, normalOptions)) { + _this.processOptions(options, 'reset'); + } + else { + _this.processOptions(computeChangedProps(oldNormalOptions, normalOptions)); + } + // handle special options last + for (var name_3 in specialOptions) { + changeHandlers[name_3](specialOptions[name_3], _this); + } + }); + }; + /* + Not meant for public API. Won't give the same precedence that setOption does + */ + Calendar.prototype.setOptions = function (options) { + var _this = this; + var changeHandlers = this.pluginSystem.hooks.optionChangeHandlers; + var normalOptions = {}; + var specialOptions = {}; + for (var name_4 in options) { + if (changeHandlers[name_4]) { + specialOptions[name_4] = options[name_4]; + } + else { + normalOptions[name_4] = options[name_4]; + } + } + this.batchRendering(function () { + _this.processOptions(normalOptions); + // handle special options last + for (var name_5 in specialOptions) { + changeHandlers[name_5](specialOptions[name_5], _this); + } + }); + }; + Calendar.prototype.processOptions = function (options, mode) { + var _this = this; + var oldDateEnv = this.dateEnv; // do this before handleOptions + var isTimeZoneDirty = false; + var isSizeDirty = false; + var anyDifficultOptions = false; + for (var name_6 in options) { + if (/^(height|contentHeight|aspectRatio)$/.test(name_6)) { + isSizeDirty = true; + } + else if (/^(defaultDate|defaultView)$/.test(name_6)) ; + else { + anyDifficultOptions = true; + if (name_6 === 'timeZone') { + isTimeZoneDirty = true; + } + } + } + if (mode === 'reset') { + anyDifficultOptions = true; + this.optionsManager.reset(options); + } + else if (mode === 'dynamic') { + this.optionsManager.addDynamic(options); // takes higher precedence + } + else { + this.optionsManager.add(options); + } + if (anyDifficultOptions) { + this.handleOptions(this.optionsManager.computed); // only for "difficult" options + this.needsFullRerender = true; + this.batchRendering(function () { + if (isTimeZoneDirty) { + _this.dispatch({ + type: 'CHANGE_TIMEZONE', + oldDateEnv: oldDateEnv + }); + } + /* HACK + has the same effect as calling this.requestRerender(true) + but recomputes the state's dateProfile + */ + _this.dispatch({ + type: 'SET_VIEW_TYPE', + viewType: _this.state.viewType + }); + }); + } + if (isSizeDirty) { + this.updateSize(); + } + }; + Calendar.prototype.setOption = function (name, val) { + var _a; + this.processOptions((_a = {}, _a[name] = val, _a), 'dynamic'); + }; + Calendar.prototype.getOption = function (name) { + return this.optionsManager.computed[name]; + }; + Calendar.prototype.opt = function (name) { + return this.optionsManager.computed[name]; + }; + Calendar.prototype.viewOpt = function (name) { + return this.viewOpts()[name]; + }; + Calendar.prototype.viewOpts = function () { + return this.viewSpecs[this.state.viewType].options; + }; + /* + rebuilds things based off of a complete set of refined options + */ + Calendar.prototype.handleOptions = function (options) { + var _this = this; + var pluginHooks = this.pluginSystem.hooks; + this.defaultAllDayEventDuration = createDuration(options.defaultAllDayEventDuration); + this.defaultTimedEventDuration = createDuration(options.defaultTimedEventDuration); + this.delayedRerender = this.buildDelayedRerender(options.rerenderDelay); + this.theme = this.buildTheme(options); + var available = this.parseRawLocales(options.locales); + this.availableRawLocales = available.map; + var locale = this.buildLocale(options.locale || available.defaultCode, available.map); + this.dateEnv = this.buildDateEnv(locale, options.timeZone, pluginHooks.namedTimeZonedImpl, options.firstDay, options.weekNumberCalculation, options.weekLabel, pluginHooks.cmdFormatter); + this.selectionConfig = this.buildSelectionConfig(options); // needs dateEnv. do after :( + // ineffecient to do every time? + this.viewSpecs = buildViewSpecs(pluginHooks.views, this.optionsManager); + // ineffecient to do every time? + this.dateProfileGenerators = mapHash(this.viewSpecs, function (viewSpec) { + return new viewSpec.class.prototype.dateProfileGeneratorClass(viewSpec, _this); + }); + }; + Calendar.prototype.getAvailableLocaleCodes = function () { + return Object.keys(this.availableRawLocales); + }; + Calendar.prototype._buildSelectionConfig = function (rawOpts) { + return processScopedUiProps('select', rawOpts, this); + }; + Calendar.prototype._buildEventUiSingleBase = function (rawOpts) { + if (rawOpts.editable) { // so 'editable' affected events + rawOpts = __assign({}, rawOpts, { eventEditable: true }); + } + return processScopedUiProps('event', rawOpts, this); + }; + // Trigger + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.hasPublicHandlers = function (name) { + return this.hasHandlers(name) || + this.opt(name); // handler specified in options + }; + Calendar.prototype.publiclyTrigger = function (name, args) { + var optHandler = this.opt(name); + this.triggerWith(name, this, args); + if (optHandler) { + return optHandler.apply(this, args); + } + }; + Calendar.prototype.publiclyTriggerAfterSizing = function (name, args) { + var afterSizingTriggers = this.afterSizingTriggers; + (afterSizingTriggers[name] || (afterSizingTriggers[name] = [])).push(args); + }; + Calendar.prototype.releaseAfterSizingTriggers = function () { + var afterSizingTriggers = this.afterSizingTriggers; + for (var name_7 in afterSizingTriggers) { + for (var _i = 0, _a = afterSizingTriggers[name_7]; _i < _a.length; _i++) { + var args = _a[_i]; + this.publiclyTrigger(name_7, args); + } + } + this.afterSizingTriggers = {}; + }; + // View + // ----------------------------------------------------------------------------------------------------------------- + // Returns a boolean about whether the view is okay to instantiate at some point + Calendar.prototype.isValidViewType = function (viewType) { + return Boolean(this.viewSpecs[viewType]); + }; + Calendar.prototype.changeView = function (viewType, dateOrRange) { + var dateMarker = null; + if (dateOrRange) { + if (dateOrRange.start && dateOrRange.end) { // a range + this.optionsManager.addDynamic({ visibleRange: dateOrRange }); // will not rerender + this.handleOptions(this.optionsManager.computed); // ...but yuck + } + else { // a date + dateMarker = this.dateEnv.createMarker(dateOrRange); // just like gotoDate + } + } + this.unselect(); + this.dispatch({ + type: 'SET_VIEW_TYPE', + viewType: viewType, + dateMarker: dateMarker + }); + }; + // Forces navigation to a view for the given date. + // `viewType` can be a specific view name or a generic one like "week" or "day". + // needs to change + Calendar.prototype.zoomTo = function (dateMarker, viewType) { + var spec; + viewType = viewType || 'day'; // day is default zoom + spec = this.viewSpecs[viewType] || + this.getUnitViewSpec(viewType); + this.unselect(); + if (spec) { + this.dispatch({ + type: 'SET_VIEW_TYPE', + viewType: spec.type, + dateMarker: dateMarker + }); + } + else { + this.dispatch({ + type: 'SET_DATE', + dateMarker: dateMarker + }); + } + }; + // Given a duration singular unit, like "week" or "day", finds a matching view spec. + // Preference is given to views that have corresponding buttons. + Calendar.prototype.getUnitViewSpec = function (unit) { + var viewTypes; + var i; + var spec; + // put views that have buttons first. there will be duplicates, but oh well + viewTypes = this.component.header.viewsWithButtons; // TODO: include footer as well? + for (var viewType in this.viewSpecs) { + viewTypes.push(viewType); + } + for (i = 0; i < viewTypes.length; i++) { + spec = this.viewSpecs[viewTypes[i]]; + if (spec) { + if (spec.singleUnit === unit) { + return spec; + } + } + } + }; + // Current Date + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.getInitialDate = function () { + var defaultDateInput = this.opt('defaultDate'); + // compute the initial ambig-timezone date + if (defaultDateInput != null) { + return this.dateEnv.createMarker(defaultDateInput); + } + else { + return this.getNow(); // getNow already returns unzoned + } + }; + Calendar.prototype.prev = function () { + this.unselect(); + this.dispatch({ type: 'PREV' }); + }; + Calendar.prototype.next = function () { + this.unselect(); + this.dispatch({ type: 'NEXT' }); + }; + Calendar.prototype.prevYear = function () { + this.unselect(); + this.dispatch({ + type: 'SET_DATE', + dateMarker: this.dateEnv.addYears(this.state.currentDate, -1) + }); + }; + Calendar.prototype.nextYear = function () { + this.unselect(); + this.dispatch({ + type: 'SET_DATE', + dateMarker: this.dateEnv.addYears(this.state.currentDate, 1) + }); + }; + Calendar.prototype.today = function () { + this.unselect(); + this.dispatch({ + type: 'SET_DATE', + dateMarker: this.getNow() + }); + }; + Calendar.prototype.gotoDate = function (zonedDateInput) { + this.unselect(); + this.dispatch({ + type: 'SET_DATE', + dateMarker: this.dateEnv.createMarker(zonedDateInput) + }); + }; + Calendar.prototype.incrementDate = function (deltaInput) { + var delta = createDuration(deltaInput); + if (delta) { // else, warn about invalid input? + this.unselect(); + this.dispatch({ + type: 'SET_DATE', + dateMarker: this.dateEnv.add(this.state.currentDate, delta) + }); + } + }; + // for external API + Calendar.prototype.getDate = function () { + return this.dateEnv.toDate(this.state.currentDate); + }; + // Date Formatting Utils + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.formatDate = function (d, formatter) { + var dateEnv = this.dateEnv; + return dateEnv.format(dateEnv.createMarker(d), createFormatter(formatter)); + }; + // `settings` is for formatter AND isEndExclusive + Calendar.prototype.formatRange = function (d0, d1, settings) { + var dateEnv = this.dateEnv; + return dateEnv.formatRange(dateEnv.createMarker(d0), dateEnv.createMarker(d1), createFormatter(settings, this.opt('defaultRangeSeparator')), settings); + }; + Calendar.prototype.formatIso = function (d, omitTime) { + var dateEnv = this.dateEnv; + return dateEnv.formatIso(dateEnv.createMarker(d), { omitTime: omitTime }); + }; + // Sizing + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.windowResize = function (ev) { + if (!this.isHandlingWindowResize && + this.component && // why? + ev.target === window // not a jqui resize event + ) { + this.isHandlingWindowResize = true; + this.updateSize(); + this.publiclyTrigger('windowResize', [this.view]); + this.isHandlingWindowResize = false; + } + }; + Calendar.prototype.updateSize = function () { + if (this.component) { // when? + this.component.updateSize(true); + } + }; + // Component Registration + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.registerInteractiveComponent = function (component, settingsInput) { + var settings = parseInteractionSettings(component, settingsInput); + var DEFAULT_INTERACTIONS = [ + EventClicking, + EventHovering + ]; + var interactionClasses = DEFAULT_INTERACTIONS.concat(this.pluginSystem.hooks.componentInteractions); + var interactions = interactionClasses.map(function (interactionClass) { + return new interactionClass(settings); + }); + this.interactionsStore[component.uid] = interactions; + interactionSettingsStore[component.uid] = settings; + }; + Calendar.prototype.unregisterInteractiveComponent = function (component) { + for (var _i = 0, _a = this.interactionsStore[component.uid]; _i < _a.length; _i++) { + var listener = _a[_i]; + listener.destroy(); + } + delete this.interactionsStore[component.uid]; + delete interactionSettingsStore[component.uid]; + }; + // Date Selection / Event Selection / DayClick + // ----------------------------------------------------------------------------------------------------------------- + // this public method receives start/end dates in any format, with any timezone + // NOTE: args were changed from v3 + Calendar.prototype.select = function (dateOrObj, endDate) { + var selectionInput; + if (endDate == null) { + if (dateOrObj.start != null) { + selectionInput = dateOrObj; + } + else { + selectionInput = { + start: dateOrObj, + end: null + }; + } + } + else { + selectionInput = { + start: dateOrObj, + end: endDate + }; + } + var selection = parseDateSpan(selectionInput, this.dateEnv, createDuration({ days: 1 }) // TODO: cache this? + ); + if (selection) { // throw parse error otherwise? + this.dispatch({ type: 'SELECT_DATES', selection: selection }); + this.triggerDateSelect(selection); + } + }; + // public method + Calendar.prototype.unselect = function (pev) { + if (this.state.dateSelection) { + this.dispatch({ type: 'UNSELECT_DATES' }); + this.triggerDateUnselect(pev); + } + }; + Calendar.prototype.triggerDateSelect = function (selection, pev) { + var arg = this.buildDateSpanApi(selection); + arg.jsEvent = pev ? pev.origEvent : null; + arg.view = this.view; + this.publiclyTrigger('select', [arg]); + }; + Calendar.prototype.triggerDateUnselect = function (pev) { + this.publiclyTrigger('unselect', [ + { + jsEvent: pev ? pev.origEvent : null, + view: this.view + } + ]); + }; + // TODO: receive pev? + Calendar.prototype.triggerDateClick = function (dateSpan, dayEl, view, ev) { + var arg = this.buildDatePointApi(dateSpan); + arg.dayEl = dayEl; + arg.jsEvent = ev; + arg.view = view; + this.publiclyTrigger('dateClick', [arg]); + }; + Calendar.prototype.buildDatePointApi = function (dateSpan) { + var props = {}; + for (var _i = 0, _a = this.pluginSystem.hooks.datePointTransforms; _i < _a.length; _i++) { + var transform = _a[_i]; + __assign(props, transform(dateSpan, this)); + } + __assign(props, buildDatePointApi(dateSpan, this.dateEnv)); + return props; + }; + Calendar.prototype.buildDateSpanApi = function (dateSpan) { + var props = {}; + for (var _i = 0, _a = this.pluginSystem.hooks.dateSpanTransforms; _i < _a.length; _i++) { + var transform = _a[_i]; + __assign(props, transform(dateSpan, this)); + } + __assign(props, buildDateSpanApi(dateSpan, this.dateEnv)); + return props; + }; + // Date Utils + // ----------------------------------------------------------------------------------------------------------------- + // Returns a DateMarker for the current date, as defined by the client's computer or from the `now` option + Calendar.prototype.getNow = function () { + var now = this.opt('now'); + if (typeof now === 'function') { + now = now(); + } + if (now == null) { + return this.dateEnv.createNowMarker(); + } + return this.dateEnv.createMarker(now); + }; + // Event-Date Utilities + // ----------------------------------------------------------------------------------------------------------------- + // Given an event's allDay status and start date, return what its fallback end date should be. + // TODO: rename to computeDefaultEventEnd + Calendar.prototype.getDefaultEventEnd = function (allDay, marker) { + var end = marker; + if (allDay) { + end = startOfDay(end); + end = this.dateEnv.add(end, this.defaultAllDayEventDuration); + } + else { + end = this.dateEnv.add(end, this.defaultTimedEventDuration); + } + return end; + }; + // Public Events API + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.addEvent = function (eventInput, sourceInput) { + if (eventInput instanceof EventApi) { + var def = eventInput._def; + var instance = eventInput._instance; + // not already present? don't want to add an old snapshot + if (!this.state.eventStore.defs[def.defId]) { + this.dispatch({ + type: 'ADD_EVENTS', + eventStore: eventTupleToStore({ def: def, instance: instance }) // TODO: better util for two args? + }); + } + return eventInput; + } + var sourceId; + if (sourceInput instanceof EventSourceApi) { + sourceId = sourceInput.internalEventSource.sourceId; + } + else if (sourceInput != null) { + var sourceApi = this.getEventSourceById(sourceInput); // TODO: use an internal function + if (!sourceApi) { + console.warn('Could not find an event source with ID "' + sourceInput + '"'); // TODO: test + return null; + } + else { + sourceId = sourceApi.internalEventSource.sourceId; + } + } + var tuple = parseEvent(eventInput, sourceId, this); + if (tuple) { + this.dispatch({ + type: 'ADD_EVENTS', + eventStore: eventTupleToStore(tuple) + }); + return new EventApi(this, tuple.def, tuple.def.recurringDef ? null : tuple.instance); + } + return null; + }; + // TODO: optimize + Calendar.prototype.getEventById = function (id) { + var _a = this.state.eventStore, defs = _a.defs, instances = _a.instances; + id = String(id); + for (var defId in defs) { + var def = defs[defId]; + if (def.publicId === id) { + if (def.recurringDef) { + return new EventApi(this, def, null); + } + else { + for (var instanceId in instances) { + var instance = instances[instanceId]; + if (instance.defId === def.defId) { + return new EventApi(this, def, instance); + } + } + } + } + } + return null; + }; + Calendar.prototype.getEvents = function () { + var _a = this.state.eventStore, defs = _a.defs, instances = _a.instances; + var eventApis = []; + for (var id in instances) { + var instance = instances[id]; + var def = defs[instance.defId]; + eventApis.push(new EventApi(this, def, instance)); + } + return eventApis; + }; + Calendar.prototype.removeAllEvents = function () { + this.dispatch({ type: 'REMOVE_ALL_EVENTS' }); + }; + Calendar.prototype.rerenderEvents = function () { + this.dispatch({ type: 'RESET_EVENTS' }); + }; + // Public Event Sources API + // ----------------------------------------------------------------------------------------------------------------- + Calendar.prototype.getEventSources = function () { + var sourceHash = this.state.eventSources; + var sourceApis = []; + for (var internalId in sourceHash) { + sourceApis.push(new EventSourceApi(this, sourceHash[internalId])); + } + return sourceApis; + }; + Calendar.prototype.getEventSourceById = function (id) { + var sourceHash = this.state.eventSources; + id = String(id); + for (var sourceId in sourceHash) { + if (sourceHash[sourceId].publicId === id) { + return new EventSourceApi(this, sourceHash[sourceId]); + } + } + return null; + }; + Calendar.prototype.addEventSource = function (sourceInput) { + if (sourceInput instanceof EventSourceApi) { + // not already present? don't want to add an old snapshot + if (!this.state.eventSources[sourceInput.internalEventSource.sourceId]) { + this.dispatch({ + type: 'ADD_EVENT_SOURCES', + sources: [sourceInput.internalEventSource] + }); + } + return sourceInput; + } + var eventSource = parseEventSource(sourceInput, this); + if (eventSource) { // TODO: error otherwise? + this.dispatch({ type: 'ADD_EVENT_SOURCES', sources: [eventSource] }); + return new EventSourceApi(this, eventSource); + } + return null; + }; + Calendar.prototype.removeAllEventSources = function () { + this.dispatch({ type: 'REMOVE_ALL_EVENT_SOURCES' }); + }; + Calendar.prototype.refetchEvents = function () { + this.dispatch({ type: 'FETCH_EVENT_SOURCES' }); + }; + return Calendar; + }()); + EmitterMixin.mixInto(Calendar); + // for memoizers + // ----------------------------------------------------------------------------------------------------------------- + function buildDateEnv(locale, timeZone, namedTimeZoneImpl, firstDay, weekNumberCalculation, weekLabel, cmdFormatter) { + return new DateEnv({ + calendarSystem: 'gregory', + timeZone: timeZone, + namedTimeZoneImpl: namedTimeZoneImpl, + locale: locale, + weekNumberCalculation: weekNumberCalculation, + firstDay: firstDay, + weekLabel: weekLabel, + cmdFormatter: cmdFormatter + }); + } + function buildTheme(calendarOptions) { + var themeClass = this.pluginSystem.hooks.themeClasses[calendarOptions.themeSystem] || StandardTheme; + return new themeClass(calendarOptions); + } + function buildDelayedRerender(wait) { + var func = this.tryRerender.bind(this); + if (wait != null) { + func = debounce(func, wait); + } + return func; + } + function buildEventUiBySource(eventSources) { + return mapHash(eventSources, function (eventSource) { + return eventSource.ui; + }); + } + function buildEventUiBases(eventDefs, eventUiSingleBase, eventUiBySource) { + var eventUiBases = { '': eventUiSingleBase }; + for (var defId in eventDefs) { + var def = eventDefs[defId]; + if (def.sourceId && eventUiBySource[def.sourceId]) { + eventUiBases[defId] = eventUiBySource[def.sourceId]; + } + } + return eventUiBases; + } + + var View = /** @class */ (function (_super) { + __extends(View, _super); + function View(context, viewSpec, dateProfileGenerator, parentEl) { + var _this = _super.call(this, context, createElement('div', { className: 'fc-view fc-' + viewSpec.type + '-view' }), true // isView (HACK) + ) || this; + _this.renderDatesMem = memoizeRendering(_this.renderDatesWrap, _this.unrenderDatesWrap); + _this.renderBusinessHoursMem = memoizeRendering(_this.renderBusinessHours, _this.unrenderBusinessHours, [_this.renderDatesMem]); + _this.renderDateSelectionMem = memoizeRendering(_this.renderDateSelectionWrap, _this.unrenderDateSelectionWrap, [_this.renderDatesMem]); + _this.renderEventsMem = memoizeRendering(_this.renderEvents, _this.unrenderEvents, [_this.renderDatesMem]); + _this.renderEventSelectionMem = memoizeRendering(_this.renderEventSelectionWrap, _this.unrenderEventSelectionWrap, [_this.renderEventsMem]); + _this.renderEventDragMem = memoizeRendering(_this.renderEventDragWrap, _this.unrenderEventDragWrap, [_this.renderDatesMem]); + _this.renderEventResizeMem = memoizeRendering(_this.renderEventResizeWrap, _this.unrenderEventResizeWrap, [_this.renderDatesMem]); + _this.viewSpec = viewSpec; + _this.dateProfileGenerator = dateProfileGenerator; + _this.type = viewSpec.type; + _this.eventOrderSpecs = parseFieldSpecs(_this.opt('eventOrder')); + _this.nextDayThreshold = createDuration(_this.opt('nextDayThreshold')); + parentEl.appendChild(_this.el); + _this.initialize(); + return _this; + } + View.prototype.initialize = function () { + }; + Object.defineProperty(View.prototype, "activeStart", { + // Date Setting/Unsetting + // ----------------------------------------------------------------------------------------------------------------- + get: function () { + return this.dateEnv.toDate(this.props.dateProfile.activeRange.start); + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(View.prototype, "activeEnd", { + get: function () { + return this.dateEnv.toDate(this.props.dateProfile.activeRange.end); + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(View.prototype, "currentStart", { + get: function () { + return this.dateEnv.toDate(this.props.dateProfile.currentRange.start); + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(View.prototype, "currentEnd", { + get: function () { + return this.dateEnv.toDate(this.props.dateProfile.currentRange.end); + }, + enumerable: true, + configurable: true + }); + // General Rendering + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.render = function (props) { + this.renderDatesMem(props.dateProfile); + this.renderBusinessHoursMem(props.businessHours); + this.renderDateSelectionMem(props.dateSelection); + this.renderEventsMem(props.eventStore); + this.renderEventSelectionMem(props.eventSelection); + this.renderEventDragMem(props.eventDrag); + this.renderEventResizeMem(props.eventResize); + }; + View.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.renderDatesMem.unrender(); // should unrender everything else + }; + // Sizing + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.updateSize = function (isResize, viewHeight, isAuto) { + var calendar = this.calendar; + if (isResize || calendar.isViewUpdated || calendar.isDatesUpdated || calendar.isEventsUpdated) { + // sort of the catch-all sizing + // anything that might cause dimension changes + this.updateBaseSize(isResize, viewHeight, isAuto); + } + }; + View.prototype.updateBaseSize = function (isResize, viewHeight, isAuto) { + }; + // Date Rendering + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.renderDatesWrap = function (dateProfile) { + this.renderDates(dateProfile); + this.addScroll({ isDateInit: true }); + this.startNowIndicator(dateProfile); // shouldn't render yet because updateSize will be called soon + }; + View.prototype.unrenderDatesWrap = function () { + this.stopNowIndicator(); + this.unrenderDates(); + }; + View.prototype.renderDates = function (dateProfile) { }; + View.prototype.unrenderDates = function () { }; + // Business Hours + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.renderBusinessHours = function (businessHours) { }; + View.prototype.unrenderBusinessHours = function () { }; + // Date Selection + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.renderDateSelectionWrap = function (selection) { + if (selection) { + this.renderDateSelection(selection); + } + }; + View.prototype.unrenderDateSelectionWrap = function (selection) { + if (selection) { + this.unrenderDateSelection(selection); + } + }; + View.prototype.renderDateSelection = function (selection) { }; + View.prototype.unrenderDateSelection = function (selection) { }; + // Event Rendering + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.renderEvents = function (eventStore) { }; + View.prototype.unrenderEvents = function () { }; + // util for subclasses + View.prototype.sliceEvents = function (eventStore, allDay) { + var props = this.props; + return sliceEventStore(eventStore, props.eventUiBases, props.dateProfile.activeRange, allDay ? this.nextDayThreshold : null).fg; + }; + // Event Selection + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.renderEventSelectionWrap = function (instanceId) { + if (instanceId) { + this.renderEventSelection(instanceId); + } + }; + View.prototype.unrenderEventSelectionWrap = function (instanceId) { + if (instanceId) { + this.unrenderEventSelection(instanceId); + } + }; + View.prototype.renderEventSelection = function (instanceId) { }; + View.prototype.unrenderEventSelection = function (instanceId) { }; + // Event Drag + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.renderEventDragWrap = function (state) { + if (state) { + this.renderEventDrag(state); + } + }; + View.prototype.unrenderEventDragWrap = function (state) { + if (state) { + this.unrenderEventDrag(state); + } + }; + View.prototype.renderEventDrag = function (state) { }; + View.prototype.unrenderEventDrag = function (state) { }; + // Event Resize + // ----------------------------------------------------------------------------------------------------------------- + View.prototype.renderEventResizeWrap = function (state) { + if (state) { + this.renderEventResize(state); + } + }; + View.prototype.unrenderEventResizeWrap = function (state) { + if (state) { + this.unrenderEventResize(state); + } + }; + View.prototype.renderEventResize = function (state) { }; + View.prototype.unrenderEventResize = function (state) { }; + /* Now Indicator + ------------------------------------------------------------------------------------------------------------------*/ + // Immediately render the current time indicator and begins re-rendering it at an interval, + // which is defined by this.getNowIndicatorUnit(). + // TODO: somehow do this for the current whole day's background too + View.prototype.startNowIndicator = function (dateProfile) { + var _this = this; + var dateEnv = this.dateEnv; + var unit; + var update; + var delay; // ms wait value + if (this.opt('nowIndicator')) { + unit = this.getNowIndicatorUnit(dateProfile); + if (unit) { + update = this.updateNowIndicator.bind(this); + this.initialNowDate = this.calendar.getNow(); + this.initialNowQueriedMs = new Date().valueOf(); + // wait until the beginning of the next interval + delay = dateEnv.add(dateEnv.startOf(this.initialNowDate, unit), createDuration(1, unit)).valueOf() - this.initialNowDate.valueOf(); + // TODO: maybe always use setTimeout, waiting until start of next unit + this.nowIndicatorTimeoutID = setTimeout(function () { + _this.nowIndicatorTimeoutID = null; + update(); + if (unit === 'second') { + delay = 1000; // every second + } + else { + delay = 1000 * 60; // otherwise, every minute + } + _this.nowIndicatorIntervalID = setInterval(update, delay); // update every interval + }, delay); + } + // rendering will be initiated in updateSize + } + }; + // rerenders the now indicator, computing the new current time from the amount of time that has passed + // since the initial getNow call. + View.prototype.updateNowIndicator = function () { + if (this.props.dateProfile && // a way to determine if dates were rendered yet + this.initialNowDate // activated before? + ) { + this.unrenderNowIndicator(); // won't unrender if unnecessary + this.renderNowIndicator(addMs(this.initialNowDate, new Date().valueOf() - this.initialNowQueriedMs)); + this.isNowIndicatorRendered = true; + } + }; + // Immediately unrenders the view's current time indicator and stops any re-rendering timers. + // Won't cause side effects if indicator isn't rendered. + View.prototype.stopNowIndicator = function () { + if (this.isNowIndicatorRendered) { + if (this.nowIndicatorTimeoutID) { + clearTimeout(this.nowIndicatorTimeoutID); + this.nowIndicatorTimeoutID = null; + } + if (this.nowIndicatorIntervalID) { + clearInterval(this.nowIndicatorIntervalID); + this.nowIndicatorIntervalID = null; + } + this.unrenderNowIndicator(); + this.isNowIndicatorRendered = false; + } + }; + View.prototype.getNowIndicatorUnit = function (dateProfile) { + // subclasses should implement + }; + // Renders a current time indicator at the given datetime + View.prototype.renderNowIndicator = function (date) { + // SUBCLASSES MUST PASS TO CHILDREN! + }; + // Undoes the rendering actions from renderNowIndicator + View.prototype.unrenderNowIndicator = function () { + // SUBCLASSES MUST PASS TO CHILDREN! + }; + /* Scroller + ------------------------------------------------------------------------------------------------------------------*/ + View.prototype.addScroll = function (scroll) { + var queuedScroll = this.queuedScroll || (this.queuedScroll = {}); + __assign(queuedScroll, scroll); + }; + View.prototype.popScroll = function (isResize) { + this.applyQueuedScroll(isResize); + this.queuedScroll = null; + }; + View.prototype.applyQueuedScroll = function (isResize) { + this.applyScroll(this.queuedScroll || {}, isResize); + }; + View.prototype.queryScroll = function () { + var scroll = {}; + if (this.props.dateProfile) { // dates rendered yet? + __assign(scroll, this.queryDateScroll()); + } + return scroll; + }; + View.prototype.applyScroll = function (scroll, isResize) { + if (scroll.isDateInit) { + delete scroll.isDateInit; + if (this.props.dateProfile) { // dates rendered yet? + __assign(scroll, this.computeInitialDateScroll()); + } + } + if (this.props.dateProfile) { // dates rendered yet? + this.applyDateScroll(scroll); + } + }; + View.prototype.computeInitialDateScroll = function () { + return {}; // subclasses must implement + }; + View.prototype.queryDateScroll = function () { + return {}; // subclasses must implement + }; + View.prototype.applyDateScroll = function (scroll) { + // subclasses must implement + }; + return View; + }(DateComponent)); + EmitterMixin.mixInto(View); + View.prototype.usesMinMaxTime = false; + View.prototype.dateProfileGeneratorClass = DateProfileGenerator; + + var FgEventRenderer = /** @class */ (function () { + function FgEventRenderer(context) { + this.segs = []; + this.isSizeDirty = false; + this.context = context; + } + FgEventRenderer.prototype.renderSegs = function (segs, mirrorInfo) { + this.rangeUpdated(); // called too frequently :( + // render an `.el` on each seg + // returns a subset of the segs. segs that were actually rendered + segs = this.renderSegEls(segs, mirrorInfo); + this.segs = segs; + this.attachSegs(segs, mirrorInfo); + this.isSizeDirty = true; + this.context.view.triggerRenderedSegs(this.segs, Boolean(mirrorInfo)); + }; + FgEventRenderer.prototype.unrender = function (_segs, mirrorInfo) { + this.context.view.triggerWillRemoveSegs(this.segs, Boolean(mirrorInfo)); + this.detachSegs(this.segs); + this.segs = []; + }; + // Updates values that rely on options and also relate to range + FgEventRenderer.prototype.rangeUpdated = function () { + var options = this.context.options; + var displayEventTime; + var displayEventEnd; + this.eventTimeFormat = createFormatter(options.eventTimeFormat || this.computeEventTimeFormat(), options.defaultRangeSeparator); + displayEventTime = options.displayEventTime; + if (displayEventTime == null) { + displayEventTime = this.computeDisplayEventTime(); // might be based off of range + } + displayEventEnd = options.displayEventEnd; + if (displayEventEnd == null) { + displayEventEnd = this.computeDisplayEventEnd(); // might be based off of range + } + this.displayEventTime = displayEventTime; + this.displayEventEnd = displayEventEnd; + }; + // Renders and assigns an `el` property for each foreground event segment. + // Only returns segments that successfully rendered. + FgEventRenderer.prototype.renderSegEls = function (segs, mirrorInfo) { + var html = ''; + var i; + if (segs.length) { // don't build an empty html string + // build a large concatenation of event segment HTML + for (i = 0; i < segs.length; i++) { + html += this.renderSegHtml(segs[i], mirrorInfo); + } + // Grab individual elements from the combined HTML string. Use each as the default rendering. + // Then, compute the 'el' for each segment. An el might be null if the eventRender callback returned false. + htmlToElements(html).forEach(function (el, i) { + var seg = segs[i]; + if (el) { + seg.el = el; + } + }); + segs = filterSegsViaEls(this.context.view, segs, Boolean(mirrorInfo)); + } + return segs; + }; + // Generic utility for generating the HTML classNames for an event segment's element + FgEventRenderer.prototype.getSegClasses = function (seg, isDraggable, isResizable, mirrorInfo) { + var classes = [ + 'fc-event', + seg.isStart ? 'fc-start' : 'fc-not-start', + seg.isEnd ? 'fc-end' : 'fc-not-end' + ].concat(seg.eventRange.ui.classNames); + if (isDraggable) { + classes.push('fc-draggable'); + } + if (isResizable) { + classes.push('fc-resizable'); + } + if (mirrorInfo) { + classes.push('fc-mirror'); + if (mirrorInfo.isDragging) { + classes.push('fc-dragging'); + } + if (mirrorInfo.isResizing) { + classes.push('fc-resizing'); + } + } + return classes; + }; + // Compute the text that should be displayed on an event's element. + // `range` can be the Event object itself, or something range-like, with at least a `start`. + // If event times are disabled, or the event has no time, will return a blank string. + // If not specified, formatter will default to the eventTimeFormat setting, + // and displayEnd will default to the displayEventEnd setting. + FgEventRenderer.prototype.getTimeText = function (eventRange, formatter, displayEnd) { + var def = eventRange.def, instance = eventRange.instance; + return this._getTimeText(instance.range.start, def.hasEnd ? instance.range.end : null, def.allDay, formatter, displayEnd, instance.forcedStartTzo, instance.forcedEndTzo); + }; + FgEventRenderer.prototype._getTimeText = function (start, end, allDay, formatter, displayEnd, forcedStartTzo, forcedEndTzo) { + var dateEnv = this.context.dateEnv; + if (formatter == null) { + formatter = this.eventTimeFormat; + } + if (displayEnd == null) { + displayEnd = this.displayEventEnd; + } + if (this.displayEventTime && !allDay) { + if (displayEnd && end) { + return dateEnv.formatRange(start, end, formatter, { + forcedStartTzo: forcedStartTzo, + forcedEndTzo: forcedEndTzo + }); + } + else { + return dateEnv.format(start, formatter, { + forcedTzo: forcedStartTzo + }); + } + } + return ''; + }; + FgEventRenderer.prototype.computeEventTimeFormat = function () { + return { + hour: 'numeric', + minute: '2-digit', + omitZeroMinute: true + }; + }; + FgEventRenderer.prototype.computeDisplayEventTime = function () { + return true; + }; + FgEventRenderer.prototype.computeDisplayEventEnd = function () { + return true; + }; + // Utility for generating event skin-related CSS properties + FgEventRenderer.prototype.getSkinCss = function (ui) { + return { + 'background-color': ui.backgroundColor, + 'border-color': ui.borderColor, + color: ui.textColor + }; + }; + FgEventRenderer.prototype.sortEventSegs = function (segs) { + var specs = this.context.view.eventOrderSpecs; + var objs = segs.map(buildSegCompareObj); + objs.sort(function (obj0, obj1) { + return compareByFieldSpecs(obj0, obj1, specs); + }); + return objs.map(function (c) { + return c._seg; + }); + }; + FgEventRenderer.prototype.computeSizes = function (force) { + if (force || this.isSizeDirty) { + this.computeSegSizes(this.segs); + } + }; + FgEventRenderer.prototype.assignSizes = function (force) { + if (force || this.isSizeDirty) { + this.assignSegSizes(this.segs); + this.isSizeDirty = false; + } + }; + FgEventRenderer.prototype.computeSegSizes = function (segs) { + }; + FgEventRenderer.prototype.assignSegSizes = function (segs) { + }; + // Manipulation on rendered segs + FgEventRenderer.prototype.hideByHash = function (hash) { + if (hash) { + for (var _i = 0, _a = this.segs; _i < _a.length; _i++) { + var seg = _a[_i]; + if (hash[seg.eventRange.instance.instanceId]) { + seg.el.style.visibility = 'hidden'; + } + } + } + }; + FgEventRenderer.prototype.showByHash = function (hash) { + if (hash) { + for (var _i = 0, _a = this.segs; _i < _a.length; _i++) { + var seg = _a[_i]; + if (hash[seg.eventRange.instance.instanceId]) { + seg.el.style.visibility = ''; + } + } + } + }; + FgEventRenderer.prototype.selectByInstanceId = function (instanceId) { + if (instanceId) { + for (var _i = 0, _a = this.segs; _i < _a.length; _i++) { + var seg = _a[_i]; + var eventInstance = seg.eventRange.instance; + if (eventInstance && eventInstance.instanceId === instanceId && + seg.el // necessary? + ) { + seg.el.classList.add('fc-selected'); + } + } + } + }; + FgEventRenderer.prototype.unselectByInstanceId = function (instanceId) { + if (instanceId) { + for (var _i = 0, _a = this.segs; _i < _a.length; _i++) { + var seg = _a[_i]; + if (seg.el) { // necessary? + seg.el.classList.remove('fc-selected'); + } + } + } + }; + return FgEventRenderer; + }()); + // returns a object with all primitive props that can be compared + function buildSegCompareObj(seg) { + var eventDef = seg.eventRange.def; + var range = seg.eventRange.instance.range; + var start = range.start ? range.start.valueOf() : 0; // TODO: better support for open-range events + var end = range.end ? range.end.valueOf() : 0; // " + return __assign({}, eventDef.extendedProps, eventDef, { id: eventDef.publicId, start: start, + end: end, duration: end - start, allDay: Number(eventDef.allDay), _seg: seg // for later retrieval + }); + } + + var FillRenderer = /** @class */ (function () { + function FillRenderer(context) { + this.fillSegTag = 'div'; + this.dirtySizeFlags = {}; + this.context = context; + this.containerElsByType = {}; + this.segsByType = {}; + } + FillRenderer.prototype.getSegsByType = function (type) { + return this.segsByType[type] || []; + }; + FillRenderer.prototype.renderSegs = function (type, segs) { + var _a; + var renderedSegs = this.renderSegEls(type, segs); // assignes `.el` to each seg. returns successfully rendered segs + var containerEls = this.attachSegs(type, renderedSegs); + if (containerEls) { + (_a = (this.containerElsByType[type] || (this.containerElsByType[type] = []))).push.apply(_a, containerEls); + } + this.segsByType[type] = renderedSegs; + if (type === 'bgEvent') { + this.context.view.triggerRenderedSegs(renderedSegs, false); // isMirror=false + } + this.dirtySizeFlags[type] = true; + }; + // Unrenders a specific type of fill that is currently rendered on the grid + FillRenderer.prototype.unrender = function (type) { + var segs = this.segsByType[type]; + if (segs) { + if (type === 'bgEvent') { + this.context.view.triggerWillRemoveSegs(segs, false); // isMirror=false + } + this.detachSegs(type, segs); + } + }; + // Renders and assigns an `el` property for each fill segment. Generic enough to work with different types. + // Only returns segments that successfully rendered. + FillRenderer.prototype.renderSegEls = function (type, segs) { + var _this = this; + var html = ''; + var i; + if (segs.length) { + // build a large concatenation of segment HTML + for (i = 0; i < segs.length; i++) { + html += this.renderSegHtml(type, segs[i]); + } + // Grab individual elements from the combined HTML string. Use each as the default rendering. + // Then, compute the 'el' for each segment. + htmlToElements(html).forEach(function (el, i) { + var seg = segs[i]; + if (el) { + seg.el = el; + } + }); + if (type === 'bgEvent') { + segs = filterSegsViaEls(this.context.view, segs, false // isMirror. background events can never be mirror elements + ); + } + // correct element type? (would be bad if a non-TD were inserted into a table for example) + segs = segs.filter(function (seg) { + return elementMatches(seg.el, _this.fillSegTag); + }); + } + return segs; + }; + // Builds the HTML needed for one fill segment. Generic enough to work with different types. + FillRenderer.prototype.renderSegHtml = function (type, seg) { + var css = null; + var classNames = []; + if (type !== 'highlight' && type !== 'businessHours') { + css = { + 'background-color': seg.eventRange.ui.backgroundColor + }; + } + if (type !== 'highlight') { + classNames = classNames.concat(seg.eventRange.ui.classNames); + } + if (type === 'businessHours') { + classNames.push('fc-bgevent'); + } + else { + classNames.push('fc-' + type.toLowerCase()); + } + return '<' + this.fillSegTag + + (classNames.length ? ' class="' + classNames.join(' ') + '"' : '') + + (css ? ' style="' + cssToStr(css) + '"' : '') + + '>'; + }; + FillRenderer.prototype.detachSegs = function (type, segs) { + var containerEls = this.containerElsByType[type]; + if (containerEls) { + containerEls.forEach(removeElement); + delete this.containerElsByType[type]; + } + }; + FillRenderer.prototype.computeSizes = function (force) { + for (var type in this.segsByType) { + if (force || this.dirtySizeFlags[type]) { + this.computeSegSizes(this.segsByType[type]); + } + } + }; + FillRenderer.prototype.assignSizes = function (force) { + for (var type in this.segsByType) { + if (force || this.dirtySizeFlags[type]) { + this.assignSegSizes(this.segsByType[type]); + } + } + this.dirtySizeFlags = {}; + }; + FillRenderer.prototype.computeSegSizes = function (segs) { + }; + FillRenderer.prototype.assignSegSizes = function (segs) { + }; + return FillRenderer; + }()); + + var NamedTimeZoneImpl = /** @class */ (function () { + function NamedTimeZoneImpl(timeZoneName) { + this.timeZoneName = timeZoneName; + } + return NamedTimeZoneImpl; + }()); + + /* + An abstraction for a dragging interaction originating on an event. + Does higher-level things than PointerDragger, such as possibly: + - a "mirror" that moves with the pointer + - a minimum number of pixels or other criteria for a true drag to begin + + subclasses must emit: + - pointerdown + - dragstart + - dragmove + - pointerup + - dragend + */ + var ElementDragging = /** @class */ (function () { + function ElementDragging(el) { + this.emitter = new EmitterMixin(); + } + ElementDragging.prototype.destroy = function () { + }; + ElementDragging.prototype.setMirrorIsVisible = function (bool) { + // optional if subclass doesn't want to support a mirror + }; + ElementDragging.prototype.setMirrorNeedsRevert = function (bool) { + // optional if subclass doesn't want to support a mirror + }; + ElementDragging.prototype.setAutoScrollEnabled = function (bool) { + // optional + }; + return ElementDragging; + }()); + + function formatDate(dateInput, settings) { + if (settings === void 0) { settings = {}; } + var dateEnv = buildDateEnv$1(settings); + var formatter = createFormatter(settings); + var dateMeta = dateEnv.createMarkerMeta(dateInput); + if (!dateMeta) { // TODO: warning? + return ''; + } + return dateEnv.format(dateMeta.marker, formatter, { + forcedTzo: dateMeta.forcedTzo + }); + } + function formatRange(startInput, endInput, settings // mixture of env and formatter settings + ) { + var dateEnv = buildDateEnv$1(typeof settings === 'object' && settings ? settings : {}); // pass in if non-null object + var formatter = createFormatter(settings, globalDefaults.defaultRangeSeparator); + var startMeta = dateEnv.createMarkerMeta(startInput); + var endMeta = dateEnv.createMarkerMeta(endInput); + if (!startMeta || !endMeta) { // TODO: warning? + return ''; + } + return dateEnv.formatRange(startMeta.marker, endMeta.marker, formatter, { + forcedStartTzo: startMeta.forcedTzo, + forcedEndTzo: endMeta.forcedTzo, + isEndExclusive: settings.isEndExclusive + }); + } + // TODO: more DRY and optimized + function buildDateEnv$1(settings) { + var locale = buildLocale(settings.locale || 'en', parseRawLocales([]).map); // TODO: don't hardcode 'en' everywhere + // ensure required settings + settings = __assign({ timeZone: globalDefaults.timeZone, calendarSystem: 'gregory' }, settings, { locale: locale }); + return new DateEnv(settings); + } + + var DRAG_META_PROPS = { + startTime: createDuration, + duration: createDuration, + create: Boolean, + sourceId: String + }; + var DRAG_META_DEFAULTS = { + create: true + }; + function parseDragMeta(raw) { + var leftoverProps = {}; + var refined = refineProps(raw, DRAG_META_PROPS, DRAG_META_DEFAULTS, leftoverProps); + refined.leftoverProps = leftoverProps; + return refined; + } + + // Computes a default column header formatting string if `colFormat` is not explicitly defined + function computeFallbackHeaderFormat(datesRepDistinctDays, dayCnt) { + // if more than one week row, or if there are a lot of columns with not much space, + // put just the day numbers will be in each cell + if (!datesRepDistinctDays || dayCnt > 10) { + return { weekday: 'short' }; // "Sat" + } + else if (dayCnt > 1) { + return { weekday: 'short', month: 'numeric', day: 'numeric', omitCommas: true }; // "Sat 11/12" + } + else { + return { weekday: 'long' }; // "Saturday" + } + } + function renderDateCell(dateMarker, dateProfile, datesRepDistinctDays, colCnt, colHeadFormat, context, colspan, otherAttrs) { + var view = context.view, dateEnv = context.dateEnv, theme = context.theme, options = context.options; + var isDateValid = rangeContainsMarker(dateProfile.activeRange, dateMarker); // TODO: called too frequently. cache somehow. + var classNames = [ + 'fc-day-header', + theme.getClass('widgetHeader') + ]; + var innerHtml; + if (typeof options.columnHeaderHtml === 'function') { + innerHtml = options.columnHeaderHtml(dateEnv.toDate(dateMarker)); + } + else if (typeof options.columnHeaderText === 'function') { + innerHtml = htmlEscape(options.columnHeaderText(dateEnv.toDate(dateMarker))); + } + else { + innerHtml = htmlEscape(dateEnv.format(dateMarker, colHeadFormat)); + } + // if only one row of days, the classNames on the header can represent the specific days beneath + if (datesRepDistinctDays) { + classNames = classNames.concat( + // includes the day-of-week class + // noThemeHighlight=true (don't highlight the header) + getDayClasses(dateMarker, dateProfile, context, true)); + } + else { + classNames.push('fc-' + DAY_IDS[dateMarker.getUTCDay()]); // only add the day-of-week class + } + return '' + + ' 1 ? + ' colspan="' + colspan + '"' : + '') + + (otherAttrs ? + ' ' + otherAttrs : + '') + + '>' + + (isDateValid ? + // don't make a link if the heading could represent multiple days, or if there's only one day (forceOff) + buildGotoAnchorHtml(view, { date: dateMarker, forceOff: !datesRepDistinctDays || colCnt === 1 }, innerHtml) : + // if not valid, display text, but no link + innerHtml) + + ''; + } + + var DayHeader = /** @class */ (function (_super) { + __extends(DayHeader, _super); + function DayHeader(context, parentEl) { + var _this = _super.call(this, context) || this; + parentEl.innerHTML = ''; // because might be nbsp + parentEl.appendChild(_this.el = htmlToElement('
' + + '' + + '' + + '
' + + '
')); + _this.thead = _this.el.querySelector('thead'); + return _this; + } + DayHeader.prototype.destroy = function () { + removeElement(this.el); + }; + DayHeader.prototype.render = function (props) { + var dates = props.dates, datesRepDistinctDays = props.datesRepDistinctDays; + var parts = []; + if (props.renderIntroHtml) { + parts.push(props.renderIntroHtml()); + } + var colHeadFormat = createFormatter(this.opt('columnHeaderFormat') || + computeFallbackHeaderFormat(datesRepDistinctDays, dates.length)); + for (var _i = 0, dates_1 = dates; _i < dates_1.length; _i++) { + var date = dates_1[_i]; + parts.push(renderDateCell(date, props.dateProfile, datesRepDistinctDays, dates.length, colHeadFormat, this.context)); + } + if (this.isRtl) { + parts.reverse(); + } + this.thead.innerHTML = '' + parts.join('') + ''; + }; + return DayHeader; + }(Component)); + + var DaySeries = /** @class */ (function () { + function DaySeries(range, dateProfileGenerator) { + var date = range.start; + var end = range.end; + var indices = []; + var dates = []; + var dayIndex = -1; + while (date < end) { // loop each day from start to end + if (dateProfileGenerator.isHiddenDay(date)) { + indices.push(dayIndex + 0.5); // mark that it's between indices + } + else { + dayIndex++; + indices.push(dayIndex); + dates.push(date); + } + date = addDays(date, 1); + } + this.dates = dates; + this.indices = indices; + this.cnt = dates.length; + } + DaySeries.prototype.sliceRange = function (range) { + var firstIndex = this.getDateDayIndex(range.start); // inclusive first index + var lastIndex = this.getDateDayIndex(addDays(range.end, -1)); // inclusive last index + var clippedFirstIndex = Math.max(0, firstIndex); + var clippedLastIndex = Math.min(this.cnt - 1, lastIndex); + // deal with in-between indices + clippedFirstIndex = Math.ceil(clippedFirstIndex); // in-between starts round to next cell + clippedLastIndex = Math.floor(clippedLastIndex); // in-between ends round to prev cell + if (clippedFirstIndex <= clippedLastIndex) { + return { + firstIndex: clippedFirstIndex, + lastIndex: clippedLastIndex, + isStart: firstIndex === clippedFirstIndex, + isEnd: lastIndex === clippedLastIndex + }; + } + else { + return null; + } + }; + // Given a date, returns its chronolocial cell-index from the first cell of the grid. + // If the date lies between cells (because of hiddenDays), returns a floating-point value between offsets. + // If before the first offset, returns a negative number. + // If after the last offset, returns an offset past the last cell offset. + // Only works for *start* dates of cells. Will not work for exclusive end dates for cells. + DaySeries.prototype.getDateDayIndex = function (date) { + var indices = this.indices; + var dayOffset = Math.floor(diffDays(this.dates[0], date)); + if (dayOffset < 0) { + return indices[0] - 1; + } + else if (dayOffset >= indices.length) { + return indices[indices.length - 1] + 1; + } + else { + return indices[dayOffset]; + } + }; + return DaySeries; + }()); + + var DayTable = /** @class */ (function () { + function DayTable(daySeries, breakOnWeeks) { + var dates = daySeries.dates; + var daysPerRow; + var firstDay; + var rowCnt; + if (breakOnWeeks) { + // count columns until the day-of-week repeats + firstDay = dates[0].getUTCDay(); + for (daysPerRow = 1; daysPerRow < dates.length; daysPerRow++) { + if (dates[daysPerRow].getUTCDay() === firstDay) { + break; + } + } + rowCnt = Math.ceil(dates.length / daysPerRow); + } + else { + rowCnt = 1; + daysPerRow = dates.length; + } + this.rowCnt = rowCnt; + this.colCnt = daysPerRow; + this.daySeries = daySeries; + this.cells = this.buildCells(); + this.headerDates = this.buildHeaderDates(); + } + DayTable.prototype.buildCells = function () { + var rows = []; + for (var row = 0; row < this.rowCnt; row++) { + var cells = []; + for (var col = 0; col < this.colCnt; col++) { + cells.push(this.buildCell(row, col)); + } + rows.push(cells); + } + return rows; + }; + DayTable.prototype.buildCell = function (row, col) { + return { + date: this.daySeries.dates[row * this.colCnt + col] + }; + }; + DayTable.prototype.buildHeaderDates = function () { + var dates = []; + for (var col = 0; col < this.colCnt; col++) { + dates.push(this.cells[0][col].date); + } + return dates; + }; + DayTable.prototype.sliceRange = function (range) { + var colCnt = this.colCnt; + var seriesSeg = this.daySeries.sliceRange(range); + var segs = []; + if (seriesSeg) { + var firstIndex = seriesSeg.firstIndex, lastIndex = seriesSeg.lastIndex; + var index = firstIndex; + while (index <= lastIndex) { + var row = Math.floor(index / colCnt); + var nextIndex = Math.min((row + 1) * colCnt, lastIndex + 1); + segs.push({ + row: row, + firstCol: index % colCnt, + lastCol: (nextIndex - 1) % colCnt, + isStart: seriesSeg.isStart && index === firstIndex, + isEnd: seriesSeg.isEnd && (nextIndex - 1) === lastIndex + }); + index = nextIndex; + } + } + return segs; + }; + return DayTable; + }()); + + var Slicer = /** @class */ (function () { + function Slicer() { + this.sliceBusinessHours = memoize(this._sliceBusinessHours); + this.sliceDateSelection = memoize(this._sliceDateSpan); + this.sliceEventStore = memoize(this._sliceEventStore); + this.sliceEventDrag = memoize(this._sliceInteraction); + this.sliceEventResize = memoize(this._sliceInteraction); + } + Slicer.prototype.sliceProps = function (props, dateProfile, nextDayThreshold, component) { + var extraArgs = []; + for (var _i = 4; _i < arguments.length; _i++) { + extraArgs[_i - 4] = arguments[_i]; + } + var eventUiBases = props.eventUiBases; + var eventSegs = this.sliceEventStore.apply(this, [props.eventStore, eventUiBases, dateProfile, nextDayThreshold, component].concat(extraArgs)); + return { + dateSelectionSegs: this.sliceDateSelection.apply(this, [props.dateSelection, eventUiBases, component].concat(extraArgs)), + businessHourSegs: this.sliceBusinessHours.apply(this, [props.businessHours, dateProfile, nextDayThreshold, component].concat(extraArgs)), + fgEventSegs: eventSegs.fg, + bgEventSegs: eventSegs.bg, + eventDrag: this.sliceEventDrag.apply(this, [props.eventDrag, eventUiBases, dateProfile, nextDayThreshold, component].concat(extraArgs)), + eventResize: this.sliceEventResize.apply(this, [props.eventResize, eventUiBases, dateProfile, nextDayThreshold, component].concat(extraArgs)), + eventSelection: props.eventSelection + }; // TODO: give interactionSegs? + }; + Slicer.prototype.sliceNowDate = function (// does not memoize + date, component) { + var extraArgs = []; + for (var _i = 2; _i < arguments.length; _i++) { + extraArgs[_i - 2] = arguments[_i]; + } + return this._sliceDateSpan.apply(this, [{ range: { start: date, end: addMs(date, 1) }, allDay: false }, + {}, + component].concat(extraArgs)); + }; + Slicer.prototype._sliceBusinessHours = function (businessHours, dateProfile, nextDayThreshold, component) { + var extraArgs = []; + for (var _i = 4; _i < arguments.length; _i++) { + extraArgs[_i - 4] = arguments[_i]; + } + if (!businessHours) { + return []; + } + return this._sliceEventStore.apply(this, [expandRecurring(businessHours, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), component.calendar), + {}, + dateProfile, + nextDayThreshold, + component].concat(extraArgs)).bg; + }; + Slicer.prototype._sliceEventStore = function (eventStore, eventUiBases, dateProfile, nextDayThreshold, component) { + var extraArgs = []; + for (var _i = 5; _i < arguments.length; _i++) { + extraArgs[_i - 5] = arguments[_i]; + } + if (eventStore) { + var rangeRes = sliceEventStore(eventStore, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold); + return { + bg: this.sliceEventRanges(rangeRes.bg, component, extraArgs), + fg: this.sliceEventRanges(rangeRes.fg, component, extraArgs) + }; + } + else { + return { bg: [], fg: [] }; + } + }; + Slicer.prototype._sliceInteraction = function (interaction, eventUiBases, dateProfile, nextDayThreshold, component) { + var extraArgs = []; + for (var _i = 5; _i < arguments.length; _i++) { + extraArgs[_i - 5] = arguments[_i]; + } + if (!interaction) { + return null; + } + var rangeRes = sliceEventStore(interaction.mutatedEvents, eventUiBases, computeActiveRange(dateProfile, Boolean(nextDayThreshold)), nextDayThreshold); + return { + segs: this.sliceEventRanges(rangeRes.fg, component, extraArgs), + affectedInstances: interaction.affectedEvents.instances, + isEvent: interaction.isEvent, + sourceSeg: interaction.origSeg + }; + }; + Slicer.prototype._sliceDateSpan = function (dateSpan, eventUiBases, component) { + var extraArgs = []; + for (var _i = 3; _i < arguments.length; _i++) { + extraArgs[_i - 3] = arguments[_i]; + } + if (!dateSpan) { + return []; + } + var eventRange = fabricateEventRange(dateSpan, eventUiBases, component.calendar); + var segs = this.sliceRange.apply(this, [dateSpan.range].concat(extraArgs)); + for (var _a = 0, segs_1 = segs; _a < segs_1.length; _a++) { + var seg = segs_1[_a]; + seg.component = component; + seg.eventRange = eventRange; + } + return segs; + }; + /* + "complete" seg means it has component and eventRange + */ + Slicer.prototype.sliceEventRanges = function (eventRanges, component, // TODO: kill + extraArgs) { + var segs = []; + for (var _i = 0, eventRanges_1 = eventRanges; _i < eventRanges_1.length; _i++) { + var eventRange = eventRanges_1[_i]; + segs.push.apply(segs, this.sliceEventRange(eventRange, component, extraArgs)); + } + return segs; + }; + /* + "complete" seg means it has component and eventRange + */ + Slicer.prototype.sliceEventRange = function (eventRange, component, // TODO: kill + extraArgs) { + var segs = this.sliceRange.apply(this, [eventRange.range].concat(extraArgs)); + for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) { + var seg = segs_2[_i]; + seg.component = component; + seg.eventRange = eventRange; + seg.isStart = eventRange.isStart && seg.isStart; + seg.isEnd = eventRange.isEnd && seg.isEnd; + } + return segs; + }; + return Slicer; + }()); + /* + for incorporating minTime/maxTime if appropriate + TODO: should be part of DateProfile! + TimelineDateProfile already does this btw + */ + function computeActiveRange(dateProfile, isComponentAllDay) { + var range = dateProfile.activeRange; + if (isComponentAllDay) { + return range; + } + return { + start: addMs(range.start, dateProfile.minTime.milliseconds), + end: addMs(range.end, dateProfile.maxTime.milliseconds - 864e5) // 864e5 = ms in a day + }; + } + + // exports + // -------------------------------------------------------------------------------------------------- + var version = '4.0.2'; + + exports.Calendar = Calendar; + exports.Component = Component; + exports.DateComponent = DateComponent; + exports.DateEnv = DateEnv; + exports.DateProfileGenerator = DateProfileGenerator; + exports.DayHeader = DayHeader; + exports.DaySeries = DaySeries; + exports.DayTable = DayTable; + exports.ElementDragging = ElementDragging; + exports.ElementScrollController = ElementScrollController; + exports.EmitterMixin = EmitterMixin; + exports.EventApi = EventApi; + exports.FgEventRenderer = FgEventRenderer; + exports.FillRenderer = FillRenderer; + exports.Interaction = Interaction; + exports.Mixin = Mixin; + exports.NamedTimeZoneImpl = NamedTimeZoneImpl; + exports.PositionCache = PositionCache; + exports.ScrollComponent = ScrollComponent; + exports.ScrollController = ScrollController; + exports.Slicer = Slicer; + exports.Splitter = Splitter; + exports.Theme = Theme; + exports.View = View; + exports.WindowScrollController = WindowScrollController; + exports.addDays = addDays; + exports.addDurations = addDurations; + exports.addMs = addMs; + exports.addWeeks = addWeeks; + exports.allowContextMenu = allowContextMenu; + exports.allowSelection = allowSelection; + exports.appendToElement = appendToElement; + exports.applyAll = applyAll; + exports.applyMutationToEventStore = applyMutationToEventStore; + exports.applyStyle = applyStyle; + exports.applyStyleProp = applyStyleProp; + exports.asRoughMinutes = asRoughMinutes; + exports.asRoughMs = asRoughMs; + exports.asRoughSeconds = asRoughSeconds; + exports.buildGotoAnchorHtml = buildGotoAnchorHtml; + exports.buildSegCompareObj = buildSegCompareObj; + exports.capitaliseFirstLetter = capitaliseFirstLetter; + exports.combineEventUis = combineEventUis; + exports.compareByFieldSpec = compareByFieldSpec; + exports.compareByFieldSpecs = compareByFieldSpecs; + exports.compareNumbers = compareNumbers; + exports.compensateScroll = compensateScroll; + exports.computeClippingRect = computeClippingRect; + exports.computeEdges = computeEdges; + exports.computeFallbackHeaderFormat = computeFallbackHeaderFormat; + exports.computeHeightAndMargins = computeHeightAndMargins; + exports.computeInnerRect = computeInnerRect; + exports.computeRect = computeRect; + exports.computeVisibleDayRange = computeVisibleDayRange; + exports.config = config; + exports.constrainPoint = constrainPoint; + exports.createDuration = createDuration; + exports.createElement = createElement; + exports.createEmptyEventStore = createEmptyEventStore; + exports.createEventInstance = createEventInstance; + exports.createFormatter = createFormatter; + exports.createPlugin = createPlugin; + exports.cssToStr = cssToStr; + exports.debounce = debounce; + exports.diffDates = diffDates; + exports.diffDayAndTime = diffDayAndTime; + exports.diffDays = diffDays; + exports.diffPoints = diffPoints; + exports.diffWeeks = diffWeeks; + exports.diffWholeDays = diffWholeDays; + exports.diffWholeWeeks = diffWholeWeeks; + exports.disableCursor = disableCursor; + exports.distributeHeight = distributeHeight; + exports.elementClosest = elementClosest; + exports.elementMatches = elementMatches; + exports.enableCursor = enableCursor; + exports.eventTupleToStore = eventTupleToStore; + exports.filterEventStoreDefs = filterEventStoreDefs; + exports.filterHash = filterHash; + exports.findChildren = findChildren; + exports.findElements = findElements; + exports.flexibleCompare = flexibleCompare; + exports.forceClassName = forceClassName; + exports.formatDate = formatDate; + exports.formatIsoTimeString = formatIsoTimeString; + exports.formatRange = formatRange; + exports.freezeRaw = freezeRaw; + exports.getAllDayHtml = getAllDayHtml; + exports.getClippingParents = getClippingParents; + exports.getDayClasses = getDayClasses; + exports.getElSeg = getElSeg; + exports.getRectCenter = getRectCenter; + exports.getRelevantEvents = getRelevantEvents; + exports.globalDefaults = globalDefaults; + exports.greatestDurationDenominator = greatestDurationDenominator; + exports.hasBgRendering = hasBgRendering; + exports.htmlEscape = htmlEscape; + exports.htmlToElement = htmlToElement; + exports.insertAfterElement = insertAfterElement; + exports.interactionSettingsStore = interactionSettingsStore; + exports.interactionSettingsToStore = interactionSettingsToStore; + exports.intersectRanges = intersectRanges; + exports.intersectRects = intersectRects; + exports.isArraysEqual = isArraysEqual; + exports.isDateSpansEqual = isDateSpansEqual; + exports.isInt = isInt; + exports.isInteractionValid = isInteractionValid; + exports.isMultiDayRange = isMultiDayRange; + exports.isObjectsSimilar = isObjectsSimilar; + exports.isPropsValid = isPropsValid; + exports.isSingleDay = isSingleDay; + exports.isValidDate = isValidDate; + exports.isValuesSimilar = isValuesSimilar; + exports.listenBySelector = listenBySelector; + exports.mapHash = mapHash; + exports.matchCellWidths = matchCellWidths; + exports.memoize = memoize; + exports.memoizeOutput = memoizeOutput; + exports.memoizeRendering = memoizeRendering; + exports.mergeEventStores = mergeEventStores; + exports.multiplyDuration = multiplyDuration; + exports.padStart = padStart; + exports.parseBusinessHours = parseBusinessHours; + exports.parseDragMeta = parseDragMeta; + exports.parseEventDef = parseEventDef; + exports.parseFieldSpecs = parseFieldSpecs; + exports.parseMarker = parse; + exports.pointInsideRect = pointInsideRect; + exports.prependToElement = prependToElement; + exports.preventContextMenu = preventContextMenu; + exports.preventDefault = preventDefault; + exports.preventSelection = preventSelection; + exports.processScopedUiProps = processScopedUiProps; + exports.rangeContainsMarker = rangeContainsMarker; + exports.rangeContainsRange = rangeContainsRange; + exports.rangesEqual = rangesEqual; + exports.rangesIntersect = rangesIntersect; + exports.refineProps = refineProps; + exports.removeElement = removeElement; + exports.removeExact = removeExact; + exports.renderDateCell = renderDateCell; + exports.requestJson = requestJson; + exports.sliceEventStore = sliceEventStore; + exports.startOfDay = startOfDay; + exports.subtractInnerElHeight = subtractInnerElHeight; + exports.translateRect = translateRect; + exports.uncompensateScroll = uncompensateScroll; + exports.undistributeHeight = undistributeHeight; + exports.unpromisify = unpromisify; + exports.version = version; + exports.whenTransitionDone = whenTransitionDone; + exports.wholeDivideDurations = wholeDivideDurations; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/library/fullcalendar/packages/core/main.min.css b/library/fullcalendar/packages/core/main.min.css new file mode 100644 index 000000000..3ac6b3e2f --- /dev/null +++ b/library/fullcalendar/packages/core/main.min.css @@ -0,0 +1,5 @@ +/*! +FullCalendar Core Package v4.0.2 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/.fc-button:not(:disabled),.fc-event.fc-draggable,.fc-event[href],.fc-popover .fc-header .fc-close,a.fc-more,a[data-goto]{cursor:pointer}.fc-bg,.fc-row .fc-bgevent-skeleton,.fc-row .fc-highlight-skeleton{bottom:0}.fc{direction:ltr;text-align:left}.fc-rtl{text-align:right}body .fc{font-size:1em}.fc-highlight{background:#bce8f1;opacity:.3}.fc-bgevent{background:#8fdf82;opacity:.3}.fc-nonbusiness{background:#d7d7d7}.fc-popover{position:absolute;box-shadow:0 2px 6px rgba(0,0,0,.15)}.fc-popover .fc-header{display:flex;flex-direction:row;justify-content:space-between;align-items:center;padding:2px 4px}.fc-rtl .fc-popover .fc-header{flex-direction:row-reverse}.fc-popover .fc-header .fc-title{margin:0 2px}.fc-popover .fc-header .fc-close{opacity:.65;font-size:1.1em}.fc-divider{border-style:solid;border-width:1px}hr.fc-divider{height:0;margin:0;padding:0 0 2px;border-width:1px 0}.fc-bg table,.fc-row .fc-bgevent-skeleton table,.fc-row .fc-highlight-skeleton table{height:100%}.fc-bg,.fc-bgevent-skeleton,.fc-highlight-skeleton,.fc-mirror-skeleton{position:absolute;top:0;left:0;right:0}.fc table{width:100%;box-sizing:border-box;table-layout:fixed;border-collapse:collapse;border-spacing:0;font-size:1em}.fc th{text-align:center}.fc td,.fc th{border-style:solid;border-width:1px;padding:0;vertical-align:top}.fc td.fc-today{border-style:double}a[data-goto]:hover{text-decoration:underline}.fc .fc-row{border-style:solid;border-width:0}.fc-row table{border-left:0 hidden transparent;border-right:0 hidden transparent;border-bottom:0 hidden transparent}.fc-row:first-child table{border-top:0 hidden transparent}.fc-row{position:relative}.fc-row .fc-bg{z-index:1}.fc-row .fc-bgevent-skeleton td,.fc-row .fc-highlight-skeleton td{border-color:transparent}.fc-row .fc-bgevent-skeleton{z-index:2}.fc-row .fc-highlight-skeleton{z-index:3}.fc-row .fc-content-skeleton{position:relative;z-index:4;padding-bottom:2px}.fc-row .fc-mirror-skeleton{z-index:5}.fc .fc-row .fc-content-skeleton table,.fc .fc-row .fc-content-skeleton td,.fc .fc-row .fc-mirror-skeleton td{background:0 0;border-color:transparent}.fc-row .fc-content-skeleton td,.fc-row .fc-mirror-skeleton td{border-bottom:0}.fc-row .fc-content-skeleton tbody td,.fc-row .fc-mirror-skeleton tbody td{border-top:0}.fc-scroller{-webkit-overflow-scrolling:touch}.fc-scroller>.fc-day-grid,.fc-scroller>.fc-time-grid{position:relative;width:100%}.fc-event{position:relative;display:block;font-size:.85em;line-height:1.4;border-radius:3px;border:1px solid #3788d8}.fc-event,.fc-event-dot{background-color:#3788d8}.fc-event,.fc-event:hover{color:#fff;text-decoration:none}.fc-not-allowed,.fc-not-allowed .fc-event{cursor:not-allowed}.fc-event .fc-content{position:relative;z-index:2}.fc-event .fc-resizer{position:absolute;z-index:4;display:none}.fc-event.fc-allow-mouse-resize .fc-resizer,.fc-event.fc-selected .fc-resizer{display:block}.fc-event.fc-selected .fc-resizer:before{content:"";position:absolute;z-index:9999;top:50%;left:50%;width:40px;height:40px;margin-left:-20px;margin-top:-20px}.fc-event.fc-selected{z-index:9999!important;box-shadow:0 2px 5px rgba(0,0,0,.2)}.fc-event.fc-selected:after{content:"";position:absolute;z-index:1;top:-1px;right:-1px;bottom:-1px;left:-1px;background:#000;opacity:.25}.fc-event.fc-dragging.fc-selected{box-shadow:0 2px 7px rgba(0,0,0,.3)}.fc-event.fc-dragging:not(.fc-selected){opacity:.75}.fc-h-event.fc-selected:before{content:"";position:absolute;z-index:3;top:-10px;bottom:-10px;left:0;right:0}.fc-ltr .fc-h-event.fc-not-start,.fc-rtl .fc-h-event.fc-not-end{margin-left:0;border-left-width:0;padding-left:1px;border-top-left-radius:0;border-bottom-left-radius:0}.fc-ltr .fc-h-event.fc-not-end,.fc-rtl .fc-h-event.fc-not-start{margin-right:0;border-right-width:0;padding-right:1px;border-top-right-radius:0;border-bottom-right-radius:0}.fc-ltr .fc-h-event .fc-start-resizer,.fc-rtl .fc-h-event .fc-end-resizer{cursor:w-resize;left:-1px}.fc-ltr .fc-h-event .fc-end-resizer,.fc-rtl .fc-h-event .fc-start-resizer{cursor:e-resize;right:-1px}.fc-h-event.fc-allow-mouse-resize .fc-resizer{width:7px;top:-1px;bottom:-1px}.fc-h-event.fc-selected .fc-resizer{border-radius:4px;border-width:1px;width:6px;height:6px;border-style:solid;border-color:inherit;background:#fff;top:50%;margin-top:-4px}.fc-ltr .fc-h-event.fc-selected .fc-start-resizer,.fc-rtl .fc-h-event.fc-selected .fc-end-resizer{margin-left:-4px}.fc-ltr .fc-h-event.fc-selected .fc-end-resizer,.fc-rtl .fc-h-event.fc-selected .fc-start-resizer{margin-right:-4px}.fc-day-grid-event{margin:1px 2px 0;padding:0 1px}tr:first-child>td>.fc-day-grid-event{margin-top:2px}.fc-mirror-skeleton tr:first-child>td>.fc-day-grid-event{margin-top:0}.fc-day-grid-event .fc-content{white-space:nowrap;overflow:hidden}.fc-day-grid-event .fc-time{font-weight:700}.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer,.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer{margin-left:-2px}.fc-ltr .fc-day-grid-event.fc-allow-mouse-resize .fc-end-resizer,.fc-rtl .fc-day-grid-event.fc-allow-mouse-resize .fc-start-resizer{margin-right:-2px}a.fc-more{margin:1px 3px;font-size:.85em;text-decoration:none}a.fc-more:hover{text-decoration:underline}.fc-limited{display:none}.fc-button,.fc-icon{display:inline-block;font-weight:400;text-align:center}.fc-day-grid .fc-row{z-index:1}.fc-more-popover{z-index:2;width:220px}.fc-more-popover .fc-event-container{padding:10px}.fc-now-indicator{position:absolute;border:0 solid red}.fc-unselectable{-webkit-user-select:none;-khtml-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;-webkit-touch-callout:none;-webkit-tap-highlight-color:transparent}.fc-unthemed .fc-content,.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-list-view,.fc-unthemed .fc-popover,.fc-unthemed .fc-row,.fc-unthemed tbody,.fc-unthemed td,.fc-unthemed th,.fc-unthemed thead{border-color:#ddd}.fc-unthemed .fc-popover{background-color:#fff}.fc-unthemed .fc-divider,.fc-unthemed .fc-list-heading td,.fc-unthemed .fc-popover .fc-header{background:#eee}.fc-unthemed td.fc-today{background:#fcf8e3}.fc-unthemed .fc-disabled-day{background:#d7d7d7;opacity:.3}@font-face{font-family:fcicons;src:url("data:application/x-font-ttf;charset=utf-8;base64,AAEAAAALAIAAAwAwT1MvMg8SBfAAAAC8AAAAYGNtYXAXVtKNAAABHAAAAFRnYXNwAAAAEAAAAXAAAAAIZ2x5ZgYydxIAAAF4AAAFNGhlYWQUJ7cIAAAGrAAAADZoaGVhB20DzAAABuQAAAAkaG10eCIABhQAAAcIAAAALGxvY2ED4AU6AAAHNAAAABhtYXhwAA8AjAAAB0wAAAAgbmFtZXsr690AAAdsAAABhnBvc3QAAwAAAAAI9AAAACAAAwPAAZAABQAAApkCzAAAAI8CmQLMAAAB6wAzAQkAAAAAAAAAAAAAAAAAAAABEAAAAAAAAAAAAAAAAAAAAABAAADpBgPA/8AAQAPAAEAAAAABAAAAAAAAAAAAAAAgAAAAAAADAAAAAwAAABwAAQADAAAAHAADAAEAAAAcAAQAOAAAAAoACAACAAIAAQAg6Qb//f//AAAAAAAg6QD//f//AAH/4xcEAAMAAQAAAAAAAAAAAAAAAQAB//8ADwABAAAAAAAAAAAAAgAANzkBAAAAAAEAAAAAAAAAAAACAAA3OQEAAAAAAQAAAAAAAAAAAAIAADc5AQAAAAABAWIAjQKeAskAEwAAJSc3NjQnJiIHAQYUFwEWMjc2NCcCnuLiDQ0MJAz/AA0NAQAMJAwNDcni4gwjDQwM/wANIwz/AA0NDCMNAAAAAQFiAI0CngLJABMAACUBNjQnASYiBwYUHwEHBhQXFjI3AZ4BAA0N/wAMJAwNDeLiDQ0MJAyNAQAMIw0BAAwMDSMM4uINIwwNDQAAAAIA4gC3Ax4CngATACcAACUnNzY0JyYiDwEGFB8BFjI3NjQnISc3NjQnJiIPAQYUHwEWMjc2NCcB87e3DQ0MIw3VDQ3VDSMMDQ0BK7e3DQ0MJAzVDQ3VDCQMDQ3zuLcMJAwNDdUNIwzWDAwNIwy4twwkDA0N1Q0jDNYMDA0jDAAAAgDiALcDHgKeABMAJwAAJTc2NC8BJiIHBhQfAQcGFBcWMjchNzY0LwEmIgcGFB8BBwYUFxYyNwJJ1Q0N1Q0jDA0Nt7cNDQwjDf7V1Q0N1QwkDA0Nt7cNDQwkDLfWDCMN1Q0NDCQMt7gMIw0MDNYMIw3VDQ0MJAy3uAwjDQwMAAADAFUAAAOrA1UAMwBoAHcAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMhMjY1NCYjISIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAAVYRGRkR/qoRGRkRA1UFBAUOCQkVDAsZDf2rDRkLDBUJCA4FBQUFBQUOCQgVDAsZDQJVDRkLDBUJCQ4FBAVVAgECBQMCBwQECAX9qwQJAwQHAwMFAQICAgIBBQMDBwQDCQQCVQUIBAQHAgMFAgEC/oAZEhEZGRESGQAAAAADAFUAAAOrA1UAMwBoAIkAABMiBgcOAQcOAQcOARURFBYXHgEXHgEXHgEzITI2Nz4BNz4BNz4BNRE0JicuAScuAScuASMFITIWFx4BFx4BFx4BFREUBgcOAQcOAQcOASMhIiYnLgEnLgEnLgE1ETQ2Nz4BNz4BNz4BMxMzFRQWMzI2PQEzMjY1NCYrATU0JiMiBh0BIyIGFRQWM9UNGAwLFQkJDgUFBQUFBQ4JCRULDBgNAlYNGAwLFQkJDgUFBQUFBQ4JCRULDBgN/aoCVgQIBAQHAwMFAQIBAQIBBQMDBwQECAT9qgQIBAQHAwMFAQIBAQIBBQMDBwQECASAgBkSEhmAERkZEYAZEhIZgBEZGREDVQUEBQ4JCRUMCxkN/asNGQsMFQkIDgUFBQUFBQ4JCBUMCxkNAlUNGQsMFQkJDgUEBVUCAQIFAwIHBAQIBf2rBAkDBAcDAwUBAgICAgEFAwMHBAMJBAJVBQgEBAcCAwUCAQL+gIASGRkSgBkSERmAEhkZEoAZERIZAAABAOIAjQMeAskAIAAAExcHBhQXFjI/ARcWMjc2NC8BNzY0JyYiDwEnJiIHBhQX4uLiDQ0MJAzi4gwkDA0N4uINDQwkDOLiDCQMDQ0CjeLiDSMMDQ3h4Q0NDCMN4uIMIw0MDOLiDAwNIwwAAAABAAAAAQAAa5n0y18PPPUACwQAAAAAANivOVsAAAAA2K85WwAAAAADqwNVAAAACAACAAAAAAAAAAEAAAPA/8AAAAQAAAAAAAOrAAEAAAAAAAAAAAAAAAAAAAALBAAAAAAAAAAAAAAAAgAAAAQAAWIEAAFiBAAA4gQAAOIEAABVBAAAVQQAAOIAAAAAAAoAFAAeAEQAagCqAOoBngJkApoAAQAAAAsAigADAAAAAAACAAAAAAAAAAAAAAAAAAAAAAAAAA4ArgABAAAAAAABAAcAAAABAAAAAAACAAcAYAABAAAAAAADAAcANgABAAAAAAAEAAcAdQABAAAAAAAFAAsAFQABAAAAAAAGAAcASwABAAAAAAAKABoAigADAAEECQABAA4ABwADAAEECQACAA4AZwADAAEECQADAA4APQADAAEECQAEAA4AfAADAAEECQAFABYAIAADAAEECQAGAA4AUgADAAEECQAKADQApGZjaWNvbnMAZgBjAGkAYwBvAG4Ac1ZlcnNpb24gMS4wAFYAZQByAHMAaQBvAG4AIAAxAC4AMGZjaWNvbnMAZgBjAGkAYwBvAG4Ac2ZjaWNvbnMAZgBjAGkAYwBvAG4Ac1JlZ3VsYXIAUgBlAGcAdQBsAGEAcmZjaWNvbnMAZgBjAGkAYwBvAG4Ac0ZvbnQgZ2VuZXJhdGVkIGJ5IEljb01vb24uAEYAbwBuAHQAIABnAGUAbgBlAHIAYQB0AGUAZAAgAGIAeQAgAEkAYwBvAE0AbwBvAG4ALgAAAAMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=") format("truetype");font-weight:400;font-style:normal}.fc-icon{font-family:fcicons!important;speak:none;font-style:normal;font-variant:normal;text-transform:none;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;width:1em;height:1em}.fc-icon-chevron-left:before{content:"\e900"}.fc-icon-chevron-right:before{content:"\e901"}.fc-icon-chevrons-left:before{content:"\e902"}.fc-icon-chevrons-right:before{content:"\e903"}.fc-icon-minus-square:before{content:"\e904"}.fc-icon-plus-square:before{content:"\e905"}.fc-icon-x:before{content:"\e906"}.fc-button{overflow:visible;text-transform:none;margin:0;font-family:inherit}.fc-button::-moz-focus-inner{padding:0;border-style:none}.fc-button{-webkit-appearance:button;color:#212529;vertical-align:middle;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;padding:.4em .65em;font-size:1em;line-height:1.5;border-radius:.25em}.fc-button:hover{color:#212529;text-decoration:none}.fc-button:focus{outline:0;-webkit-box-shadow:0 0 0 .2rem rgba(44,62,80,.25);box-shadow:0 0 0 .2rem rgba(44,62,80,.25)}.fc-button:disabled{opacity:.65}.fc-button-primary{color:#fff;background-color:#2C3E50;border-color:#2C3E50}.fc-button-primary:hover{color:#fff;background-color:#1e2b37;border-color:#1a252f}.fc-button-primary:focus{-webkit-box-shadow:0 0 0 .2rem rgba(76,91,106,.5);box-shadow:0 0 0 .2rem rgba(76,91,106,.5)}.fc-button-primary:disabled{color:#fff;background-color:#2C3E50;border-color:#2C3E50}.fc-button-primary:not(:disabled).fc-button-active,.fc-button-primary:not(:disabled):active{color:#fff;background-color:#1a252f;border-color:#151e27}.fc-button-primary:not(:disabled).fc-button-active:focus,.fc-button-primary:not(:disabled):active:focus{-webkit-box-shadow:0 0 0 .2rem rgba(76,91,106,.5);box-shadow:0 0 0 .2rem rgba(76,91,106,.5)}.fc-button .fc-icon{vertical-align:middle;font-size:1.5em}.fc-button-group{position:relative;display:-webkit-inline-box;display:-ms-inline-flexbox;display:inline-flex;vertical-align:middle}.fc-button-group>.fc-button{position:relative;-webkit-box-flex:1;-ms-flex:1 1 auto;flex:1 1 auto}.fc-button-group>.fc-button.fc-button-active,.fc-button-group>.fc-button:active,.fc-button-group>.fc-button:focus,.fc-button-group>.fc-button:hover{z-index:1}.fc-button-group>.fc-button:not(:first-child){margin-left:-1px;border-top-left-radius:0;border-bottom-left-radius:0}.fc-button-group>.fc-button:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0}.fc-unthemed .fc-popover{border-width:1px;border-style:solid}.fc-unthemed .fc-list-item:hover td{background-color:#f5f5f5}.fc-toolbar{display:flex;justify-content:space-between;align-items:center}.fc-toolbar.fc-header-toolbar{margin-bottom:1.5em}.fc-toolbar.fc-footer-toolbar{margin-top:1.5em}.fc-toolbar>*>:not(:first-child){margin-left:.75em}.fc-toolbar h2{font-size:1.75em;margin:0}.fc-view-container{position:relative}.fc-view-container *,.fc-view-container :after,.fc-view-container :before{-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box}.fc-view,.fc-view>table{position:relative;z-index:1}@media print{.fc-bg,.fc-bgevent-container,.fc-bgevent-skeleton,.fc-business-container,.fc-event .fc-resizer,.fc-highlight-container,.fc-highlight-skeleton,.fc-mirror-container,.fc-mirror-skeleton{display:none}.fc tbody .fc-row,.fc-time-grid{min-height:0!important}.fc-time-grid .fc-event.fc-not-end:after,.fc-time-grid .fc-event.fc-not-start:before{content:"..."}.fc{max-width:100%!important}.fc-event{background:#fff!important;color:#000!important;page-break-inside:avoid}.fc hr,.fc tbody,.fc td,.fc th,.fc thead,.fc-row{border-color:#ccc!important;background:#fff!important}.fc tbody .fc-row{height:auto!important}.fc tbody .fc-row .fc-content-skeleton{position:static;padding-bottom:0!important}.fc tbody .fc-row .fc-content-skeleton tbody tr:last-child td{padding-bottom:1em}.fc tbody .fc-row .fc-content-skeleton table{height:1em}.fc-more,.fc-more-cell{display:none!important}.fc tr.fc-limited{display:table-row!important}.fc td.fc-limited{display:table-cell!important}.fc-popover,.fc-timeGrid-view .fc-axis{display:none}.fc-slats,.fc-time-grid hr{display:none!important}.fc button,.fc-button-group,.fc-time-grid .fc-event .fc-time span{display:none}.fc-time-grid .fc-content-skeleton{position:static}.fc-time-grid .fc-content-skeleton table{height:4em}.fc-time-grid .fc-event-container{margin:0!important}.fc-time-grid .fc-event{position:static!important;margin:3px 2px!important}.fc-time-grid .fc-event.fc-not-end{border-bottom-width:1px!important}.fc-time-grid .fc-event.fc-not-start{border-top-width:1px!important}.fc-time-grid .fc-event .fc-time{white-space:normal!important}.fc-time-grid .fc-event .fc-time:after{content:attr(data-full)}.fc-day-grid-container,.fc-scroller,.fc-time-grid-container{overflow:visible!important;height:auto!important}.fc-row{border:0!important;margin:0!important}} \ No newline at end of file diff --git a/library/fullcalendar/packages/core/main.min.js b/library/fullcalendar/packages/core/main.min.js new file mode 100644 index 000000000..a961abf95 --- /dev/null +++ b/library/fullcalendar/packages/core/main.min.js @@ -0,0 +1,9 @@ +/*! +FullCalendar Core Package v4.0.2 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports):"function"==typeof define&&define.amd?define(["exports"],t):(e=e||self,t(e.FullCalendar={}))}(this,function(e){"use strict";function t(e,t,n){var r=document.createElement(e);if(t)for(var i in t)"style"===i?g(r,t[i]):mi[i]?r[i]=t[i]:r.setAttribute(i,t[i]);return"string"==typeof n?r.innerHTML=n:null!=n&&a(r,n),r}function n(e){e=e.trim();var t=document.createElement(o(e));return t.innerHTML=e,t.firstChild}function r(e){return Array.prototype.slice.call(i(e))}function i(e){e=e.trim();var t=document.createElement(o(e));return t.innerHTML=e,t.childNodes}function o(e){return Ei[e.substr(0,3)]||"div"}function a(e,t){for(var n=l(t),r=0;r=t.left&&e.left=t.top&&e.top
");document.body.appendChild(e);var n=e.firstChild,r=n.getBoundingClientRect().left>e.getBoundingClientRect().left;return c(e),r}function I(e){return e=Math.max(0,e),e=Math.round(e)}function C(e,t){void 0===t&&(t=!1);var n=window.getComputedStyle(e),r=parseInt(n.borderLeftWidth,10)||0,i=parseInt(n.borderRightWidth,10)||0,o=parseInt(n.borderTopWidth,10)||0,a=parseInt(n.borderBottomWidth,10)||0,s=I(e.offsetWidth-e.clientWidth-r-i),u=I(e.offsetHeight-e.clientHeight-o-a),l={borderLeft:r,borderRight:i,borderTop:o,borderBottom:a,scrollbarBottom:u,scrollbarLeft:0,scrollbarRight:0};return w()&&"rtl"===n.direction?l.scrollbarLeft=s:l.scrollbarRight=s,t&&(l.paddingLeft=parseInt(n.paddingLeft,10)||0,l.paddingRight=parseInt(n.paddingRight,10)||0,l.paddingTop=parseInt(n.paddingTop,10)||0,l.paddingBottom=parseInt(n.paddingBottom,10)||0),l}function M(e,t){void 0===t&&(t=!1);var n=k(e),r=C(e,t),i={left:n.left+r.borderLeft+r.scrollbarLeft,right:n.right-r.borderRight-r.scrollbarRight,top:n.top+r.borderTop,bottom:n.bottom-r.borderBottom-r.scrollbarBottom};return t&&(i.left+=r.paddingLeft,i.right-=r.paddingRight,i.top+=r.paddingTop,i.bottom-=r.paddingBottom),i}function k(e){var t=e.getBoundingClientRect();return{left:t.left+window.pageXOffset,top:t.top+window.pageYOffset,right:t.right+window.pageXOffset,bottom:t.bottom+window.pageYOffset}}function O(){return{left:window.pageXOffset,right:window.pageXOffset+document.documentElement.clientWidth,top:window.pageYOffset,bottom:window.pageYOffset+document.documentElement.clientHeight}}function _(e){var t=window.getComputedStyle(e);return e.getBoundingClientRect().height+parseInt(t.marginTop,10)+parseInt(t.marginBottom,10)}function P(e){for(var t=[];e instanceof HTMLElement;){var n=window.getComputedStyle(e);if("fixed"===n.position)break;/(auto|scroll)/.test(n.overflow+n.overflowY+n.overflowX)&&t.push(e),e=e.parentNode}return t}function H(e){return P(e).map(function(e){return M(e)}).concat(O()).reduce(function(e,t){return E(e,t)||t})}function x(e){e.preventDefault()}function N(e,t,n,r){function i(e){var t=d(e.target,n);t&&r.call(t,e,t)}return e.addEventListener(t,i),function(){e.removeEventListener(t,i)}}function z(e,t,n,r){var i;return N(e,"mouseover",t,function(e,t){if(t!==i){i=t,n(e,t);var o=function(e){i=null,r(e,t),t.removeEventListener("mouseleave",o)};t.addEventListener("mouseleave",o)}})}function U(e,t){var n=function(r){t(r),wi.forEach(function(t){e.removeEventListener(t,n)})};wi.forEach(function(t){e.addEventListener(t,n)})}function L(e,t){var n=ie(e);return n[2]+=7*t,oe(n)}function A(e,t){var n=ie(e);return n[2]+=t,oe(n)}function V(e,t){var n=ie(e);return n[6]+=t,oe(n)}function B(e,t){return F(e,t)/7}function F(e,t){return(t.valueOf()-e.valueOf())/864e5}function W(e,t){return(t.valueOf()-e.valueOf())/36e5}function Z(e,t){return(t.valueOf()-e.valueOf())/6e4}function j(e,t){return(t.valueOf()-e.valueOf())/1e3}function Y(e,t){var n=X(e),r=X(t);return{years:0,months:0,days:Math.round(F(n,r)),milliseconds:t.valueOf()-r.valueOf()-(e.valueOf()-n.valueOf())}}function q(e,t){var n=G(e,t);return null!==n&&n%7==0?n/7:null}function G(e,t){return se(e)===se(t)?Math.round(F(e,t)):null}function X(e){return oe([e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate()])}function J(e){return oe([e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),e.getUTCHours()])}function K(e){return oe([e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),e.getUTCHours(),e.getUTCMinutes()])}function Q(e){return oe([e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),e.getUTCHours(),e.getUTCMinutes(),e.getUTCSeconds()])}function $(e,t,n){var r=e.getUTCFullYear(),i=ee(e,r,t,n);if(i<1)return ee(e,r-1,t,n);var o=ee(e,r+1,t,n);return o>=1?Math.min(i,o):i}function ee(e,t,n,r){var i=oe([t,0,1+te(t,n,r)]),o=X(e),a=Math.round(F(i,o));return Math.floor(a/7)+1}function te(e,t,n){var r=7+t-n;return-(7+oe([e,0,r]).getUTCDay()-t)%7+r-1}function ne(e){return[e.getFullYear(),e.getMonth(),e.getDate(),e.getHours(),e.getMinutes(),e.getSeconds(),e.getMilliseconds()]}function re(e){return new Date(e[0],e[1]||0,null==e[2]?1:e[2],e[3]||0,e[4]||0,e[5]||0)}function ie(e){return[e.getUTCFullYear(),e.getUTCMonth(),e.getUTCDate(),e.getUTCHours(),e.getUTCMinutes(),e.getUTCSeconds(),e.getUTCMilliseconds()]}function oe(e){return 1===e.length&&(e=e.concat([0])),new Date(Date.UTC.apply(Date,e))}function ae(e){return!isNaN(e.valueOf())}function se(e){return 1e3*e.getUTCHours()*60*60+1e3*e.getUTCMinutes()*60+1e3*e.getUTCSeconds()+e.getUTCMilliseconds()}function ue(e,t){var n;return"string"==typeof e?le(e):"object"==typeof e&&e?ce(e):"number"==typeof e?ce((n={},n[t||"milliseconds"]=e,n)):null}function le(e){var t=Ci.exec(e);if(t){var n=t[1]?-1:1;return{years:0,months:0,days:n*(t[2]?parseInt(t[2],10):0),milliseconds:n*(60*(t[3]?parseInt(t[3],10):0)*60*1e3+60*(t[4]?parseInt(t[4],10):0)*1e3+1e3*(t[5]?parseInt(t[5],10):0)+(t[6]?parseInt(t[6],10):0))}}return null}function ce(e){return{years:e.years||e.year||0,months:e.months||e.month||0,days:(e.days||e.day||0)+7*de(e),milliseconds:60*(e.hours||e.hour||0)*60*1e3+60*(e.minutes||e.minute||0)*1e3+1e3*(e.seconds||e.second||0)+(e.milliseconds||e.millisecond||e.ms||0)}}function de(e){return e.weeks||e.week||0}function fe(e,t){return e.years===t.years&&e.months===t.months&&e.days===t.days&&e.milliseconds===t.milliseconds}function pe(e){return 0===e.years&&0===e.months&&1===e.days&&0===e.milliseconds}function he(e,t){return{years:e.years+t.years,months:e.months+t.months,days:e.days+t.days,milliseconds:e.milliseconds+t.milliseconds}}function ve(e,t){return{years:e.years-t.years,months:e.months-t.months,days:e.days-t.days,milliseconds:e.milliseconds-t.milliseconds}}function ge(e,t){return{years:e.years*t,months:e.months*t,days:e.days*t,milliseconds:e.milliseconds*t}}function ye(e){return Ee(e)/365}function me(e){return Ee(e)/30}function Ee(e){return be(e)/864e5}function Se(e){return be(e)/6e4}function De(e){return be(e)/1e3}function be(e){return 31536e6*e.years+2592e6*e.months+864e5*e.days+e.milliseconds}function Te(e,t){for(var n=null,r=0;rt&&(t=r)}}),t++,e.forEach(function(e){e.style.width=t+"px"}),t}function Pe(e,t){var n={position:"relative",left:-1};g(e,n),g(t,n);var r=e.offsetHeight-t.offsetHeight,i={position:"",left:""};return g(e,i),g(t,i),r}function He(e){e.classList.add("fc-unselectable"),e.addEventListener("selectstart",x)}function xe(e){e.classList.remove("fc-unselectable"),e.removeEventListener("selectstart",x)}function Ne(e){e.addEventListener("contextmenu",x)}function ze(e){e.removeEventListener("contextmenu",x)}function Ue(e){var t,n,r=[],i=[];for("string"==typeof e?i=e.split(/\s*,\s*/):"function"==typeof e?i=[e]:Array.isArray(e)&&(i=e),t=0;t=be(t)&&(r=A(r,1))}return e.start&&(n=X(e.start),r&&r<=n&&(r=A(n,1))),{start:n,end:r}}function Qe(e){var t=Ke(e);return F(t.start,t.end)>1}function $e(e,t,n,r){return"year"===r?ue(n.diffWholeYears(e,t),"year"):"month"===r?ue(n.diffWholeMonths(e,t),"month"):Y(e,t)}function et(e,t){function n(){this.constructor=e}Mi(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}function tt(e,t,n,r,i){for(var o=0;o=0;o--)if("object"==typeof(a=e[o][r])&&a)i.unshift(a);else if(void 0!==a){u[r]=a;break}i.length&&(u[r]=rt(i))}for(n=e.length-1;n>=0;n--){s=e[n];for(r in s)r in u||(u[r]=s[r])}return u}function it(e,t){var n={};for(var r in e)t(e[r],r)&&(n[r]=e[r]);return n}function ot(e,t){var n={};for(var r in e)n[r]=t(e[r],r);return n}function at(e){for(var t={},n=0,r=e;no&&i.push({start:o,end:r.start}),r.end>o&&(o=r.end);return ot.start)&&(null===e.start||null===t.end||e.start=e.start)&&(null===e.end||null!==t.end&&t.end<=e.end)}function Rt(e,t){return(null===e.start||t>=e.start)&&(null===e.end||t=t.end?new Date(t.end.valueOf()-1):e}function Ct(e,t){for(var n=0,r=0;r10&&(null==t?r=r.replace("Z",""):0!==t&&(r=r.replace("Z",Wt(t,!0)))),r}function Ft(e){return Fe(e.getUTCHours(),2)+":"+Fe(e.getUTCMinutes(),2)+":"+Fe(e.getUTCSeconds(),2)}function Wt(e,t){void 0===t&&(t=!1);var n=e<0?"-":"+",r=Math.abs(e),i=Math.floor(r/60),o=Math.round(r%60);return t?n+Fe(i,2)+":"+Fe(o,2):"GMT"+n+i+(o?":"+Fe(o,2):"")}function Zt(e,t,n,r){var i=jt(e,n.calendarSystem);return{date:i,start:i,end:t?jt(t,n.calendarSystem):null,timeZone:n.timeZone,localeCodes:n.locale.codes,separator:r}}function jt(e,t){var n=t.markerToArray(e.marker);return{marker:e.marker,timeZoneOffset:e.timeZoneOffset,array:n,year:n[0],month:n[1],day:n[2],hour:n[3],minute:n[4],second:n[5],millisecond:n[6]}}function Yt(e,t,n,r){var i={},o={},a={},s=[],u=[],l=Kt(e.defs,t);for(var c in e.defs){var d=e.defs[c];"inverse-background"===d.rendering&&(d.groupId?(i[d.groupId]=[],a[d.groupId]||(a[d.groupId]=d)):o[c]=[])}for(var f in e.instances){var p=e.instances[f],d=e.defs[p.defId],h=l[d.defId],v=p.range,g=!d.allDay&&r?Ke(v,r):v,y=Dt(g,n);y&&("inverse-background"===d.rendering?d.groupId?i[d.groupId].push(y):o[p.defId].push(y):("background"===d.rendering?s:u).push({def:d,ui:h,instance:p,range:y,isStart:g.start&&g.start.valueOf()===y.start.valueOf(),isEnd:g.end&&g.end.valueOf()===y.end.valueOf()}))}for(var m in i)for(var E=i[m],S=Et(E,n),D=0,b=S;D/g,">").replace(/'/g,"'").replace(/"/g,""").replace(/\n/g,"
")}function Tn(e){var t=[];for(var n in e){var r=e[n];null!=r&&""!==r&&t.push(n+":"+r)}return t.join(";")}function wn(e){var t=[];for(var n in e){var r=e[n];null!=r&&t.push(n+'="'+bn(r)+'"')}return t.join(" ")}function Rn(e){return Array.isArray(e)?e:"string"==typeof e?e.split(/\s+/):[]}function In(e,t,n){var r=Ge(e,Fi,{},n),i=Dn(r.constraint,t);return{startEditable:null!=r.startEditable?r.startEditable:r.editable,durationEditable:null!=r.durationEditable?r.durationEditable:r.editable,constraints:null!=i?[i]:[],overlap:r.overlap,allows:null!=r.allow?[r.allow]:[],backgroundColor:r.backgroundColor||r.color,borderColor:r.borderColor||r.color,textColor:r.textColor,classNames:r.classNames.concat(r.className)}}function Cn(e,t,n,r){var i={},o={};for(var a in Fi){var s=e+Be(a);i[a]=t[s],o[s]=!0}if("event"===e&&(i.editable=t.editable),r)for(var a in t)o[a]||(r[a]=t[a]);return In(i,n)}function Mn(e){return e.reduce(kn,Wi)}function kn(e,t){return{startEditable:null!=t.startEditable?t.startEditable:e.startEditable,durationEditable:null!=t.durationEditable?t.durationEditable:e.durationEditable,constraints:e.constraints.concat(t.constraints),overlap:"boolean"==typeof t.overlap?t.overlap:e.overlap,allows:e.allows.concat(t.allows),backgroundColor:t.backgroundColor||e.backgroundColor,borderColor:t.borderColor||e.borderColor,textColor:t.textColor||e.textColor,classNames:e.classNames.concat(t.classNames)}}function On(e,t,n,r){var i=zn(t,n),o={},a=tt(e,i,n.dateEnv,n.pluginSystem.hooks.recurringTypes,o);if(a){var s=_n(o,t,a.allDay,Boolean(a.duration),n);return s.recurringDef={typeId:a.typeId,typeData:a.typeData,duration:a.duration},{def:s,instance:null}}var u={},l=Hn(e,i,n,u,r);if(l){var s=_n(u,t,l.allDay,l.hasEnd,n);return{def:s,instance:Pn(s.defId,l.range,l.forcedStartTzo,l.forcedEndTzo)}}return null}function _n(e,t,n,r,i){var o={},a=Nn(e,i,o);a.defId=String(Yi++),a.sourceId=t,a.allDay=n,a.hasEnd=r;for(var s=0,u=i.pluginSystem.hooks.eventDefParsers;s0){if(e.length!==t.length)return!1;for(var r=0;r0){for(var r in e)if(!(r in t))return!1;for(var r in t){if(!(r in e))return!1;if(!Vn(e[r],t[r],n-1))return!1}return!0}return!1}function Wn(e,t,n){void 0===n&&(n=1);var r={};for(var i in t)i in e&&Vn(e[i],t[i],n-1)||(r[i]=t[i]);return r}function Zn(e,t){for(var n in e)if(!(n in t))return!0;return!1}function jn(e,t,n){var r=[];e&&r.push(e),t&&r.push(t);var i={"":Mn(r)};return n&&ki(i,n),i}function Yn(e,t,n,r){var i,o,a,s,u=e.dateEnv;return t instanceof Date?i=t:(i=t.date,o=t.type,a=t.forceOff),s={date:u.formatIso(i,{omitTime:!0}),type:o||"day"},"string"==typeof n&&(r=n,n=null),n=n?" "+wn(n):"",r=r||"",!a&&e.opt("navLinks")?"'+r+"
":""+r+""}function qn(e){return e.opt("allDayHtml")||bn(e.opt("allDayText"))} +function Gn(e,t,n,r){var i,o,a=n.calendar,s=n.view,u=n.theme,l=n.dateEnv,c=[];return Rt(t.activeRange,e)?(c.push("fc-"+Ri[e.getUTCDay()]),s.opt("monthMode")&&l.getMonth(e)!==l.getMonth(t.currentRange.start)&&c.push("fc-other-month"),i=X(a.getNow()),o=A(i,1),e=o?c.push("fc-future"):(c.push("fc-today"),!0!==r&&c.push(u.getClass("today")))):c.push("fc-disabled-day"),c}function Xn(e,t,n){var r=!1,i=function(){r||(r=!0,t.apply(this,arguments))},o=function(){r||(r=!0,n&&n.apply(this,arguments))},a=e(i,o);a&&"function"==typeof a.then&&a.then(i,o)}function Jn(e,t,n){(e[t]||(e[t]=[])).push(n)}function Kn(e,t,n){n?e[t]&&(e[t]=e[t].filter(function(e){return e!==n})):delete e[t]}function Qn(e,t,n){var r={},i=!1;for(var o in t)o in e&&(e[o]===t[o]||n[o]&&n[o](e[o],t[o]))?r[o]=e[o]:(r[o]=t[o],i=!0);for(var o in e)if(!(o in t)){i=!0;break}return{anyChanges:i,comboProps:r}}function $n(e){return{id:String(so++),deps:e.deps||[],reducers:e.reducers||[],eventDefParsers:e.eventDefParsers||[],eventDragMutationMassagers:e.eventDragMutationMassagers||[],eventDefMutationAppliers:e.eventDefMutationAppliers||[],dateSelectionTransformers:e.dateSelectionTransformers||[],datePointTransforms:e.datePointTransforms||[],dateSpanTransforms:e.dateSpanTransforms||[],views:e.views||{},viewPropsTransformers:e.viewPropsTransformers||[],isPropsValid:e.isPropsValid||null,externalDefTransforms:e.externalDefTransforms||[],eventResizeJoinTransforms:e.eventResizeJoinTransforms||[],viewContainerModifiers:e.viewContainerModifiers||[],eventDropTransformers:e.eventDropTransformers||[],componentInteractions:e.componentInteractions||[],calendarInteractions:e.calendarInteractions||[],themeClasses:e.themeClasses||{},eventSourceDefs:e.eventSourceDefs||[],cmdFormatter:e.cmdFormatter,recurringTypes:e.recurringTypes||[],namedTimeZonedImpl:e.namedTimeZonedImpl,defaultView:e.defaultView||"",elementDraggingImpl:e.elementDraggingImpl,optionChangeHandlers:e.optionChangeHandlers||{}}}function er(e,t){return{reducers:e.reducers.concat(t.reducers),eventDefParsers:e.eventDefParsers.concat(t.eventDefParsers),eventDragMutationMassagers:e.eventDragMutationMassagers.concat(t.eventDragMutationMassagers),eventDefMutationAppliers:e.eventDefMutationAppliers.concat(t.eventDefMutationAppliers),dateSelectionTransformers:e.dateSelectionTransformers.concat(t.dateSelectionTransformers),datePointTransforms:e.datePointTransforms.concat(t.datePointTransforms),dateSpanTransforms:e.dateSpanTransforms.concat(t.dateSpanTransforms),views:ki({},e.views,t.views),viewPropsTransformers:e.viewPropsTransformers.concat(t.viewPropsTransformers),isPropsValid:t.isPropsValid||e.isPropsValid,externalDefTransforms:e.externalDefTransforms.concat(t.externalDefTransforms),eventResizeJoinTransforms:e.eventResizeJoinTransforms.concat(t.eventResizeJoinTransforms),viewContainerModifiers:e.viewContainerModifiers.concat(t.viewContainerModifiers),eventDropTransformers:e.eventDropTransformers.concat(t.eventDropTransformers),calendarInteractions:e.calendarInteractions.concat(t.calendarInteractions),componentInteractions:e.componentInteractions.concat(t.componentInteractions),themeClasses:ki({},e.themeClasses,t.themeClasses),eventSourceDefs:e.eventSourceDefs.concat(t.eventSourceDefs),cmdFormatter:t.cmdFormatter||e.cmdFormatter,recurringTypes:e.recurringTypes.concat(t.recurringTypes),namedTimeZonedImpl:t.namedTimeZonedImpl||e.namedTimeZonedImpl,defaultView:e.defaultView||t.defaultView,elementDraggingImpl:e.elementDraggingImpl||t.elementDraggingImpl,optionChangeHandlers:ki({},e.optionChangeHandlers,t.optionChangeHandlers)}}function tr(e,t,n,r,i){e=e.toUpperCase();var o=null;"GET"===e?t=nr(t,n):o=rr(n);var a=new XMLHttpRequest;a.open(e,t,!0),"GET"!==e&&a.setRequestHeader("Content-Type","application/x-www-form-urlencoded"),a.onload=function(){if(a.status>=200&&a.status<400)try{var e=JSON.parse(a.responseText);r(e,a)}catch(e){i("Failure parsing JSON",a)}else i("Request failed",a)},a.onerror=function(){i("Request failed",a)},a.send(o)}function nr(e,t){return e+(-1===e.indexOf("?")?"?":"&")+rr(t)}function rr(e){var t=[];for(var n in e)t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return t.join("&")}function ir(e,t,n){var r,i,o,a,s=n.dateEnv,u={};return r=e.startParam,null==r&&(r=n.opt("startParam")),i=e.endParam,null==i&&(i=n.opt("endParam")),o=e.timeZoneParam,null==o&&(o=n.opt("timeZoneParam")),a="function"==typeof e.extraParams?e.extraParams():e.extraParams||{},ki(u,a),u[r]=s.formatIso(t.start),u[i]=s.formatIso(t.end),"local"!==s.timeZone&&(u[o]=s.timeZone),u}function or(e,t,n,r){for(var i=e?at(e):null,o=X(n.start),a=n.end,s=[];o0?e[0].code:"en",n=window.FullCalendarLocalesAll||[],r=window.FullCalendarLocales||{},i=n.concat(st(r),e),o={en:wo},a=0,s=i;a0;i--){var o=r.slice(0,i).join("-");if(t[o])return t[o]}return null}function hr(e,t,n){var r=rt([wo,n],["buttonText"]);delete r.code;var i=r.week;return delete r.week,{codeArg:e,codes:t,week:i,simpleNumberFormat:new Intl.NumberFormat(e),options:r}}function vr(e){return new Io[e]}function gr(e){var t=null,n=!1,r=Mo.exec(e);r&&(n=!r[1],n?e+="T00:00:00Z":e=e.replace(ko,function(e,n,r,i,o){return t=n?0:(60*parseInt(i,10)+parseInt(o||0,10))*("-"===r?-1:1),""})+"Z");var i=new Date(e);return ae(i)?{marker:i,isTimeUnspecified:n,timeZoneOffset:t}:null}function yr(e,t){return!t.pluginSystem.hooks.eventSourceDefs[e.sourceDefId].ignoreRange}function mr(e,t){for(var n=t.pluginSystem.hooks.eventSourceDefs,r=n.length-1;r>=0;r--){var i=n[r],o=i.parseMeta(e);if(o){var a=Er("object"==typeof e?e:{},o,r,t);return a._raw=Xe(e),a}}return null}function Er(e,t,n,r){var i={},o=Ge(e,_o,{},i),a={},s=In(i,r,a);return o.isFetching=!1,o.latestFetchId="",o.fetchRange=null,o.publicId=String(e.id||""),o.sourceId=String(Po++),o.sourceDefId=n,o.meta=t,o.ui=s,o.extendedProps=a,o}function Sr(e,t,n,r){switch(t.type){case"ADD_EVENT_SOURCES":return Dr(e,t.sources,n?n.activeRange:null,r);case"REMOVE_EVENT_SOURCE":return br(e,t.sourceId);case"PREV":case"NEXT":case"SET_DATE":case"SET_VIEW_TYPE":return n?Tr(e,n.activeRange,r):e;case"FETCH_EVENT_SOURCES":case"CHANGE_TIMEZONE":return Rr(e,t.sourceIds?at(t.sourceIds):Mr(e,r),n?n.activeRange:null,r);case"RECEIVE_EVENTS":case"RECEIVE_EVENT_ERROR":return Cr(e,t.sourceId,t.fetchId,t.fetchRange);case"REMOVE_ALL_EVENT_SOURCES":return{};default:return e}}function Dr(e,t,n,r){for(var i={},o=0,a=t;oe.fetchRange.end:!e.latestFetchId}function Rr(e,t,n,r){var i={};for(var o in e){var a=e[o];t[o]?i[o]=Ir(a,n,r):i[o]=a}return i}function Ir(e,t,n){var r=n.pluginSystem.hooks.eventSourceDefs[e.sourceDefId],i=String(Ho++);return r.fetch({eventSource:e,calendar:n,range:t},function(r){var o,a,s=r.rawEvents,u=n.opt("eventSourceSuccess");e.success&&(a=e.success(s,r.xhr)),u&&(o=u(s,r.xhr)),s=a||o||s,n.dispatch({type:"RECEIVE_EVENTS",sourceId:e.sourceId,fetchId:i,fetchRange:t,rawEvents:s})},function(r){var o=n.opt("eventSourceFailure");console.warn(r.message,r),e.failure&&e.failure(r),o&&o(r),n.dispatch({type:"RECEIVE_EVENT_ERROR",sourceId:e.sourceId,fetchId:i,fetchRange:t,error:r})}),ki({},e,{isFetching:!0,latestFetchId:i})}function Cr(e,t,n,r){var i,o=e[t];return o&&n===o.latestFetchId?ki({},e,(i={},i[t]=ki({},o,{isFetching:!1,fetchRange:r}),i)):e}function Mr(e,t){return it(e,function(e){return yr(e,t)})}function kr(e,t){return bt(e.activeRange,t.activeRange)&&bt(e.validRange,t.validRange)&&fe(e.minTime,t.minTime)&&fe(e.maxTime,t.maxTime)}function Or(e,t,n){for(var r=_r(e.viewType,t),i=Pr(e.dateProfile,t,e.currentDate,r,n),o=Sr(e.eventSources,t,i,n),a=ki({},e,{viewType:r,dateProfile:i,currentDate:Hr(e.currentDate,t,i),eventSources:o,eventStore:rn(e.eventStore,t,o,i,n),dateSelection:xr(e.dateSelection,t,n),eventSelection:Nr(e.eventSelection,t),eventDrag:zr(e.eventDrag,t,o,n),eventResize:Ur(e.eventResize,t,o,n),eventSourceLoadingLevel:Lr(o),loadingLevel:Lr(o)}),s=0,u=n.pluginSystem.hooks.reducers;s1?{year:"numeric",month:"short",day:"numeric"}:{year:"numeric",month:"long",day:"numeric"}}function ni(e){return e.map(function(e){return new e})}function ri(e,t){return{component:e,el:t.el,useEventCenter:null==t.useEventCenter||t.useEventCenter}}function ii(e){var t;return t={},t[e.component.uid]=e,t}function oi(e,t,n,r,i,o,a){return new Oo({calendarSystem:"gregory",timeZone:t,namedTimeZoneImpl:n,locale:e,weekNumberCalculation:i,firstDay:r,weekLabel:o,cmdFormatter:a})}function ai(e){return new(this.pluginSystem.hooks.themeClasses[e.themeSystem]||Wo)(e)}function si(e){var t=this.tryRerender.bind(this);return null!=e&&(t=qe(t,e)),t}function ui(e){return ot(e,function(e){return e.ui})}function li(e,t,n){var r={"":t};for(var i in e){var o=e[i];o.sourceId&&n[o.sourceId]&&(r[i]=n[o.sourceId])}return r}function ci(e){var t=e.eventRange.def,n=e.eventRange.instance.range,r=n.start?n.start.valueOf():0,i=n.end?n.end.valueOf():0;return ki({},t.extendedProps,t,{id:t.publicId,start:r,end:i,duration:i-r,allDay:Number(t.allDay),_seg:e})}function di(e,t){void 0===t&&(t={});var n=pi(t),r=Vt(t),i=n.createMarkerMeta(e);return i?n.format(i.marker,r,{forcedTzo:i.forcedTzo}):""}function fi(e,t,n){var r=pi("object"==typeof n&&n?n:{}),i=Vt(n,So.defaultRangeSeparator),o=r.createMarkerMeta(e),a=r.createMarkerMeta(t);return o&&a?r.formatRange(o.marker,a.marker,i,{forcedStartTzo:o.forcedTzo,forcedEndTzo:a.forcedTzo,isEndExclusive:n.isEndExclusive}):""}function pi(e){var t=dr(e.locale||"en",cr([]).map);return e=ki({timeZone:So.timeZone,calendarSystem:"gregory"},e,{locale:t}),new Oo(e)}function hi(e){var t={},n=Ge(e,Jo,Ko,t);return n.leftoverProps=t,n}function vi(e,t){return!e||t>10?{weekday:"short"}:t>1?{weekday:"short",month:"numeric",day:"numeric",omitCommas:!0}:{weekday:"long"}}function gi(e,t,n,r,i,o,a,s){var u,l=o.view,c=o.dateEnv,d=o.theme,f=o.options,p=Rt(t.activeRange,e),h=["fc-day-header",d.getClass("widgetHeader")];return u="function"==typeof f.columnHeaderHtml?f.columnHeaderHtml(c.toDate(e)):bn("function"==typeof f.columnHeaderText?f.columnHeaderText(c.toDate(e)):c.format(e,i)),n?h=h.concat(Gn(e,t,o,!0)):h.push("fc-"+Ri[e.getUTCDay()]),'1?' colspan="'+a+'"':"")+(s?" "+s:"")+">"+(p?Yn(l,{date:e,forceOff:!n||1===r},u):u)+""}function yi(e,t){var n=e.activeRange;return t?n:{start:V(n.start,e.minTime.milliseconds),end:V(n.end,e.maxTime.milliseconds-864e5)}}var mi={className:!0,colSpan:!0,rowSpan:!0},Ei={"1)||"numeric"!==i.year&&"2-digit"!==i.year||"numeric"!==i.month&&"2-digit"!==i.month||"numeric"!==i.day&&"2-digit"!==i.day||(s=1);var u=this.format(e,n),l=this.format(t,n);if(u===l)return u;var c=Lt(i,s),d=_t(c,o,n),f=d(e),p=d(t),h=At(u,f,l,p),v=o.separator||"";return h?h.before+f+v+p+h.after:u+v+l},e.prototype.getLargestUnit=function(){switch(this.severity){case 7:case 6:case 5:return"year";case 4:return"month";case 3:return"week";default:return"day"}},e}(),Li=function(){function e(e,t){this.cmdStr=e,this.separator=t}return e.prototype.format=function(e,t){return t.cmdFormatter(this.cmdStr,Zt(e,null,t,this.separator))},e.prototype.formatRange=function(e,t,n){return n.cmdFormatter(this.cmdStr,Zt(e,t,n,this.separator))},e}(),Ai=function(){function e(e){this.func=e}return e.prototype.format=function(e,t){return this.func(Zt(e,null,t))},e.prototype.formatRange=function(e,t,n){return this.func(Zt(e,t,n))},e}(),Vi=function(){function e(e,t){this.calendar=e,this.internalEventSource=t}return e.prototype.remove=function(){this.calendar.dispatch({type:"REMOVE_EVENT_SOURCE",sourceId:this.internalEventSource.sourceId})},e.prototype.refetch=function(){this.calendar.dispatch({type:"FETCH_EVENT_SOURCES",sourceIds:[this.internalEventSource.sourceId]})},Object.defineProperty(e.prototype,"id",{get:function(){return this.internalEventSource.publicId},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"url",{get:function(){return this.internalEventSource.meta.url},enumerable:!0,configurable:!0}),e}(),Bi=function(){function e(e,t,n){this._calendar=e,this._def=t,this._instance=n||null}return e.prototype.setProp=function(e,t){var n,r;if(e in ji);else if(e in Zi)"function"==typeof Zi[e]&&(t=Zi[e](t)),this.mutate({standardProps:(n={},n[e]=t,n)});else if(e in Fi){var i=void 0;"function"==typeof Fi[e]&&(t=Fi[e](t)),"color"===e?i={backgroundColor:t,borderColor:t}:"editable"===e?i={startEditable:t,durationEditable:t}:(r={},r[e]=t,i=r),this.mutate({standardProps:{ui:i}})}},e.prototype.setExtendedProp=function(e,t){var n;this.mutate({extendedProps:(n={},n[e]=t,n)})},e.prototype.setStart=function(e,t){void 0===t&&(t={});var n=this._calendar.dateEnv,r=n.createMarker(e);if(r&&this._instance){var i=this._instance.range,o=$e(i.start,r,n,t.granularity),a=null;if(t.maintainDuration){a=ve($e(i.start,i.end,n,t.granularity),$e(r,i.end,n,t.granularity))}this.mutate({startDelta:o,endDelta:a})}},e.prototype.setEnd=function(e,t){void 0===t&&(t={});var n,r=this._calendar.dateEnv;if((null==e||(n=r.createMarker(e)))&&this._instance)if(n){var i=$e(this._instance.range.end,n,r,t.granularity);this.mutate({endDelta:i})}else this.mutate({standardProps:{hasEnd:!1}})},e.prototype.setDates=function(e,t,n){void 0===n&&(n={});var r,i=this._calendar.dateEnv,o={allDay:n.allDay},a=i.createMarker(e);if(a&&(null==t||(r=i.createMarker(t)))&&this._instance){var s=this._instance.range;!0===n.allDay&&(s=Je(s));var u=$e(s.start,a,i,n.granularity);if(r){var l=$e(s.end,r,i,n.granularity);this.mutate({startDelta:u,endDelta:l,standardProps:o})}else o.hasEnd=!1,this.mutate({startDelta:u,standardProps:o})}},e.prototype.moveStart=function(e){var t=ue(e);t&&this.mutate({startDelta:t})},e.prototype.moveEnd=function(e){var t=ue(e);t&&this.mutate({endDelta:t})},e.prototype.moveDates=function(e){var t=ue(e);t&&this.mutate({startDelta:t,endDelta:t})},e.prototype.setAllDay=function(e,t){void 0===t&&(t={});var n={allDay:e},r=t.maintainDuration;null==r&&(r=this._calendar.opt("allDayMaintainDuration")),this._def.allDay!==e&&(n.hasEnd=r),this.mutate({standardProps:n})},e.prototype.formatRange=function(e){var t=this._calendar.dateEnv,n=this._instance,r=Vt(e,this._calendar.opt("defaultRangeSeparator"));return this._def.hasEnd?t.formatRange(n.range.start,n.range.end,r,{forcedStartTzo:n.forcedStartTzo,forcedEndTzo:n.forcedEndTzo}):t.format(n.range.start,r,{forcedTzo:n.forcedStartTzo})},e.prototype.mutate=function(e){var t=this._def,n=this._instance;if(n){this._calendar.dispatch({type:"MUTATE_EVENTS",instanceId:n.instanceId,mutation:e,fromApi:!0});var r=this._calendar.state.eventStore;this._def=r.defs[t.defId],this._instance=r.instances[n.instanceId]}},e.prototype.remove=function(){this._calendar.dispatch({type:"REMOVE_EVENT_DEF",defId:this._def.defId})},Object.defineProperty(e.prototype,"source",{get:function(){var e=this._def.sourceId;return e?new Vi(this._calendar,this._calendar.state.eventSources[e]):null},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"start",{get:function(){return this._instance?this._calendar.dateEnv.toDate(this._instance.range.start):null},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"end",{get:function(){return this._instance&&this._def.hasEnd?this._calendar.dateEnv.toDate(this._instance.range.end):null},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"id",{get:function(){return this._def.publicId},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"groupId",{get:function(){return this._def.groupId},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"allDay",{get:function(){return this._def.allDay},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"title",{get:function(){return this._def.title},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"url",{get:function(){return this._def.url},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"rendering",{get:function(){return this._def.rendering},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"startEditable",{get:function(){return this._def.ui.startEditable},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"durationEditable",{get:function(){return this._def.ui.durationEditable},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"constraint",{get:function(){return this._def.ui.constraints[0]||null},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"overlap",{get:function(){return this._def.ui.overlap},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"allow",{get:function(){return this._def.ui.allows[0]||null},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"backgroundColor",{get:function(){return this._def.ui.backgroundColor},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"borderColor",{get:function(){return this._def.ui.borderColor},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"textColor",{get:function(){return this._def.ui.textColor},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"classNames",{get:function(){return this._def.ui.classNames},enumerable:!0,configurable:!0}),Object.defineProperty(e.prototype,"extendedProps",{get:function(){return this._def.extendedProps},enumerable:!0,configurable:!0}),e}(),Fi={editable:Boolean,startEditable:Boolean,durationEditable:Boolean,constraint:null,overlap:null,allow:null,className:Rn,classNames:Rn,color:String,backgroundColor:String,borderColor:String,textColor:String},Wi={startEditable:null,durationEditable:null,constraints:[],overlap:null,allows:[],backgroundColor:"",borderColor:"",textColor:"",classNames:[]},Zi={id:String,groupId:String,title:String,url:String,rendering:String,extendedProps:null},ji={start:null,date:null,end:null,allDay:null},Yi=0,qi={startTime:"09:00",endTime:"17:00",daysOfWeek:[1,2,3,4,5],rendering:"inverse-background",classNames:"fc-nonbusiness",groupId:"_businessHours"},Gi=vt(),Xi=function(){function e(){this.getKeysForEventDefs=kt(this._getKeysForEventDefs),this.splitDateSelection=kt(this._splitDateSpan),this.splitEventStore=kt(this._splitEventStore),this.splitIndividualUi=kt(this._splitIndividualUi),this.splitEventDrag=kt(this._splitInteraction),this.splitEventResize=kt(this._splitInteraction),this.eventUiBuilders={}}return e.prototype.splitProps=function(e){var t=this,n=this.getKeyInfo(e),r=this.getKeysForEventDefs(e.eventStore),i=this.splitDateSelection(e.dateSelection),o=this.splitIndividualUi(e.eventUiBases,r),a=this.splitEventStore(e.eventStore,r),s=this.splitEventDrag(e.eventDrag),u=this.splitEventResize(e.eventResize),l={};this.eventUiBuilders=ot(n,function(e,n){return t.eventUiBuilders[n]||kt(jn)});for(var c in n){var d=n[c],f=a[c]||Gi,p=this.eventUiBuilders[c];l[c]={businessHours:d.businessHours||e.businessHours,dateSelection:i[c]||null,eventStore:f,eventUiBases:p(e.eventUiBases[""],d.ui,o[c]),eventSelection:f.instances[e.eventSelection]?e.eventSelection:"",eventDrag:s[c]||null,eventResize:u[c]||null}}return l},e.prototype._splitDateSpan=function(e){var t={};if(e)for(var n=this.getKeysForDateSpan(e),r=0,i=n;r=n[t]&&e=n[t]&&e0},e.prototype.canScrollHorizontally=function(){return this.getMaxScrollLeft()>0},e.prototype.canScrollUp=function(){return this.getScrollTop()>0},e.prototype.canScrollDown=function(){return this.getScrollTop()0},e.prototype.canScrollRight=function(){return this.getScrollLeft()1&&(o=X(o),o=A(o,-1),o=r.add(o,n))),{start:i,end:o}},e.prototype.buildRangeFromDuration=function(e,t,n,r){function i(){s=c.startOf(e,d),u=c.add(s,n),l={start:s,end:u}}var o,a,s,u,l,c=this.dateEnv,d=this.options.dateAlignment;return d||(o=this.options.dateIncrement,o?(a=ue(o),d=be(a) ")),l=!1):((r=f[e])?(d=function(e){r.click&&r.click.call(E,e)},(v=s.getCustomButtonIconClass(r))||(v=s.getIconClass(e))||(g=r.text)):(a=c[e])?(i.viewsWithButtons.push(e),d=function(){u.changeView(e)},(g=a.buttonTextOverride)||(v=s.getIconClass(e))||(g=a.buttonTextDefault)):u[e]&&(d=function(){u[e]()},(g=p[e])||(v=s.getIconClass(e))||(g=h[e])),d&&(m=["fc-"+e+"-button",s.getClass("button")],g?(y=bn(g),S=""):v&&(y="",S=' aria-label="'+e+'"'),E=n('"),E.addEventListener("click",d),o.push(E)))}),o.length>1){r=document.createElement("div");var v=s.getClass("buttonGroup");l&&v&&r.classList.add(v),a(r,o),d.appendChild(r)}else a(d,o)}),d},r.prototype.updateToday=function(e){this.toggleButtonEnabled("today",e)},r.prototype.updatePrev=function(e){this.toggleButtonEnabled("prev",e)},r.prototype.updateNext=function(e){this.toggleButtonEnabled("next",e)},r.prototype.updateTitle=function(e){p(this.el,"h2").forEach(function(t){t.innerText=e})},r.prototype.updateActiveButton=function(e){var t=this.theme.getClass("buttonActive");p(this.el,"button").forEach(function(n){e&&n.classList.contains("fc-"+e+"-button")?n.classList.add(t):n.classList.remove(t)})},r.prototype.toggleButtonEnabled=function(e,t){p(this.el,".fc-"+e+"-button").forEach(function(e){e.disabled=!t})},r}(oo),Lo=function(e){function n(n,r){var i=e.call(this,n)||this;i._renderToolbars=An(i.renderToolbars),i.buildViewPropTransformers=kt(ni),i.el=r,s(r,i.contentEl=t("div",{className:"fc-view-container"}));for(var o=i.calendar,a=0,u=o.pluginSystem.hooks.viewContainerModifiers;a"},e.prototype.detachSegs=function(e,t){var n=this.containerElsByType[e];n&&(n.forEach(c),delete this.containerElsByType[e])},e.prototype.computeSizes=function(e){for(var t in this.segsByType)(e||this.dirtySizeFlags[t])&&this.computeSegSizes(this.segsByType[t])},e.prototype.assignSizes=function(e){for(var t in this.segsByType)(e||this.dirtySizeFlags[t])&&this.assignSegSizes(this.segsByType[t]);this.dirtySizeFlags={}},e.prototype.computeSegSizes=function(e){},e.prototype.assignSegSizes=function(e){},e}(),Go=function(){function e(e){this.timeZoneName=e}return e}(),Xo=function(){function e(e){this.emitter=new Ki}return e.prototype.destroy=function(){},e.prototype.setMirrorIsVisible=function(e){},e.prototype.setMirrorNeedsRevert=function(e){},e.prototype.setAutoScrollEnabled=function(e){},e}(),Jo={startTime:ue,duration:ue,create:Boolean,sourceId:String},Ko={create:!0},Qo=function(e){function t(t,r){var i=e.call(this,t)||this;return r.innerHTML="",r.appendChild(i.el=n('
')),i.thead=i.el.querySelector("thead"),i}return et(t,e),t.prototype.destroy=function(){c(this.el)},t.prototype.render=function(e){var t=e.dates,n=e.datesRepDistinctDays,r=[];e.renderIntroHtml&&r.push(e.renderIntroHtml());for(var i=Vt(this.opt("columnHeaderFormat")||vi(n,t.length)),o=0,a=t;o"+r.join("")+""},t}(oo),$o=function(){function e(e,t){for(var n=e.start,r=e.end,i=[],o=[],a=-1;n=t.length?t[t.length-1]+1:t[n]},e}(),ea=function(){function e(e,t){var n,r,i,o=e.dates;if(t){for(r=o[0].getUTCDay(),n=1;n * { + /* work around the way we do column resizing and ensure a minimum width */ + display: inline-block; + min-width: 1.25em; } diff --git a/library/fullcalendar/packages/daygrid/main.js b/library/fullcalendar/packages/daygrid/main.js new file mode 100644 index 000000000..e41463eb9 --- /dev/null +++ b/library/fullcalendar/packages/daygrid/main.js @@ -0,0 +1,1630 @@ +/*! +FullCalendar Day Grid Plugin v4.0.2 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core')) : + typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core'], factory) : + (global = global || self, factory(global.FullCalendarDayGrid = {}, global.FullCalendar)); +}(this, function (exports, core) { 'use strict'; + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. All rights reserved. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of the + License at http://www.apache.org/licenses/LICENSE-2.0 + + THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED + WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, + MERCHANTABLITY OR NON-INFRINGEMENT. + + See the Apache Version 2.0 License for specific language governing permissions + and limitations under the License. + ***************************************************************************** */ + /* global Reflect, Promise */ + + var extendStatics = function(d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + + function __extends(d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + } + + var __assign = function() { + __assign = Object.assign || function __assign(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); + }; + + var DayGridDateProfileGenerator = /** @class */ (function (_super) { + __extends(DayGridDateProfileGenerator, _super); + function DayGridDateProfileGenerator() { + return _super !== null && _super.apply(this, arguments) || this; + } + // Computes the date range that will be rendered. + DayGridDateProfileGenerator.prototype.buildRenderRange = function (currentRange, currentRangeUnit, isRangeAllDay) { + var dateEnv = this.dateEnv; + var renderRange = _super.prototype.buildRenderRange.call(this, currentRange, currentRangeUnit, isRangeAllDay); + var start = renderRange.start; + var end = renderRange.end; + var endOfWeek; + // year and month views should be aligned with weeks. this is already done for week + if (/^(year|month)$/.test(currentRangeUnit)) { + start = dateEnv.startOfWeek(start); + // make end-of-week if not already + endOfWeek = dateEnv.startOfWeek(end); + if (endOfWeek.valueOf() !== end.valueOf()) { + end = core.addWeeks(endOfWeek, 1); + } + } + // ensure 6 weeks + if (this.options.monthMode && + this.options.fixedWeekCount) { + var rowCnt = Math.ceil(// could be partial weeks due to hiddenDays + core.diffWeeks(start, end)); + end = core.addWeeks(end, 6 - rowCnt); + } + return { start: start, end: end }; + }; + return DayGridDateProfileGenerator; + }(core.DateProfileGenerator)); + + /* A rectangular panel that is absolutely positioned over other content + ------------------------------------------------------------------------------------------------------------------------ + Options: + - className (string) + - content (HTML string, element, or element array) + - parentEl + - top + - left + - right (the x coord of where the right edge should be. not a "CSS" right) + - autoHide (boolean) + - show (callback) + - hide (callback) + */ + var Popover = /** @class */ (function () { + function Popover(options) { + var _this = this; + this.isHidden = true; + this.margin = 10; // the space required between the popover and the edges of the scroll container + // Triggered when the user clicks *anywhere* in the document, for the autoHide feature + this.documentMousedown = function (ev) { + // only hide the popover if the click happened outside the popover + if (_this.el && !_this.el.contains(ev.target)) { + _this.hide(); + } + }; + this.options = options; + } + // Shows the popover on the specified position. Renders it if not already + Popover.prototype.show = function () { + if (this.isHidden) { + if (!this.el) { + this.render(); + } + this.el.style.display = ''; + this.position(); + this.isHidden = false; + this.trigger('show'); + } + }; + // Hides the popover, through CSS, but does not remove it from the DOM + Popover.prototype.hide = function () { + if (!this.isHidden) { + this.el.style.display = 'none'; + this.isHidden = true; + this.trigger('hide'); + } + }; + // Creates `this.el` and renders content inside of it + Popover.prototype.render = function () { + var _this = this; + var options = this.options; + var el = this.el = core.createElement('div', { + className: 'fc-popover ' + (options.className || ''), + style: { + top: '0', + left: '0' + } + }); + if (typeof options.content === 'function') { + options.content(el); + } + options.parentEl.appendChild(el); + // when a click happens on anything inside with a 'fc-close' className, hide the popover + core.listenBySelector(el, 'click', '.fc-close', function (ev) { + _this.hide(); + }); + if (options.autoHide) { + document.addEventListener('mousedown', this.documentMousedown); + } + }; + // Hides and unregisters any handlers + Popover.prototype.destroy = function () { + this.hide(); + if (this.el) { + core.removeElement(this.el); + this.el = null; + } + document.removeEventListener('mousedown', this.documentMousedown); + }; + // Positions the popover optimally, using the top/left/right options + Popover.prototype.position = function () { + var options = this.options; + var el = this.el; + var elDims = el.getBoundingClientRect(); // only used for width,height + var origin = core.computeRect(el.offsetParent); + var clippingRect = core.computeClippingRect(options.parentEl); + var top; // the "position" (not "offset") values for the popover + var left; // + // compute top and left + top = options.top || 0; + if (options.left !== undefined) { + left = options.left; + } + else if (options.right !== undefined) { + left = options.right - elDims.width; // derive the left value from the right value + } + else { + left = 0; + } + // constrain to the view port. if constrained by two edges, give precedence to top/left + top = Math.min(top, clippingRect.bottom - elDims.height - this.margin); + top = Math.max(top, clippingRect.top + this.margin); + left = Math.min(left, clippingRect.right - elDims.width - this.margin); + left = Math.max(left, clippingRect.left + this.margin); + core.applyStyle(el, { + top: top - origin.top, + left: left - origin.left + }); + }; + // Triggers a callback. Calls a function in the option hash of the same name. + // Arguments beyond the first `name` are forwarded on. + // TODO: better code reuse for this. Repeat code + // can kill this??? + Popover.prototype.trigger = function (name) { + if (this.options[name]) { + this.options[name].apply(this, Array.prototype.slice.call(arguments, 1)); + } + }; + return Popover; + }()); + + /* Event-rendering methods for the DayGrid class + ----------------------------------------------------------------------------------------------------------------------*/ + // "Simple" is bad a name. has nothing to do with SimpleDayGrid + var SimpleDayGridEventRenderer = /** @class */ (function (_super) { + __extends(SimpleDayGridEventRenderer, _super); + function SimpleDayGridEventRenderer() { + return _super !== null && _super.apply(this, arguments) || this; + } + // Builds the HTML to be used for the default element for an individual segment + SimpleDayGridEventRenderer.prototype.renderSegHtml = function (seg, mirrorInfo) { + var options = this.context.options; + var eventRange = seg.eventRange; + var eventDef = eventRange.def; + var eventUi = eventRange.ui; + var allDay = eventDef.allDay; + var isDraggable = eventUi.startEditable; + var isResizableFromStart = allDay && seg.isStart && eventUi.durationEditable && options.eventResizableFromStart; + var isResizableFromEnd = allDay && seg.isEnd && eventUi.durationEditable; + var classes = this.getSegClasses(seg, isDraggable, isResizableFromStart || isResizableFromEnd, mirrorInfo); + var skinCss = core.cssToStr(this.getSkinCss(eventUi)); + var timeHtml = ''; + var timeText; + var titleHtml; + classes.unshift('fc-day-grid-event', 'fc-h-event'); + // Only display a timed events time if it is the starting segment + if (seg.isStart) { + timeText = this.getTimeText(eventRange); + if (timeText) { + timeHtml = '' + core.htmlEscape(timeText) + ''; + } + } + titleHtml = + '' + + (core.htmlEscape(eventDef.title || '') || ' ') + // we always want one line of height + ''; + return '' + + '
' + + (options.dir === 'rtl' ? + titleHtml + ' ' + timeHtml : // put a natural space in between + timeHtml + ' ' + titleHtml // + ) + + '
' + + (isResizableFromStart ? + '
' : + '') + + (isResizableFromEnd ? + '
' : + '') + + '
'; + }; + // Computes a default event time formatting string if `eventTimeFormat` is not explicitly defined + SimpleDayGridEventRenderer.prototype.computeEventTimeFormat = function () { + return { + hour: 'numeric', + minute: '2-digit', + omitZeroMinute: true, + meridiem: 'narrow' + }; + }; + SimpleDayGridEventRenderer.prototype.computeDisplayEventEnd = function () { + return false; // TODO: somehow consider the originating DayGrid's column count + }; + return SimpleDayGridEventRenderer; + }(core.FgEventRenderer)); + + /* Event-rendering methods for the DayGrid class + ----------------------------------------------------------------------------------------------------------------------*/ + var DayGridEventRenderer = /** @class */ (function (_super) { + __extends(DayGridEventRenderer, _super); + function DayGridEventRenderer(dayGrid) { + var _this = _super.call(this, dayGrid.context) || this; + _this.dayGrid = dayGrid; + return _this; + } + // Renders the given foreground event segments onto the grid + DayGridEventRenderer.prototype.attachSegs = function (segs, mirrorInfo) { + var rowStructs = this.rowStructs = this.renderSegRows(segs); + // append to each row's content skeleton + this.dayGrid.rowEls.forEach(function (rowNode, i) { + rowNode.querySelector('.fc-content-skeleton > table').appendChild(rowStructs[i].tbodyEl); + }); + // removes the "more.." events popover + if (!mirrorInfo) { + this.dayGrid.removeSegPopover(); + } + }; + // Unrenders all currently rendered foreground event segments + DayGridEventRenderer.prototype.detachSegs = function () { + var rowStructs = this.rowStructs || []; + var rowStruct; + while ((rowStruct = rowStructs.pop())) { + core.removeElement(rowStruct.tbodyEl); + } + this.rowStructs = null; + }; + // Uses the given events array to generate elements that should be appended to each row's content skeleton. + // Returns an array of rowStruct objects (see the bottom of `renderSegRow`). + // PRECONDITION: each segment shoud already have a rendered and assigned `.el` + DayGridEventRenderer.prototype.renderSegRows = function (segs) { + var rowStructs = []; + var segRows; + var row; + segRows = this.groupSegRows(segs); // group into nested arrays + // iterate each row of segment groupings + for (row = 0; row < segRows.length; row++) { + rowStructs.push(this.renderSegRow(row, segRows[row])); + } + return rowStructs; + }; + // Given a row # and an array of segments all in the same row, render a element, a skeleton that contains + // the segments. Returns object with a bunch of internal data about how the render was calculated. + // NOTE: modifies rowSegs + DayGridEventRenderer.prototype.renderSegRow = function (row, rowSegs) { + var dayGrid = this.dayGrid; + var colCnt = dayGrid.colCnt, isRtl = dayGrid.isRtl; + var segLevels = this.buildSegLevels(rowSegs); // group into sub-arrays of levels + var levelCnt = Math.max(1, segLevels.length); // ensure at least one level + var tbody = document.createElement('tbody'); + var segMatrix = []; // lookup for which segments are rendered into which level+col cells + var cellMatrix = []; // lookup for all elements of the level+col matrix + var loneCellMatrix = []; // lookup for elements that only take up a single column + var i; + var levelSegs; + var col; + var tr; + var j; + var seg; + var td; + // populates empty cells from the current column (`col`) to `endCol` + function emptyCellsUntil(endCol) { + while (col < endCol) { + // try to grab a cell from the level above and extend its rowspan. otherwise, create a fresh cell + td = (loneCellMatrix[i - 1] || [])[col]; + if (td) { + td.rowSpan = (td.rowSpan || 1) + 1; + } + else { + td = document.createElement('td'); + tr.appendChild(td); + } + cellMatrix[i][col] = td; + loneCellMatrix[i][col] = td; + col++; + } + } + for (i = 0; i < levelCnt; i++) { // iterate through all levels + levelSegs = segLevels[i]; + col = 0; + tr = document.createElement('tr'); + segMatrix.push([]); + cellMatrix.push([]); + loneCellMatrix.push([]); + // levelCnt might be 1 even though there are no actual levels. protect against this. + // this single empty row is useful for styling. + if (levelSegs) { + for (j = 0; j < levelSegs.length; j++) { // iterate through segments in level + seg = levelSegs[j]; + var leftCol = isRtl ? (colCnt - 1 - seg.lastCol) : seg.firstCol; + var rightCol = isRtl ? (colCnt - 1 - seg.firstCol) : seg.lastCol; + emptyCellsUntil(leftCol); + // create a container that occupies or more columns. append the event element. + td = core.createElement('td', { className: 'fc-event-container' }, seg.el); + if (leftCol !== rightCol) { + td.colSpan = rightCol - leftCol + 1; + } + else { // a single-column segment + loneCellMatrix[i][col] = td; + } + while (col <= rightCol) { + cellMatrix[i][col] = td; + segMatrix[i][col] = seg; + col++; + } + tr.appendChild(td); + } + } + emptyCellsUntil(colCnt); // finish off the row + var introHtml = dayGrid.renderProps.renderIntroHtml(); + if (introHtml) { + if (dayGrid.isRtl) { + core.appendToElement(tr, introHtml); + } + else { + core.prependToElement(tr, introHtml); + } + } + tbody.appendChild(tr); + } + return { + row: row, + tbodyEl: tbody, + cellMatrix: cellMatrix, + segMatrix: segMatrix, + segLevels: segLevels, + segs: rowSegs + }; + }; + // Stacks a flat array of segments, which are all assumed to be in the same row, into subarrays of vertical levels. + // NOTE: modifies segs + DayGridEventRenderer.prototype.buildSegLevels = function (segs) { + var _a = this.dayGrid, isRtl = _a.isRtl, colCnt = _a.colCnt; + var levels = []; + var i; + var seg; + var j; + // Give preference to elements with certain criteria, so they have + // a chance to be closer to the top. + segs = this.sortEventSegs(segs); + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + // loop through levels, starting with the topmost, until the segment doesn't collide with other segments + for (j = 0; j < levels.length; j++) { + if (!isDaySegCollision(seg, levels[j])) { + break; + } + } + // `j` now holds the desired subrow index + seg.level = j; + seg.leftCol = isRtl ? (colCnt - 1 - seg.lastCol) : seg.firstCol; // for sorting only + seg.rightCol = isRtl ? (colCnt - 1 - seg.firstCol) : seg.lastCol // for sorting only + ; + (levels[j] || (levels[j] = [])).push(seg); + } + // order segments left-to-right. very important if calendar is RTL + for (j = 0; j < levels.length; j++) { + levels[j].sort(compareDaySegCols); + } + return levels; + }; + // Given a flat array of segments, return an array of sub-arrays, grouped by each segment's row + DayGridEventRenderer.prototype.groupSegRows = function (segs) { + var segRows = []; + var i; + for (i = 0; i < this.dayGrid.rowCnt; i++) { + segRows.push([]); + } + for (i = 0; i < segs.length; i++) { + segRows[segs[i].row].push(segs[i]); + } + return segRows; + }; + // Computes a default `displayEventEnd` value if one is not expliclty defined + DayGridEventRenderer.prototype.computeDisplayEventEnd = function () { + return this.dayGrid.colCnt === 1; // we'll likely have space if there's only one day + }; + return DayGridEventRenderer; + }(SimpleDayGridEventRenderer)); + // Computes whether two segments' columns collide. They are assumed to be in the same row. + function isDaySegCollision(seg, otherSegs) { + var i; + var otherSeg; + for (i = 0; i < otherSegs.length; i++) { + otherSeg = otherSegs[i]; + if (otherSeg.firstCol <= seg.lastCol && + otherSeg.lastCol >= seg.firstCol) { + return true; + } + } + return false; + } + // A cmp function for determining the leftmost event + function compareDaySegCols(a, b) { + return a.leftCol - b.leftCol; + } + + var DayGridMirrorRenderer = /** @class */ (function (_super) { + __extends(DayGridMirrorRenderer, _super); + function DayGridMirrorRenderer() { + return _super !== null && _super.apply(this, arguments) || this; + } + DayGridMirrorRenderer.prototype.attachSegs = function (segs, mirrorInfo) { + var sourceSeg = mirrorInfo.sourceSeg; + var rowStructs = this.rowStructs = this.renderSegRows(segs); + // inject each new event skeleton into each associated row + this.dayGrid.rowEls.forEach(function (rowNode, row) { + var skeletonEl = core.htmlToElement('
'); // will be absolutely positioned + var skeletonTopEl; + var skeletonTop; + // If there is an original segment, match the top position. Otherwise, put it at the row's top level + if (sourceSeg && sourceSeg.row === row) { + skeletonTopEl = sourceSeg.el; + } + else { + skeletonTopEl = rowNode.querySelector('.fc-content-skeleton tbody'); + if (!skeletonTopEl) { // when no events + skeletonTopEl = rowNode.querySelector('.fc-content-skeleton table'); + } + } + skeletonTop = skeletonTopEl.getBoundingClientRect().top - + rowNode.getBoundingClientRect().top; // the offsetParent origin + skeletonEl.style.top = skeletonTop + 'px'; + skeletonEl.querySelector('table').appendChild(rowStructs[row].tbodyEl); + rowNode.appendChild(skeletonEl); + }); + }; + return DayGridMirrorRenderer; + }(DayGridEventRenderer)); + + var DayGridFillRenderer = /** @class */ (function (_super) { + __extends(DayGridFillRenderer, _super); + function DayGridFillRenderer(dayGrid) { + var _this = _super.call(this, dayGrid.context) || this; + _this.fillSegTag = 'td'; // override the default tag name + _this.dayGrid = dayGrid; + return _this; + } + DayGridFillRenderer.prototype.renderSegs = function (type, segs) { + // don't render timed background events + if (type === 'bgEvent') { + segs = segs.filter(function (seg) { + return seg.eventRange.def.allDay; + }); + } + _super.prototype.renderSegs.call(this, type, segs); + }; + DayGridFillRenderer.prototype.attachSegs = function (type, segs) { + var els = []; + var i; + var seg; + var skeletonEl; + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + skeletonEl = this.renderFillRow(type, seg); + this.dayGrid.rowEls[seg.row].appendChild(skeletonEl); + els.push(skeletonEl); + } + return els; + }; + // Generates the HTML needed for one row of a fill. Requires the seg's el to be rendered. + DayGridFillRenderer.prototype.renderFillRow = function (type, seg) { + var dayGrid = this.dayGrid; + var colCnt = dayGrid.colCnt, isRtl = dayGrid.isRtl; + var leftCol = isRtl ? (colCnt - 1 - seg.lastCol) : seg.firstCol; + var rightCol = isRtl ? (colCnt - 1 - seg.firstCol) : seg.lastCol; + var startCol = leftCol; + var endCol = rightCol + 1; + var className; + var skeletonEl; + var trEl; + if (type === 'businessHours') { + className = 'bgevent'; + } + else { + className = type.toLowerCase(); + } + skeletonEl = core.htmlToElement('
' + + '
' + + '
'); + trEl = skeletonEl.getElementsByTagName('tr')[0]; + if (startCol > 0) { + core.appendToElement(trEl, + // will create (startCol + 1) td's + new Array(startCol + 1).join('')); + } + seg.el.colSpan = endCol - startCol; + trEl.appendChild(seg.el); + if (endCol < colCnt) { + core.appendToElement(trEl, + // will create (colCnt - endCol) td's + new Array(colCnt - endCol + 1).join('')); + } + var introHtml = dayGrid.renderProps.renderIntroHtml(); + if (introHtml) { + if (dayGrid.isRtl) { + core.appendToElement(trEl, introHtml); + } + else { + core.prependToElement(trEl, introHtml); + } + } + return skeletonEl; + }; + return DayGridFillRenderer; + }(core.FillRenderer)); + + var DayTile = /** @class */ (function (_super) { + __extends(DayTile, _super); + function DayTile(context, el) { + var _this = _super.call(this, context, el) || this; + var eventRenderer = _this.eventRenderer = new DayTileEventRenderer(_this); + var renderFrame = _this.renderFrame = core.memoizeRendering(_this._renderFrame); + _this.renderFgEvents = core.memoizeRendering(eventRenderer.renderSegs.bind(eventRenderer), eventRenderer.unrender.bind(eventRenderer), [renderFrame]); + _this.renderEventSelection = core.memoizeRendering(eventRenderer.selectByInstanceId.bind(eventRenderer), eventRenderer.unselectByInstanceId.bind(eventRenderer), [_this.renderFgEvents]); + _this.renderEventDrag = core.memoizeRendering(eventRenderer.hideByHash.bind(eventRenderer), eventRenderer.showByHash.bind(eventRenderer), [renderFrame]); + _this.renderEventResize = core.memoizeRendering(eventRenderer.hideByHash.bind(eventRenderer), eventRenderer.showByHash.bind(eventRenderer), [renderFrame]); + context.calendar.registerInteractiveComponent(_this, { + el: _this.el, + useEventCenter: false + }); + return _this; + } + DayTile.prototype.render = function (props) { + this.renderFrame(props.date); + this.renderFgEvents(props.fgSegs); + this.renderEventSelection(props.eventSelection); + this.renderEventDrag(props.eventDragInstances); + this.renderEventResize(props.eventResizeInstances); + }; + DayTile.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.renderFrame.unrender(); // should unrender everything else + this.calendar.unregisterInteractiveComponent(this); + }; + DayTile.prototype._renderFrame = function (date) { + var _a = this, theme = _a.theme, dateEnv = _a.dateEnv; + var title = dateEnv.format(date, core.createFormatter(this.opt('dayPopoverFormat')) // TODO: cache + ); + this.el.innerHTML = + '
' + + '' + + core.htmlEscape(title) + + '' + + '' + + '
' + + '
' + + '
' + + '
'; + this.segContainerEl = this.el.querySelector('.fc-event-container'); + }; + DayTile.prototype.queryHit = function (positionLeft, positionTop, elWidth, elHeight) { + var date = this.props.date; // HACK + if (positionLeft < elWidth && positionTop < elHeight) { + return { + component: this, + dateSpan: { + allDay: true, + range: { start: date, end: core.addDays(date, 1) } + }, + dayEl: this.el, + rect: { + left: 0, + top: 0, + right: elWidth, + bottom: elHeight + }, + layer: 1 + }; + } + }; + return DayTile; + }(core.DateComponent)); + var DayTileEventRenderer = /** @class */ (function (_super) { + __extends(DayTileEventRenderer, _super); + function DayTileEventRenderer(dayTile) { + var _this = _super.call(this, dayTile.context) || this; + _this.dayTile = dayTile; + return _this; + } + DayTileEventRenderer.prototype.attachSegs = function (segs) { + for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) { + var seg = segs_1[_i]; + this.dayTile.segContainerEl.appendChild(seg.el); + } + }; + DayTileEventRenderer.prototype.detachSegs = function (segs) { + for (var _i = 0, segs_2 = segs; _i < segs_2.length; _i++) { + var seg = segs_2[_i]; + core.removeElement(seg.el); + } + }; + return DayTileEventRenderer; + }(SimpleDayGridEventRenderer)); + + var DayBgRow = /** @class */ (function () { + function DayBgRow(context) { + this.context = context; + } + DayBgRow.prototype.renderHtml = function (props) { + var parts = []; + if (props.renderIntroHtml) { + parts.push(props.renderIntroHtml()); + } + for (var _i = 0, _a = props.cells; _i < _a.length; _i++) { + var cell = _a[_i]; + parts.push(renderCellHtml(cell.date, props.dateProfile, this.context, cell.htmlAttrs)); + } + if (!props.cells.length) { + parts.push(''); + } + if (this.context.options.dir === 'rtl') { + parts.reverse(); + } + return '' + parts.join('') + ''; + }; + return DayBgRow; + }()); + function renderCellHtml(date, dateProfile, context, otherAttrs) { + var dateEnv = context.dateEnv, theme = context.theme; + var isDateValid = core.rangeContainsMarker(dateProfile.activeRange, date); // TODO: called too frequently. cache somehow. + var classes = core.getDayClasses(date, dateProfile, context); + classes.unshift('fc-day', theme.getClass('widgetContent')); + return ''; + } + + var DAY_NUM_FORMAT = core.createFormatter({ day: 'numeric' }); + var WEEK_NUM_FORMAT = core.createFormatter({ week: 'numeric' }); + var DayGrid = /** @class */ (function (_super) { + __extends(DayGrid, _super); + function DayGrid(context, el, renderProps) { + var _this = _super.call(this, context, el) || this; + _this.bottomCoordPadding = 0; // hack for extending the hit area for the last row of the coordinate grid + _this.isCellSizesDirty = false; + var eventRenderer = _this.eventRenderer = new DayGridEventRenderer(_this); + var fillRenderer = _this.fillRenderer = new DayGridFillRenderer(_this); + _this.mirrorRenderer = new DayGridMirrorRenderer(_this); + var renderCells = _this.renderCells = core.memoizeRendering(_this._renderCells, _this._unrenderCells); + _this.renderBusinessHours = core.memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'businessHours'), fillRenderer.unrender.bind(fillRenderer, 'businessHours'), [renderCells]); + _this.renderDateSelection = core.memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'highlight'), fillRenderer.unrender.bind(fillRenderer, 'highlight'), [renderCells]); + _this.renderBgEvents = core.memoizeRendering(fillRenderer.renderSegs.bind(fillRenderer, 'bgEvent'), fillRenderer.unrender.bind(fillRenderer, 'bgEvent'), [renderCells]); + _this.renderFgEvents = core.memoizeRendering(eventRenderer.renderSegs.bind(eventRenderer), eventRenderer.unrender.bind(eventRenderer), [renderCells]); + _this.renderEventSelection = core.memoizeRendering(eventRenderer.selectByInstanceId.bind(eventRenderer), eventRenderer.unselectByInstanceId.bind(eventRenderer), [_this.renderFgEvents]); + _this.renderEventDrag = core.memoizeRendering(_this._renderEventDrag, _this._unrenderEventDrag, [renderCells]); + _this.renderEventResize = core.memoizeRendering(_this._renderEventResize, _this._unrenderEventResize, [renderCells]); + _this.renderProps = renderProps; + return _this; + } + DayGrid.prototype.render = function (props) { + var cells = props.cells; + this.rowCnt = cells.length; + this.colCnt = cells[0].length; + this.renderCells(cells, props.isRigid); + this.renderBusinessHours(props.businessHourSegs); + this.renderDateSelection(props.dateSelectionSegs); + this.renderBgEvents(props.bgEventSegs); + this.renderFgEvents(props.fgEventSegs); + this.renderEventSelection(props.eventSelection); + this.renderEventDrag(props.eventDrag); + this.renderEventResize(props.eventResize); + if (this.segPopoverTile) { + this.updateSegPopoverTile(); + } + }; + DayGrid.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.renderCells.unrender(); // will unrender everything else + }; + DayGrid.prototype.getCellRange = function (row, col) { + var start = this.props.cells[row][col].date; + var end = core.addDays(start, 1); + return { start: start, end: end }; + }; + DayGrid.prototype.updateSegPopoverTile = function (date, segs) { + var ownProps = this.props; + this.segPopoverTile.receiveProps({ + date: date || this.segPopoverTile.props.date, + fgSegs: segs || this.segPopoverTile.props.fgSegs, + eventSelection: ownProps.eventSelection, + eventDragInstances: ownProps.eventDrag ? ownProps.eventDrag.affectedInstances : null, + eventResizeInstances: ownProps.eventResize ? ownProps.eventResize.affectedInstances : null + }); + }; + /* Date Rendering + ------------------------------------------------------------------------------------------------------------------*/ + DayGrid.prototype._renderCells = function (cells, isRigid) { + var _a = this, view = _a.view, dateEnv = _a.dateEnv; + var _b = this, rowCnt = _b.rowCnt, colCnt = _b.colCnt; + var html = ''; + var row; + var col; + for (row = 0; row < rowCnt; row++) { + html += this.renderDayRowHtml(row, isRigid); + } + this.el.innerHTML = html; + this.rowEls = core.findElements(this.el, '.fc-row'); + this.cellEls = core.findElements(this.el, '.fc-day, .fc-disabled-day'); + if (this.isRtl) { + this.cellEls.reverse(); + } + this.rowPositions = new core.PositionCache(this.el, this.rowEls, false, true // vertical + ); + this.colPositions = new core.PositionCache(this.el, this.cellEls.slice(0, colCnt), // only the first row + true, false // horizontal + ); + // trigger dayRender with each cell's element + for (row = 0; row < rowCnt; row++) { + for (col = 0; col < colCnt; col++) { + this.publiclyTrigger('dayRender', [ + { + date: dateEnv.toDate(cells[row][col].date), + el: this.getCellEl(row, col), + view: view + } + ]); + } + } + this.isCellSizesDirty = true; + }; + DayGrid.prototype._unrenderCells = function () { + this.removeSegPopover(); + }; + // Generates the HTML for a single row, which is a div that wraps a table. + // `row` is the row number. + DayGrid.prototype.renderDayRowHtml = function (row, isRigid) { + var theme = this.theme; + var classes = ['fc-row', 'fc-week', theme.getClass('dayRow')]; + if (isRigid) { + classes.push('fc-rigid'); + } + var bgRow = new DayBgRow(this.context); + return '' + + '
' + + '
' + + '' + + bgRow.renderHtml({ + cells: this.props.cells[row], + dateProfile: this.props.dateProfile, + renderIntroHtml: this.renderProps.renderBgIntroHtml + }) + + '
' + + '
' + + '
' + + '' + + (this.getIsNumbersVisible() ? + '' + + this.renderNumberTrHtml(row) + + '' : + '') + + '
' + + '
' + + '
'; + }; + DayGrid.prototype.getIsNumbersVisible = function () { + return this.getIsDayNumbersVisible() || + this.renderProps.cellWeekNumbersVisible || + this.renderProps.colWeekNumbersVisible; + }; + DayGrid.prototype.getIsDayNumbersVisible = function () { + return this.rowCnt > 1; + }; + /* Grid Number Rendering + ------------------------------------------------------------------------------------------------------------------*/ + DayGrid.prototype.renderNumberTrHtml = function (row) { + var intro = this.renderProps.renderNumberIntroHtml(row, this); + return '' + + '' + + (this.isRtl ? '' : intro) + + this.renderNumberCellsHtml(row) + + (this.isRtl ? intro : '') + + ''; + }; + DayGrid.prototype.renderNumberCellsHtml = function (row) { + var htmls = []; + var col; + var date; + for (col = 0; col < this.colCnt; col++) { + date = this.props.cells[row][col].date; + htmls.push(this.renderNumberCellHtml(date)); + } + if (this.isRtl) { + htmls.reverse(); + } + return htmls.join(''); + }; + // Generates the HTML for the s of the "number" row in the DayGrid's content skeleton. + // The number row will only exist if either day numbers or week numbers are turned on. + DayGrid.prototype.renderNumberCellHtml = function (date) { + var _a = this, view = _a.view, dateEnv = _a.dateEnv; + var html = ''; + var isDateValid = core.rangeContainsMarker(this.props.dateProfile.activeRange, date); // TODO: called too frequently. cache somehow. + var isDayNumberVisible = this.getIsDayNumbersVisible() && isDateValid; + var classes; + var weekCalcFirstDow; + if (!isDayNumberVisible && !this.renderProps.cellWeekNumbersVisible) { + // no numbers in day cell (week number must be along the side) + return ''; // will create an empty space above events :( + } + classes = core.getDayClasses(date, this.props.dateProfile, this.context); + classes.unshift('fc-day-top'); + if (this.renderProps.cellWeekNumbersVisible) { + weekCalcFirstDow = dateEnv.weekDow; + } + html += ''; + if (this.renderProps.cellWeekNumbersVisible && (date.getUTCDay() === weekCalcFirstDow)) { + html += core.buildGotoAnchorHtml(view, { date: date, type: 'week' }, { 'class': 'fc-week-number' }, dateEnv.format(date, WEEK_NUM_FORMAT) // inner HTML + ); + } + if (isDayNumberVisible) { + html += core.buildGotoAnchorHtml(view, date, { 'class': 'fc-day-number' }, dateEnv.format(date, DAY_NUM_FORMAT) // inner HTML + ); + } + html += ''; + return html; + }; + /* Sizing + ------------------------------------------------------------------------------------------------------------------*/ + DayGrid.prototype.updateSize = function (isResize) { + var _a = this, fillRenderer = _a.fillRenderer, eventRenderer = _a.eventRenderer, mirrorRenderer = _a.mirrorRenderer; + if (isResize || this.isCellSizesDirty) { + this.buildColPositions(); + this.buildRowPositions(); + this.isCellSizesDirty = false; + } + fillRenderer.computeSizes(isResize); + eventRenderer.computeSizes(isResize); + mirrorRenderer.computeSizes(isResize); + fillRenderer.assignSizes(isResize); + eventRenderer.assignSizes(isResize); + mirrorRenderer.assignSizes(isResize); + }; + DayGrid.prototype.buildColPositions = function () { + this.colPositions.build(); + }; + DayGrid.prototype.buildRowPositions = function () { + this.rowPositions.build(); + this.rowPositions.bottoms[this.rowCnt - 1] += this.bottomCoordPadding; // hack + }; + /* Hit System + ------------------------------------------------------------------------------------------------------------------*/ + DayGrid.prototype.positionToHit = function (leftPosition, topPosition) { + var _a = this, colPositions = _a.colPositions, rowPositions = _a.rowPositions; + var col = colPositions.leftToIndex(leftPosition); + var row = rowPositions.topToIndex(topPosition); + if (row != null && col != null) { + return { + row: row, + col: col, + dateSpan: { + range: this.getCellRange(row, col), + allDay: true + }, + dayEl: this.getCellEl(row, col), + relativeRect: { + left: colPositions.lefts[col], + right: colPositions.rights[col], + top: rowPositions.tops[row], + bottom: rowPositions.bottoms[row] + } + }; + } + }; + /* Cell System + ------------------------------------------------------------------------------------------------------------------*/ + // FYI: the first column is the leftmost column, regardless of date + DayGrid.prototype.getCellEl = function (row, col) { + return this.cellEls[row * this.colCnt + col]; + }; + /* Event Drag Visualization + ------------------------------------------------------------------------------------------------------------------*/ + DayGrid.prototype._renderEventDrag = function (state) { + if (state) { + this.eventRenderer.hideByHash(state.affectedInstances); + this.fillRenderer.renderSegs('highlight', state.segs); + } + }; + DayGrid.prototype._unrenderEventDrag = function (state) { + if (state) { + this.eventRenderer.showByHash(state.affectedInstances); + this.fillRenderer.unrender('highlight'); + } + }; + /* Event Resize Visualization + ------------------------------------------------------------------------------------------------------------------*/ + DayGrid.prototype._renderEventResize = function (state) { + if (state) { + this.eventRenderer.hideByHash(state.affectedInstances); + this.fillRenderer.renderSegs('highlight', state.segs); + this.mirrorRenderer.renderSegs(state.segs, { isResizing: true, sourceSeg: state.sourceSeg }); + } + }; + DayGrid.prototype._unrenderEventResize = function (state) { + if (state) { + this.eventRenderer.showByHash(state.affectedInstances); + this.fillRenderer.unrender('highlight'); + this.mirrorRenderer.unrender(state.segs, { isResizing: true, sourceSeg: state.sourceSeg }); + } + }; + /* More+ Link Popover + ------------------------------------------------------------------------------------------------------------------*/ + DayGrid.prototype.removeSegPopover = function () { + if (this.segPopover) { + this.segPopover.hide(); // in handler, will call segPopover's removeElement + } + }; + // Limits the number of "levels" (vertically stacking layers of events) for each row of the grid. + // `levelLimit` can be false (don't limit), a number, or true (should be computed). + DayGrid.prototype.limitRows = function (levelLimit) { + var rowStructs = this.eventRenderer.rowStructs || []; + var row; // row # + var rowLevelLimit; + for (row = 0; row < rowStructs.length; row++) { + this.unlimitRow(row); + if (!levelLimit) { + rowLevelLimit = false; + } + else if (typeof levelLimit === 'number') { + rowLevelLimit = levelLimit; + } + else { + rowLevelLimit = this.computeRowLevelLimit(row); + } + if (rowLevelLimit !== false) { + this.limitRow(row, rowLevelLimit); + } + } + }; + // Computes the number of levels a row will accomodate without going outside its bounds. + // Assumes the row is "rigid" (maintains a constant height regardless of what is inside). + // `row` is the row number. + DayGrid.prototype.computeRowLevelLimit = function (row) { + var rowEl = this.rowEls[row]; // the containing "fake" row div + var rowBottom = rowEl.getBoundingClientRect().bottom; // relative to viewport! + var trEls = core.findChildren(this.eventRenderer.rowStructs[row].tbodyEl); + var i; + var trEl; + // Reveal one level at a time and stop when we find one out of bounds + for (i = 0; i < trEls.length; i++) { + trEl = trEls[i]; + trEl.classList.remove('fc-limited'); // reset to original state (reveal) + if (trEl.getBoundingClientRect().bottom > rowBottom) { + return i; + } + } + return false; // should not limit at all + }; + // Limits the given grid row to the maximum number of levels and injects "more" links if necessary. + // `row` is the row number. + // `levelLimit` is a number for the maximum (inclusive) number of levels allowed. + DayGrid.prototype.limitRow = function (row, levelLimit) { + var _this = this; + var _a = this, colCnt = _a.colCnt, isRtl = _a.isRtl; + var rowStruct = this.eventRenderer.rowStructs[row]; + var moreNodes = []; // array of "more" links and DOM nodes + var col = 0; // col #, left-to-right (not chronologically) + var levelSegs; // array of segment objects in the last allowable level, ordered left-to-right + var cellMatrix; // a matrix (by level, then column) of all elements in the row + var limitedNodes; // array of temporarily hidden level and segment DOM nodes + var i; + var seg; + var segsBelow; // array of segment objects below `seg` in the current `col` + var totalSegsBelow; // total number of segments below `seg` in any of the columns `seg` occupies + var colSegsBelow; // array of segment arrays, below seg, one for each column (offset from segs's first column) + var td; + var rowSpan; + var segMoreNodes; // array of "more" cells that will stand-in for the current seg's cell + var j; + var moreTd; + var moreWrap; + var moreLink; + // Iterates through empty level cells and places "more" links inside if need be + var emptyCellsUntil = function (endCol) { + while (col < endCol) { + segsBelow = _this.getCellSegs(row, col, levelLimit); + if (segsBelow.length) { + td = cellMatrix[levelLimit - 1][col]; + moreLink = _this.renderMoreLink(row, col, segsBelow); + moreWrap = core.createElement('div', null, moreLink); + td.appendChild(moreWrap); + moreNodes.push(moreWrap[0]); + } + col++; + } + }; + if (levelLimit && levelLimit < rowStruct.segLevels.length) { // is it actually over the limit? + levelSegs = rowStruct.segLevels[levelLimit - 1]; + cellMatrix = rowStruct.cellMatrix; + limitedNodes = core.findChildren(rowStruct.tbodyEl).slice(levelLimit); // get level elements past the limit + limitedNodes.forEach(function (node) { + node.classList.add('fc-limited'); // hide elements and get a simple DOM-nodes array + }); + // iterate though segments in the last allowable level + for (i = 0; i < levelSegs.length; i++) { + seg = levelSegs[i]; + var leftCol = isRtl ? (colCnt - 1 - seg.lastCol) : seg.firstCol; + var rightCol = isRtl ? (colCnt - 1 - seg.firstCol) : seg.lastCol; + emptyCellsUntil(leftCol); // process empty cells before the segment + // determine *all* segments below `seg` that occupy the same columns + colSegsBelow = []; + totalSegsBelow = 0; + while (col <= rightCol) { + segsBelow = this.getCellSegs(row, col, levelLimit); + colSegsBelow.push(segsBelow); + totalSegsBelow += segsBelow.length; + col++; + } + if (totalSegsBelow) { // do we need to replace this segment with one or many "more" links? + td = cellMatrix[levelLimit - 1][leftCol]; // the segment's parent cell + rowSpan = td.rowSpan || 1; + segMoreNodes = []; + // make a replacement for each column the segment occupies. will be one for each colspan + for (j = 0; j < colSegsBelow.length; j++) { + moreTd = core.createElement('td', { className: 'fc-more-cell', rowSpan: rowSpan }); + segsBelow = colSegsBelow[j]; + moreLink = this.renderMoreLink(row, leftCol + j, [seg].concat(segsBelow) // count seg as hidden too + ); + moreWrap = core.createElement('div', null, moreLink); + moreTd.appendChild(moreWrap); + segMoreNodes.push(moreTd); + moreNodes.push(moreTd); + } + td.classList.add('fc-limited'); + core.insertAfterElement(td, segMoreNodes); + limitedNodes.push(td); + } + } + emptyCellsUntil(this.colCnt); // finish off the level + rowStruct.moreEls = moreNodes; // for easy undoing later + rowStruct.limitedEls = limitedNodes; // for easy undoing later + } + }; + // Reveals all levels and removes all "more"-related elements for a grid's row. + // `row` is a row number. + DayGrid.prototype.unlimitRow = function (row) { + var rowStruct = this.eventRenderer.rowStructs[row]; + if (rowStruct.moreEls) { + rowStruct.moreEls.forEach(core.removeElement); + rowStruct.moreEls = null; + } + if (rowStruct.limitedEls) { + rowStruct.limitedEls.forEach(function (limitedEl) { + limitedEl.classList.remove('fc-limited'); + }); + rowStruct.limitedEls = null; + } + }; + // Renders an element that represents hidden event element for a cell. + // Responsible for attaching click handler as well. + DayGrid.prototype.renderMoreLink = function (row, col, hiddenSegs) { + var _this = this; + var _a = this, view = _a.view, dateEnv = _a.dateEnv; + var a = core.createElement('a', { className: 'fc-more' }); + a.innerText = this.getMoreLinkText(hiddenSegs.length); + a.addEventListener('click', function (ev) { + var clickOption = _this.opt('eventLimitClick'); + var _col = _this.isRtl ? _this.colCnt - col - 1 : col; // HACK: props.cells has different dir system? + var date = _this.props.cells[row][_col].date; + var moreEl = ev.currentTarget; + var dayEl = _this.getCellEl(row, col); + var allSegs = _this.getCellSegs(row, col); + // rescope the segments to be within the cell's date + var reslicedAllSegs = _this.resliceDaySegs(allSegs, date); + var reslicedHiddenSegs = _this.resliceDaySegs(hiddenSegs, date); + if (typeof clickOption === 'function') { + // the returned value can be an atomic option + clickOption = _this.publiclyTrigger('eventLimitClick', [ + { + date: dateEnv.toDate(date), + allDay: true, + dayEl: dayEl, + moreEl: moreEl, + segs: reslicedAllSegs, + hiddenSegs: reslicedHiddenSegs, + jsEvent: ev, + view: view + } + ]); + } + if (clickOption === 'popover') { + _this.showSegPopover(row, col, moreEl, reslicedAllSegs); + } + else if (typeof clickOption === 'string') { // a view name + view.calendar.zoomTo(date, clickOption); + } + }); + return a; + }; + // Reveals the popover that displays all events within a cell + DayGrid.prototype.showSegPopover = function (row, col, moreLink, segs) { + var _this = this; + var _a = this, calendar = _a.calendar, view = _a.view, theme = _a.theme; + var _col = this.isRtl ? this.colCnt - col - 1 : col; // HACK: props.cells has different dir system? + var moreWrap = moreLink.parentNode; // the
wrapper around the + var topEl; // the element we want to match the top coordinate of + var options; + if (this.rowCnt === 1) { + topEl = view.el; // will cause the popover to cover any sort of header + } + else { + topEl = this.rowEls[row]; // will align with top of row + } + options = { + className: 'fc-more-popover ' + theme.getClass('popover'), + parentEl: view.el, + top: core.computeRect(topEl).top, + autoHide: true, + content: function (el) { + _this.segPopoverTile = new DayTile(_this.context, el); + _this.updateSegPopoverTile(_this.props.cells[row][_col].date, segs); + }, + hide: function () { + _this.segPopoverTile.destroy(); + _this.segPopoverTile = null; + _this.segPopover.destroy(); + _this.segPopover = null; + } + }; + // Determine horizontal coordinate. + // We use the moreWrap instead of the to avoid border confusion. + if (this.isRtl) { + options.right = core.computeRect(moreWrap).right + 1; // +1 to be over cell border + } + else { + options.left = core.computeRect(moreWrap).left - 1; // -1 to be over cell border + } + this.segPopover = new Popover(options); + this.segPopover.show(); + calendar.releaseAfterSizingTriggers(); // hack for eventPositioned + }; + // Given the events within an array of segment objects, reslice them to be in a single day + DayGrid.prototype.resliceDaySegs = function (segs, dayDate) { + var dayStart = dayDate; + var dayEnd = core.addDays(dayStart, 1); + var dayRange = { start: dayStart, end: dayEnd }; + var newSegs = []; + for (var _i = 0, segs_1 = segs; _i < segs_1.length; _i++) { + var seg = segs_1[_i]; + var eventRange = seg.eventRange; + var origRange = eventRange.range; + var slicedRange = core.intersectRanges(origRange, dayRange); + if (slicedRange) { + newSegs.push(__assign({}, seg, { eventRange: { + def: eventRange.def, + ui: __assign({}, eventRange.ui, { durationEditable: false }), + instance: eventRange.instance, + range: slicedRange + }, isStart: seg.isStart && slicedRange.start.valueOf() === origRange.start.valueOf(), isEnd: seg.isEnd && slicedRange.end.valueOf() === origRange.end.valueOf() })); + } + } + return newSegs; + }; + // Generates the text that should be inside a "more" link, given the number of events it represents + DayGrid.prototype.getMoreLinkText = function (num) { + var opt = this.opt('eventLimitText'); + if (typeof opt === 'function') { + return opt(num); + } + else { + return '+' + num + ' ' + opt; + } + }; + // Returns segments within a given cell. + // If `startLevel` is specified, returns only events including and below that level. Otherwise returns all segs. + DayGrid.prototype.getCellSegs = function (row, col, startLevel) { + var segMatrix = this.eventRenderer.rowStructs[row].segMatrix; + var level = startLevel || 0; + var segs = []; + var seg; + while (level < segMatrix.length) { + seg = segMatrix[level][col]; + if (seg) { + segs.push(seg); + } + level++; + } + return segs; + }; + return DayGrid; + }(core.DateComponent)); + + var WEEK_NUM_FORMAT$1 = core.createFormatter({ week: 'numeric' }); + /* An abstract class for the daygrid views, as well as month view. Renders one or more rows of day cells. + ----------------------------------------------------------------------------------------------------------------------*/ + // It is a manager for a DayGrid subcomponent, which does most of the heavy lifting. + // It is responsible for managing width/height. + var DayGridView = /** @class */ (function (_super) { + __extends(DayGridView, _super); + function DayGridView(context, viewSpec, dateProfileGenerator, parentEl) { + var _this = _super.call(this, context, viewSpec, dateProfileGenerator, parentEl) || this; + /* Header Rendering + ------------------------------------------------------------------------------------------------------------------*/ + // Generates the HTML that will go before the day-of week header cells + _this.renderHeadIntroHtml = function () { + var theme = _this.theme; + if (_this.colWeekNumbersVisible) { + return '' + + '' + + '' + // needed for matchCellWidths + core.htmlEscape(_this.opt('weekLabel')) + + '' + + ''; + } + return ''; + }; + /* Day Grid Rendering + ------------------------------------------------------------------------------------------------------------------*/ + // Generates the HTML that will go before content-skeleton cells that display the day/week numbers + _this.renderDayGridNumberIntroHtml = function (row, dayGrid) { + var dateEnv = _this.dateEnv; + var weekStart = dayGrid.props.cells[row][0].date; + if (_this.colWeekNumbersVisible) { + return '' + + '' + + core.buildGotoAnchorHtml(// aside from link, important for matchCellWidths + _this, { date: weekStart, type: 'week', forceOff: dayGrid.colCnt === 1 }, dateEnv.format(weekStart, WEEK_NUM_FORMAT$1) // inner HTML + ) + + ''; + } + return ''; + }; + // Generates the HTML that goes before the day bg cells for each day-row + _this.renderDayGridBgIntroHtml = function () { + var theme = _this.theme; + if (_this.colWeekNumbersVisible) { + return ''; + } + return ''; + }; + // Generates the HTML that goes before every other type of row generated by DayGrid. + // Affects mirror-skeleton and highlight-skeleton rows. + _this.renderDayGridIntroHtml = function () { + if (_this.colWeekNumbersVisible) { + return ''; + } + return ''; + }; + _this.el.classList.add('fc-dayGrid-view'); + _this.el.innerHTML = _this.renderSkeletonHtml(); + _this.scroller = new core.ScrollComponent('hidden', // overflow x + 'auto' // overflow y + ); + var dayGridContainerEl = _this.scroller.el; + _this.el.querySelector('.fc-body > tr > td').appendChild(dayGridContainerEl); + dayGridContainerEl.classList.add('fc-day-grid-container'); + var dayGridEl = core.createElement('div', { className: 'fc-day-grid' }); + dayGridContainerEl.appendChild(dayGridEl); + var cellWeekNumbersVisible; + if (_this.opt('weekNumbers')) { + if (_this.opt('weekNumbersWithinDays')) { + cellWeekNumbersVisible = true; + _this.colWeekNumbersVisible = false; + } + else { + cellWeekNumbersVisible = false; + _this.colWeekNumbersVisible = true; + } + } + else { + _this.colWeekNumbersVisible = false; + cellWeekNumbersVisible = false; + } + _this.dayGrid = new DayGrid(_this.context, dayGridEl, { + renderNumberIntroHtml: _this.renderDayGridNumberIntroHtml, + renderBgIntroHtml: _this.renderDayGridBgIntroHtml, + renderIntroHtml: _this.renderDayGridIntroHtml, + colWeekNumbersVisible: _this.colWeekNumbersVisible, + cellWeekNumbersVisible: cellWeekNumbersVisible + }); + return _this; + } + DayGridView.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.dayGrid.destroy(); + this.scroller.destroy(); + }; + // Builds the HTML skeleton for the view. + // The day-grid component will render inside of a container defined by this HTML. + DayGridView.prototype.renderSkeletonHtml = function () { + var theme = this.theme; + return '' + + '' + + (this.opt('columnHeader') ? + '' + + '' + + '' + + '' + + '' : + '') + + '' + + '' + + '' + + '' + + '' + + '
 
'; + }; + // Generates an HTML attribute string for setting the width of the week number column, if it is known + DayGridView.prototype.weekNumberStyleAttr = function () { + if (this.weekNumberWidth != null) { + return 'style="width:' + this.weekNumberWidth + 'px"'; + } + return ''; + }; + // Determines whether each row should have a constant height + DayGridView.prototype.hasRigidRows = function () { + var eventLimit = this.opt('eventLimit'); + return eventLimit && typeof eventLimit !== 'number'; + }; + /* Dimensions + ------------------------------------------------------------------------------------------------------------------*/ + DayGridView.prototype.updateSize = function (isResize, viewHeight, isAuto) { + _super.prototype.updateSize.call(this, isResize, viewHeight, isAuto); // will call updateBaseSize. important that executes first + this.dayGrid.updateSize(isResize); + }; + // Refreshes the horizontal dimensions of the view + DayGridView.prototype.updateBaseSize = function (isResize, viewHeight, isAuto) { + var dayGrid = this.dayGrid; + var eventLimit = this.opt('eventLimit'); + var headRowEl = this.header ? this.header.el : null; // HACK + var scrollerHeight; + var scrollbarWidths; + // hack to give the view some height prior to dayGrid's columns being rendered + // TODO: separate setting height from scroller VS dayGrid. + if (!dayGrid.rowEls) { + if (!isAuto) { + scrollerHeight = this.computeScrollerHeight(viewHeight); + this.scroller.setHeight(scrollerHeight); + } + return; + } + if (this.colWeekNumbersVisible) { + // Make sure all week number cells running down the side have the same width. + this.weekNumberWidth = core.matchCellWidths(core.findElements(this.el, '.fc-week-number')); + } + // reset all heights to be natural + this.scroller.clear(); + if (headRowEl) { + core.uncompensateScroll(headRowEl); + } + dayGrid.removeSegPopover(); // kill the "more" popover if displayed + // is the event limit a constant level number? + if (eventLimit && typeof eventLimit === 'number') { + dayGrid.limitRows(eventLimit); // limit the levels first so the height can redistribute after + } + // distribute the height to the rows + // (viewHeight is a "recommended" value if isAuto) + scrollerHeight = this.computeScrollerHeight(viewHeight); + this.setGridHeight(scrollerHeight, isAuto); + // is the event limit dynamically calculated? + if (eventLimit && typeof eventLimit !== 'number') { + dayGrid.limitRows(eventLimit); // limit the levels after the grid's row heights have been set + } + if (!isAuto) { // should we force dimensions of the scroll container? + this.scroller.setHeight(scrollerHeight); + scrollbarWidths = this.scroller.getScrollbarWidths(); + if (scrollbarWidths.left || scrollbarWidths.right) { // using scrollbars? + if (headRowEl) { + core.compensateScroll(headRowEl, scrollbarWidths); + } + // doing the scrollbar compensation might have created text overflow which created more height. redo + scrollerHeight = this.computeScrollerHeight(viewHeight); + this.scroller.setHeight(scrollerHeight); + } + // guarantees the same scrollbar widths + this.scroller.lockOverflow(scrollbarWidths); + } + }; + // given a desired total height of the view, returns what the height of the scroller should be + DayGridView.prototype.computeScrollerHeight = function (viewHeight) { + return viewHeight - + core.subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller + }; + // Sets the height of just the DayGrid component in this view + DayGridView.prototype.setGridHeight = function (height, isAuto) { + if (this.opt('monthMode')) { + // if auto, make the height of each row the height that it would be if there were 6 weeks + if (isAuto) { + height *= this.dayGrid.rowCnt / 6; + } + core.distributeHeight(this.dayGrid.rowEls, height, !isAuto); // if auto, don't compensate for height-hogging rows + } + else { + if (isAuto) { + core.undistributeHeight(this.dayGrid.rowEls); // let the rows be their natural height with no expanding + } + else { + core.distributeHeight(this.dayGrid.rowEls, height, true); // true = compensate for height-hogging rows + } + } + }; + /* Scroll + ------------------------------------------------------------------------------------------------------------------*/ + DayGridView.prototype.computeInitialDateScroll = function () { + return { top: 0 }; + }; + DayGridView.prototype.queryDateScroll = function () { + return { top: this.scroller.getScrollTop() }; + }; + DayGridView.prototype.applyDateScroll = function (scroll) { + if (scroll.top !== undefined) { + this.scroller.setScrollTop(scroll.top); + } + }; + return DayGridView; + }(core.View)); + DayGridView.prototype.dateProfileGeneratorClass = DayGridDateProfileGenerator; + + var SimpleDayGrid = /** @class */ (function (_super) { + __extends(SimpleDayGrid, _super); + function SimpleDayGrid(context, dayGrid) { + var _this = _super.call(this, context, dayGrid.el) || this; + _this.slicer = new DayGridSlicer(); + _this.dayGrid = dayGrid; + context.calendar.registerInteractiveComponent(_this, { el: _this.dayGrid.el }); + return _this; + } + SimpleDayGrid.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.calendar.unregisterInteractiveComponent(this); + }; + SimpleDayGrid.prototype.render = function (props) { + var dayGrid = this.dayGrid; + var dateProfile = props.dateProfile, dayTable = props.dayTable; + dayGrid.receiveProps(__assign({}, this.slicer.sliceProps(props, dateProfile, props.nextDayThreshold, dayGrid, dayTable), { dateProfile: dateProfile, cells: dayTable.cells, isRigid: props.isRigid })); + }; + SimpleDayGrid.prototype.queryHit = function (positionLeft, positionTop) { + var rawHit = this.dayGrid.positionToHit(positionLeft, positionTop); + if (rawHit) { + return { + component: this.dayGrid, + dateSpan: rawHit.dateSpan, + dayEl: rawHit.dayEl, + rect: { + left: rawHit.relativeRect.left, + right: rawHit.relativeRect.right, + top: rawHit.relativeRect.top, + bottom: rawHit.relativeRect.bottom + }, + layer: 0 + }; + } + }; + return SimpleDayGrid; + }(core.DateComponent)); + var DayGridSlicer = /** @class */ (function (_super) { + __extends(DayGridSlicer, _super); + function DayGridSlicer() { + return _super !== null && _super.apply(this, arguments) || this; + } + DayGridSlicer.prototype.sliceRange = function (dateRange, dayTable) { + return dayTable.sliceRange(dateRange); + }; + return DayGridSlicer; + }(core.Slicer)); + + var DayGridView$1 = /** @class */ (function (_super) { + __extends(DayGridView, _super); + function DayGridView(_context, viewSpec, dateProfileGenerator, parentEl) { + var _this = _super.call(this, _context, viewSpec, dateProfileGenerator, parentEl) || this; + _this.buildDayTable = core.memoize(buildDayTable); + if (_this.opt('columnHeader')) { + _this.header = new core.DayHeader(_this.context, _this.el.querySelector('.fc-head-container')); + } + _this.simpleDayGrid = new SimpleDayGrid(_this.context, _this.dayGrid); + return _this; + } + DayGridView.prototype.destroy = function () { + _super.prototype.destroy.call(this); + if (this.header) { + this.header.destroy(); + } + this.simpleDayGrid.destroy(); + }; + DayGridView.prototype.render = function (props) { + _super.prototype.render.call(this, props); + var dateProfile = this.props.dateProfile; + var dayTable = this.dayTable = + this.buildDayTable(dateProfile, this.dateProfileGenerator); + if (this.header) { + this.header.receiveProps({ + dateProfile: dateProfile, + dates: dayTable.headerDates, + datesRepDistinctDays: dayTable.rowCnt === 1, + renderIntroHtml: this.renderHeadIntroHtml + }); + } + this.simpleDayGrid.receiveProps({ + dateProfile: dateProfile, + dayTable: dayTable, + businessHours: props.businessHours, + dateSelection: props.dateSelection, + eventStore: props.eventStore, + eventUiBases: props.eventUiBases, + eventSelection: props.eventSelection, + eventDrag: props.eventDrag, + eventResize: props.eventResize, + isRigid: this.hasRigidRows(), + nextDayThreshold: this.nextDayThreshold + }); + }; + return DayGridView; + }(DayGridView)); + function buildDayTable(dateProfile, dateProfileGenerator) { + var daySeries = new core.DaySeries(dateProfile.renderRange, dateProfileGenerator); + return new core.DayTable(daySeries, /year|month|week/.test(dateProfile.currentRangeUnit)); + } + + var main = core.createPlugin({ + defaultView: 'dayGridMonth', + views: { + dayGrid: DayGridView$1, + dayGridDay: { + type: 'dayGrid', + duration: { days: 1 } + }, + dayGridWeek: { + type: 'dayGrid', + duration: { weeks: 1 } + }, + dayGridMonth: { + type: 'dayGrid', + duration: { months: 1 }, + monthMode: true, + fixedWeekCount: true + } + } + }); + + exports.AbstractDayGridView = DayGridView; + exports.DayBgRow = DayBgRow; + exports.DayGrid = DayGrid; + exports.DayGridSlicer = DayGridSlicer; + exports.DayGridView = DayGridView$1; + exports.SimpleDayGrid = SimpleDayGrid; + exports.buildBasicDayTable = buildDayTable; + exports.default = main; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/library/fullcalendar/packages/daygrid/main.min.css b/library/fullcalendar/packages/daygrid/main.min.css new file mode 100644 index 000000000..75fd5cb88 --- /dev/null +++ b/library/fullcalendar/packages/daygrid/main.min.css @@ -0,0 +1,5 @@ +/*! +FullCalendar Day Grid Plugin v4.0.2 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/.fc-dayGridDay-view .fc-content-skeleton,.fc-dayGridWeek-view .fc-content-skeleton{padding-bottom:1em}.fc-dayGrid-view .fc-body .fc-row{min-height:4em}.fc-row.fc-rigid{overflow:hidden}.fc-row.fc-rigid .fc-content-skeleton{position:absolute;top:0;left:0;right:0}.fc-day-top.fc-other-month{opacity:.3}.fc-dayGrid-view .fc-day-number,.fc-dayGrid-view .fc-week-number{padding:2px}.fc-dayGrid-view th.fc-day-number,.fc-dayGrid-view th.fc-week-number{padding:0 2px}.fc-ltr .fc-dayGrid-view .fc-day-top .fc-day-number{float:right}.fc-rtl .fc-dayGrid-view .fc-day-top .fc-day-number{float:left}.fc-ltr .fc-dayGrid-view .fc-day-top .fc-week-number{float:left;border-radius:0 0 3px}.fc-rtl .fc-dayGrid-view .fc-day-top .fc-week-number{float:right;border-radius:0 0 0 3px}.fc-dayGrid-view .fc-day-top .fc-week-number{min-width:1.5em;text-align:center;background-color:#f2f2f2;color:grey}.fc-dayGrid-view td.fc-week-number{text-align:center}.fc-dayGrid-view td.fc-week-number>*{display:inline-block;min-width:1.25em} \ No newline at end of file diff --git a/library/fullcalendar/packages/daygrid/main.min.js b/library/fullcalendar/packages/daygrid/main.min.js new file mode 100644 index 000000000..54b390012 --- /dev/null +++ b/library/fullcalendar/packages/daygrid/main.min.js @@ -0,0 +1,20 @@ +/*! +FullCalendar Day Grid Plugin v4.0.2 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@fullcalendar/core")):"function"==typeof define&&define.amd?define(["exports","@fullcalendar/core"],t):(e=e||self,t(e.FullCalendarDayGrid={},e.FullCalendar))}(this,function(e,t){"use strict";function r(e,t){function r(){this.constructor=e}l(e,t),e.prototype=null===t?Object.create(t):(r.prototype=t.prototype,new r)}function n(e,t){var r,n;for(r=0;r=e.firstCol)return!0;return!1}function i(e,t){return e.leftCol-t.leftCol}function o(e,r,n,i){var o=n.dateEnv,s=n.theme,l=t.rangeContainsMarker(r.activeRange,e),a=t.getDayClasses(e,r,n);return a.unshift("fc-day",s.getClass("widgetContent")),'"}function s(e,r){var n=new t.DaySeries(e.renderRange,r);return new t.DayTable(n,/year|month|week/.test(e.currentRangeUnit))}/*! ***************************************************************************** + Copyright (c) Microsoft Corporation. All rights reserved. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of the + License at http://www.apache.org/licenses/LICENSE-2.0 + + THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED + WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, + MERCHANTABLITY OR NON-INFRINGEMENT. + + See the Apache Version 2.0 License for specific language governing permissions + and limitations under the License. + ***************************************************************************** */ +var l=function(e,t){return(l=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var r in t)t.hasOwnProperty(r)&&(e[r]=t[r])})(e,t)},a=function(){return a=Object.assign||function(e){for(var t,r=1,n=arguments.length;r'+t.htmlEscape(n)+""),i=''+(t.htmlEscape(l.title||"")||" ")+"",'
'+("rtl"===o.dir?i+" "+g:g+" "+i)+"
"+(h?'
':"")+(p?'
':"")+"
"},n.prototype.computeEventTimeFormat=function(){return{hour:"numeric",minute:"2-digit",omitZeroMinute:!0,meridiem:"narrow"}},n.prototype.computeDisplayEventEnd=function(){return!1},n}(t.FgEventRenderer),p=function(e){function o(t){var r=e.call(this,t.context)||this;return r.dayGrid=t,r}return r(o,e),o.prototype.attachSegs=function(e,t){var r=this.rowStructs=this.renderSegRows(e);this.dayGrid.rowEls.forEach(function(e,t){e.querySelector(".fc-content-skeleton > table").appendChild(r[t].tbodyEl)}),t||this.dayGrid.removeSegPopover()},o.prototype.detachSegs=function(){for(var e,r=this.rowStructs||[];e=r.pop();)t.removeElement(e.tbodyEl);this.rowStructs=null},o.prototype.renderSegRows=function(e){var t,r,n=[];for(t=this.groupSegRows(e),r=0;r
');n&&n.row===r?o=n.el:(o=e.querySelector(".fc-content-skeleton tbody"))||(o=e.querySelector(".fc-content-skeleton table")),s=o.getBoundingClientRect().top-e.getBoundingClientRect().top,l.style.top=s+"px",l.querySelector("table").appendChild(i[r].tbodyEl),e.appendChild(l)})},n}(p),f=function(e){function n(t){var r=e.call(this,t.context)||this;return r.fillSegTag="td",r.dayGrid=t,r}return r(n,e),n.prototype.renderSegs=function(t,r){"bgEvent"===t&&(r=r.filter(function(e){return e.eventRange.def.allDay})),e.prototype.renderSegs.call(this,t,r)},n.prototype.attachSegs=function(e,t){var r,n,i,o=[];for(r=0;r
'),o=i.getElementsByTagName("tr")[0],h>0&&t.appendToElement(o,new Array(h+1).join("")),r.el.colSpan=p-h,o.appendChild(r.el),p"));var u=s.renderProps.renderIntroHtml();return u&&(s.isRtl?t.appendToElement(o,u):t.prependToElement(o,u)),i},n}(t.FillRenderer),g=function(e){function n(r,n){var i=e.call(this,r,n)||this,o=i.eventRenderer=new m(i),s=i.renderFrame=t.memoizeRendering(i._renderFrame);return i.renderFgEvents=t.memoizeRendering(o.renderSegs.bind(o),o.unrender.bind(o),[s]),i.renderEventSelection=t.memoizeRendering(o.selectByInstanceId.bind(o),o.unselectByInstanceId.bind(o),[i.renderFgEvents]),i.renderEventDrag=t.memoizeRendering(o.hideByHash.bind(o),o.showByHash.bind(o),[s]),i.renderEventResize=t.memoizeRendering(o.hideByHash.bind(o),o.showByHash.bind(o),[s]),r.calendar.registerInteractiveComponent(i,{el:i.el,useEventCenter:!1}),i}return r(n,e),n.prototype.render=function(e){this.renderFrame(e.date),this.renderFgEvents(e.fgSegs),this.renderEventSelection(e.eventSelection),this.renderEventDrag(e.eventDragInstances),this.renderEventResize(e.eventResizeInstances)},n.prototype.destroy=function(){e.prototype.destroy.call(this),this.renderFrame.unrender(),this.calendar.unregisterInteractiveComponent(this)},n.prototype._renderFrame=function(e){var r=this,n=r.theme,i=r.dateEnv,o=i.format(e,t.createFormatter(this.opt("dayPopoverFormat")));this.el.innerHTML='
'+t.htmlEscape(o)+'
',this.segContainerEl=this.el.querySelector(".fc-event-container")},n.prototype.queryHit=function(e,r,n,i){var o=this.props.date;if(e'),"rtl"===this.context.options.dir&&t.reverse(),""+t.join("")+""},e}(),v=t.createFormatter({day:"numeric"}),b=t.createFormatter({week:"numeric"}),w=function(e){function n(r,n,i){var o=e.call(this,r,n)||this;o.bottomCoordPadding=0,o.isCellSizesDirty=!1;var s=o.eventRenderer=new p(o),l=o.fillRenderer=new f(o);o.mirrorRenderer=new u(o);var a=o.renderCells=t.memoizeRendering(o._renderCells,o._unrenderCells);return o.renderBusinessHours=t.memoizeRendering(l.renderSegs.bind(l,"businessHours"),l.unrender.bind(l,"businessHours"),[a]),o.renderDateSelection=t.memoizeRendering(l.renderSegs.bind(l,"highlight"),l.unrender.bind(l,"highlight"),[a]),o.renderBgEvents=t.memoizeRendering(l.renderSegs.bind(l,"bgEvent"),l.unrender.bind(l,"bgEvent"),[a]),o.renderFgEvents=t.memoizeRendering(s.renderSegs.bind(s),s.unrender.bind(s),[a]),o.renderEventSelection=t.memoizeRendering(s.selectByInstanceId.bind(s),s.unselectByInstanceId.bind(s),[o.renderFgEvents]),o.renderEventDrag=t.memoizeRendering(o._renderEventDrag,o._unrenderEventDrag,[a]),o.renderEventResize=t.memoizeRendering(o._renderEventResize,o._unrenderEventResize,[a]),o.renderProps=i,o}return r(n,e),n.prototype.render=function(e){var t=e.cells;this.rowCnt=t.length,this.colCnt=t[0].length,this.renderCells(t,e.isRigid),this.renderBusinessHours(e.businessHourSegs),this.renderDateSelection(e.dateSelectionSegs),this.renderBgEvents(e.bgEventSegs),this.renderFgEvents(e.fgEventSegs),this.renderEventSelection(e.eventSelection),this.renderEventDrag(e.eventDrag),this.renderEventResize(e.eventResize),this.segPopoverTile&&this.updateSegPopoverTile()},n.prototype.destroy=function(){e.prototype.destroy.call(this),this.renderCells.unrender()},n.prototype.getCellRange=function(e,r){var n=this.props.cells[e][r].date;return{start:n,end:t.addDays(n,1)}},n.prototype.updateSegPopoverTile=function(e,t){var r=this.props;this.segPopoverTile.receiveProps({date:e||this.segPopoverTile.props.date,fgSegs:t||this.segPopoverTile.props.fgSegs,eventSelection:r.eventSelection,eventDragInstances:r.eventDrag?r.eventDrag.affectedInstances:null,eventResizeInstances:r.eventResize?r.eventResize.affectedInstances:null})},n.prototype._renderCells=function(e,r){var n,i,o=this,s=o.view,l=o.dateEnv,a=this,d=a.rowCnt,c=a.colCnt,h="";for(n=0;n
'+i.renderHtml({cells:this.props.cells[e],dateProfile:this.props.dateProfile,renderIntroHtml:this.renderProps.renderBgIntroHtml})+'
'+(this.getIsNumbersVisible()?""+this.renderNumberTrHtml(e)+"":"")+"
"},n.prototype.getIsNumbersVisible=function(){return this.getIsDayNumbersVisible()||this.renderProps.cellWeekNumbersVisible||this.renderProps.colWeekNumbersVisible},n.prototype.getIsDayNumbersVisible=function(){return this.rowCnt>1},n.prototype.renderNumberTrHtml=function(e){var t=this.renderProps.renderNumberIntroHtml(e,this);return""+(this.isRtl?"":t)+this.renderNumberCellsHtml(e)+(this.isRtl?t:"")+""},n.prototype.renderNumberCellsHtml=function(e){var t,r,n=[];for(t=0;t",this.renderProps.cellWeekNumbersVisible&&e.getUTCDay()===n&&(l+=t.buildGotoAnchorHtml(o,{date:e,type:"week"},{class:"fc-week-number"},s.format(e,b))),d&&(l+=t.buildGotoAnchorHtml(o,e,{class:"fc-day-number"},s.format(e,v))),l+=""):""},n.prototype.updateSize=function(e){var t=this,r=t.fillRenderer,n=t.eventRenderer,i=t.mirrorRenderer;(e||this.isCellSizesDirty)&&(this.buildColPositions(),this.buildRowPositions(),this.isCellSizesDirty=!1),r.computeSizes(e),n.computeSizes(e),i.computeSizes(e),r.assignSizes(e),n.assignSizes(e),i.assignSizes(e)},n.prototype.buildColPositions=function(){this.colPositions.build()},n.prototype.buildRowPositions=function(){this.rowPositions.build(),this.rowPositions.bottoms[this.rowCnt-1]+=this.bottomCoordPadding},n.prototype.positionToHit=function(e,t){var r=this,n=r.colPositions,i=r.rowPositions,o=n.leftToIndex(e),s=i.topToIndex(t);if(null!=s&&null!=o)return{row:s,col:o,dateSpan:{range:this.getCellRange(s,o),allDay:!0},dayEl:this.getCellEl(s,o),relativeRect:{left:n.lefts[o],right:n.rights[o],top:i.tops[s],bottom:i.bottoms[s]}}},n.prototype.getCellEl=function(e,t){return this.cellEls[e*this.colCnt+t]},n.prototype._renderEventDrag=function(e){e&&(this.eventRenderer.hideByHash(e.affectedInstances),this.fillRenderer.renderSegs("highlight",e.segs))},n.prototype._unrenderEventDrag=function(e){e&&(this.eventRenderer.showByHash(e.affectedInstances),this.fillRenderer.unrender("highlight"))},n.prototype._renderEventResize=function(e){e&&(this.eventRenderer.hideByHash(e.affectedInstances),this.fillRenderer.renderSegs("highlight",e.segs),this.mirrorRenderer.renderSegs(e.segs,{isResizing:!0,sourceSeg:e.sourceSeg}))},n.prototype._unrenderEventResize=function(e){e&&(this.eventRenderer.showByHash(e.affectedInstances),this.fillRenderer.unrender("highlight"),this.mirrorRenderer.unrender(e.segs,{isResizing:!0,sourceSeg:e.sourceSeg}))},n.prototype.removeSegPopover=function(){this.segPopover&&this.segPopover.hide()},n.prototype.limitRows=function(e){var t,r,n=this.eventRenderer.rowStructs||[];for(t=0;to)return r;return!1},n.prototype.limitRow=function(e,r){var n,i,o,s,l,a,d,c,h,p,u,f,g,m,y,v=this,b=this,w=b.colCnt,S=b.isRtl,C=this.eventRenderer.rowStructs[e],E=[],R=0,H=function(n){for(;R"+t.htmlEscape(s.opt("weekLabel"))+"":""},s.renderDayGridNumberIntroHtml=function(e,r){var n=s.dateEnv,i=r.props.cells[e][0].date;return s.colWeekNumbersVisible?'"+t.buildGotoAnchorHtml(s,{date:i,type:"week",forceOff:1===r.colCnt},n.format(i,S))+"":""},s.renderDayGridBgIntroHtml=function(){var e=s.theme;return s.colWeekNumbersVisible?'":""},s.renderDayGridIntroHtml=function(){return s.colWeekNumbersVisible?'":""},s.el.classList.add("fc-dayGrid-view"),s.el.innerHTML=s.renderSkeletonHtml(),s.scroller=new t.ScrollComponent("hidden","auto");var l=s.scroller.el;s.el.querySelector(".fc-body > tr > td").appendChild(l),l.classList.add("fc-day-grid-container");var a=t.createElement("div",{className:"fc-day-grid"});l.appendChild(a);var d;return s.opt("weekNumbers")?s.opt("weekNumbersWithinDays")?(d=!0,s.colWeekNumbersVisible=!1):(d=!1,s.colWeekNumbersVisible=!0):(s.colWeekNumbersVisible=!1,d=!1),s.dayGrid=new w(s.context,a,{renderNumberIntroHtml:s.renderDayGridNumberIntroHtml,renderBgIntroHtml:s.renderDayGridBgIntroHtml,renderIntroHtml:s.renderDayGridIntroHtml,colWeekNumbersVisible:s.colWeekNumbersVisible,cellWeekNumbersVisible:d}),s}return r(n,e),n.prototype.destroy=function(){e.prototype.destroy.call(this),this.dayGrid.destroy(),this.scroller.destroy()},n.prototype.renderSkeletonHtml=function(){var e=this.theme;return''+(this.opt("columnHeader")?'':"")+'
 
'},n.prototype.weekNumberStyleAttr=function(){return null!=this.weekNumberWidth?'style="width:'+this.weekNumberWidth+'px"':""},n.prototype.hasRigidRows=function(){var e=this.opt("eventLimit");return e&&"number"!=typeof e},n.prototype.updateSize=function(t,r,n){e.prototype.updateSize.call(this,t,r,n),this.dayGrid.updateSize(t)},n.prototype.updateBaseSize=function(e,r,n){var i,o,s=this.dayGrid,l=this.opt("eventLimit"),a=this.header?this.header.el:null;if(!s.rowEls)return void(n||(i=this.computeScrollerHeight(r),this.scroller.setHeight(i)));this.colWeekNumbersVisible&&(this.weekNumberWidth=t.matchCellWidths(t.findElements(this.el,".fc-week-number"))),this.scroller.clear(),a&&t.uncompensateScroll(a),s.removeSegPopover(),l&&"number"==typeof l&&s.limitRows(l),i=this.computeScrollerHeight(r),this.setGridHeight(i,n),l&&"number"!=typeof l&&s.limitRows(l),n||(this.scroller.setHeight(i),o=this.scroller.getScrollbarWidths(),(o.left||o.right)&&(a&&t.compensateScroll(a,o),i=this.computeScrollerHeight(r),this.scroller.setHeight(i)),this.scroller.lockOverflow(o))},n.prototype.computeScrollerHeight=function(e){return e-t.subtractInnerElHeight(this.el,this.scroller.el)},n.prototype.setGridHeight=function(e,r){this.opt("monthMode")?(r&&(e*=this.dayGrid.rowCnt/6),t.distributeHeight(this.dayGrid.rowEls,e,!r)):r?t.undistributeHeight(this.dayGrid.rowEls):t.distributeHeight(this.dayGrid.rowEls,e,!0)},n.prototype.computeInitialDateScroll=function(){return{top:0}},n.prototype.queryDateScroll=function(){return{top:this.scroller.getScrollTop()}},n.prototype.applyDateScroll=function(e){void 0!==e.top&&this.scroller.setScrollTop(e.top)},n}(t.View);C.prototype.dateProfileGeneratorClass=d;var E=function(e){function t(t,r){var n=e.call(this,t,r.el)||this;return n.slicer=new R,n.dayGrid=r,t.calendar.registerInteractiveComponent(n,{el:n.dayGrid.el}),n}return r(t,e),t.prototype.destroy=function(){e.prototype.destroy.call(this),this.calendar.unregisterInteractiveComponent(this)},t.prototype.render=function(e){var t=this.dayGrid,r=e.dateProfile,n=e.dayTable;t.receiveProps(a({},this.slicer.sliceProps(e,r,e.nextDayThreshold,t,n),{dateProfile:r,cells:n.cells,isRigid:e.isRigid}))},t.prototype.queryHit=function(e,t){var r=this.dayGrid.positionToHit(e,t);if(r)return{component:this.dayGrid,dateSpan:r.dateSpan,dayEl:r.dayEl,rect:{left:r.relativeRect.left,right:r.relativeRect.right,top:r.relativeRect.top,bottom:r.relativeRect.bottom},layer:0}},t}(t.DateComponent),R=function(e){function t(){return null!==e&&e.apply(this,arguments)||this}return r(t,e),t.prototype.sliceRange=function(e,t){return t.sliceRange(e)},t}(t.Slicer),H=function(e){function n(r,n,i,o){var l=e.call(this,r,n,i,o)||this;return l.buildDayTable=t.memoize(s),l.opt("columnHeader")&&(l.header=new t.DayHeader(l.context,l.el.querySelector(".fc-head-container"))),l.simpleDayGrid=new E(l.context,l.dayGrid),l}return r(n,e),n.prototype.destroy=function(){e.prototype.destroy.call(this),this.header&&this.header.destroy(),this.simpleDayGrid.destroy()},n.prototype.render=function(t){e.prototype.render.call(this,t);var r=this.props.dateProfile,n=this.dayTable=this.buildDayTable(r,this.dateProfileGenerator);this.header&&this.header.receiveProps({dateProfile:r,dates:n.headerDates,datesRepDistinctDays:1===n.rowCnt,renderIntroHtml:this.renderHeadIntroHtml}),this.simpleDayGrid.receiveProps({dateProfile:r,dayTable:n,businessHours:t.businessHours,dateSelection:t.dateSelection,eventStore:t.eventStore,eventUiBases:t.eventUiBases,eventSelection:t.eventSelection,eventDrag:t.eventDrag,eventResize:t.eventResize,isRigid:this.hasRigidRows(),nextDayThreshold:this.nextDayThreshold})},n}(C),D=t.createPlugin({defaultView:"dayGridMonth",views:{dayGrid:H,dayGridDay:{type:"dayGrid",duration:{days:1}},dayGridWeek:{type:"dayGrid",duration:{weeks:1}},dayGridMonth:{type:"dayGrid",duration:{months:1},monthMode:!0,fixedWeekCount:!0}}});e.AbstractDayGridView=C,e.DayBgRow=y,e.DayGrid=w,e.DayGridSlicer=R,e.DayGridView=H,e.SimpleDayGrid=E,e.buildBasicDayTable=s,e.default=D,Object.defineProperty(e,"__esModule",{value:!0})}); \ No newline at end of file diff --git a/library/fullcalendar/packages/google-calendar/main.js b/library/fullcalendar/packages/google-calendar/main.js new file mode 100644 index 000000000..89ce3d7a6 --- /dev/null +++ b/library/fullcalendar/packages/google-calendar/main.js @@ -0,0 +1,169 @@ +/*! +FullCalendar Google Calendar Plugin v4.0.2 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core')) : + typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core'], factory) : + (global = global || self, factory(global.FullCalendarGoogleCalendar = {}, global.FullCalendar)); +}(this, function (exports, core) { 'use strict'; + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. All rights reserved. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of the + License at http://www.apache.org/licenses/LICENSE-2.0 + + THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED + WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, + MERCHANTABLITY OR NON-INFRINGEMENT. + + See the Apache Version 2.0 License for specific language governing permissions + and limitations under the License. + ***************************************************************************** */ + + var __assign = function() { + __assign = Object.assign || function __assign(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); + }; + + // TODO: expose somehow + var API_BASE = 'https://www.googleapis.com/calendar/v3/calendars'; + var STANDARD_PROPS = { + url: String, + googleCalendarApiKey: String, + googleCalendarId: String, + data: null + }; + var eventSourceDef = { + parseMeta: function (raw) { + if (typeof raw === 'string') { + raw = { url: raw }; + } + if (typeof raw === 'object') { + var standardProps = core.refineProps(raw, STANDARD_PROPS); + if (!standardProps.googleCalendarId && standardProps.url) { + standardProps.googleCalendarId = parseGoogleCalendarId(standardProps.url); + } + delete standardProps.url; + if (standardProps.googleCalendarId) { + return standardProps; + } + } + return null; + }, + fetch: function (arg, onSuccess, onFailure) { + var calendar = arg.calendar; + var meta = arg.eventSource.meta; + var apiKey = meta.googleCalendarApiKey || calendar.opt('googleCalendarApiKey'); + if (!apiKey) { + onFailure({ + message: 'Specify a googleCalendarApiKey. See http://fullcalendar.io/docs/google_calendar/' + }); + } + else { + var url = buildUrl(meta); + var requestParams_1 = buildRequestParams(arg.range, apiKey, meta.data, calendar.dateEnv); + core.requestJson('GET', url, requestParams_1, function (body, xhr) { + if (body.error) { + onFailure({ + message: 'Google Calendar API: ' + body.error.message, + errors: body.error.errors, + xhr: xhr + }); + } + else { + onSuccess({ + rawEvents: gcalItemsToRawEventDefs(body.items, requestParams_1.timeZone), + xhr: xhr + }); + } + }, function (message, xhr) { + onFailure({ message: message, xhr: xhr }); + }); + } + } + }; + function parseGoogleCalendarId(url) { + var match; + // detect if the ID was specified as a single string. + // will match calendars like "asdf1234@calendar.google.com" in addition to person email calendars. + if (/^[^\/]+@([^\/\.]+\.)*(google|googlemail|gmail)\.com$/.test(url)) { + return url; + } + else if ((match = /^https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/([^\/]*)/.exec(url)) || + (match = /^https?:\/\/www.google.com\/calendar\/feeds\/([^\/]*)/.exec(url))) { + return decodeURIComponent(match[1]); + } + } + function buildUrl(meta) { + return API_BASE + '/' + encodeURIComponent(meta.googleCalendarId) + '/events'; + } + function buildRequestParams(range, apiKey, extraParams, dateEnv) { + var params; + var startStr; + var endStr; + if (dateEnv.canComputeOffset) { + // strings will naturally have offsets, which GCal needs + startStr = dateEnv.formatIso(range.start); + endStr = dateEnv.formatIso(range.end); + } + else { + // when timezone isn't known, we don't know what the UTC offset should be, so ask for +/- 1 day + // from the UTC day-start to guarantee we're getting all the events + // (start/end will be UTC-coerced dates, so toISOString is okay) + startStr = core.addDays(range.start, -1).toISOString(); + endStr = core.addDays(range.end, 1).toISOString(); + } + params = __assign({}, (extraParams || {}), { key: apiKey, timeMin: startStr, timeMax: endStr, singleEvents: true, maxResults: 9999 }); + if (dateEnv.timeZone !== 'local') { + params.timeZone = dateEnv.timeZone; + } + return params; + } + function gcalItemsToRawEventDefs(items, gcalTimezone) { + return items.map(function (item) { + return gcalItemToRawEventDef(item, gcalTimezone); + }); + } + function gcalItemToRawEventDef(item, gcalTimezone) { + var url = item.htmlLink || null; + // make the URLs for each event show times in the correct timezone + if (url && gcalTimezone) { + url = injectQsComponent(url, 'ctz=' + gcalTimezone); + } + return { + id: item.id, + title: item.summary, + start: item.start.dateTime || item.start.date, + end: item.end.dateTime || item.end.date, + url: url, + location: item.location, + description: item.description + }; + } + // Injects a string like "arg=value" into the querystring of a URL + // TODO: move to a general util file? + function injectQsComponent(url, component) { + // inject it after the querystring but before the fragment + return url.replace(/(\?.*?)?(#|$)/, function (whole, qs, hash) { + return (qs ? qs + '&' : '?') + component + hash; + }); + } + var main = core.createPlugin({ + eventSourceDefs: [eventSourceDef] + }); + + exports.default = main; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/library/fullcalendar/packages/google-calendar/main.min.js b/library/fullcalendar/packages/google-calendar/main.min.js new file mode 100644 index 000000000..51947e659 --- /dev/null +++ b/library/fullcalendar/packages/google-calendar/main.min.js @@ -0,0 +1,20 @@ +/*! +FullCalendar Google Calendar Plugin v4.0.2 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ +!function(e,r){"object"==typeof exports&&"undefined"!=typeof module?r(exports,require("@fullcalendar/core")):"function"==typeof define&&define.amd?define(["exports","@fullcalendar/core"],r):(e=e||self,r(e.FullCalendarGoogleCalendar={},e.FullCalendar))}(this,function(e,r){"use strict";function t(e){var r;return/^[^\/]+@([^\/\.]+\.)*(google|googlemail|gmail)\.com$/.test(e)?e:(r=/^https:\/\/www.googleapis.com\/calendar\/v3\/calendars\/([^\/]*)/.exec(e))||(r=/^https?:\/\/www.google.com\/calendar\/feeds\/([^\/]*)/.exec(e))?decodeURIComponent(r[1]):void 0}function n(e){return s+"/"+encodeURIComponent(e.googleCalendarId)+"/events"}function o(e,t,n,o){var a,l,i;return o.canComputeOffset?(l=o.formatIso(e.start),i=o.formatIso(e.end)):(l=r.addDays(e.start,-1).toISOString(),i=r.addDays(e.end,1).toISOString()),a=d({},n||{},{key:t,timeMin:l,timeMax:i,singleEvents:!0,maxResults:9999}),"local"!==o.timeZone&&(a.timeZone=o.timeZone),a}function a(e,r){return e.map(function(e){return l(e,r)})}function l(e,r){var t=e.htmlLink||null;return t&&r&&(t=i(t,"ctz="+r)),{id:e.id,title:e.summary,start:e.start.dateTime||e.start.date,end:e.end.dateTime||e.end.date,url:t,location:e.location,description:e.description}}function i(e,r){return e.replace(/(\?.*?)?(#|$)/,function(e,t,n){return(t?t+"&":"?")+r+n})}/*! ***************************************************************************** + Copyright (c) Microsoft Corporation. All rights reserved. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of the + License at http://www.apache.org/licenses/LICENSE-2.0 + + THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED + WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, + MERCHANTABLITY OR NON-INFRINGEMENT. + + See the Apache Version 2.0 License for specific language governing permissions + and limitations under the License. + ***************************************************************************** */ +var d=function(){return d=Object.assign||function(e){for(var r,t=1,n=arguments.length;t 0) { + this.everMovedDown = true; + } + if (xDelta < 0) { + this.everMovedLeft = true; + } + else if (xDelta > 0) { + this.everMovedRight = true; + } + this.pointerScreenX = pointerScreenX; + this.pointerScreenY = pointerScreenY; + if (!this.isAnimating) { + this.isAnimating = true; + this.requestAnimation(getTime()); + } + } + }; + AutoScroller.prototype.stop = function () { + if (this.isEnabled) { + this.isAnimating = false; // will stop animation + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + scrollCache.destroy(); + } + this.scrollCaches = null; + } + }; + AutoScroller.prototype.requestAnimation = function (now) { + this.msSinceRequest = now; + requestAnimationFrame(this.animate); + }; + AutoScroller.prototype.handleSide = function (edge, seconds) { + var scrollCache = edge.scrollCache; + var edgeThreshold = this.edgeThreshold; + var invDistance = edgeThreshold - edge.distance; + var velocity = // the closer to the edge, the faster we scroll + (invDistance * invDistance) / (edgeThreshold * edgeThreshold) * // quadratic + this.maxVelocity * seconds; + var sign = 1; + switch (edge.name) { + case 'left': + sign = -1; + // falls through + case 'right': + scrollCache.setScrollLeft(scrollCache.getScrollLeft() + velocity * sign); + break; + case 'top': + sign = -1; + // falls through + case 'bottom': + scrollCache.setScrollTop(scrollCache.getScrollTop() + velocity * sign); + break; + } + }; + // left/top are relative to document topleft + AutoScroller.prototype.computeBestEdge = function (left, top) { + var edgeThreshold = this.edgeThreshold; + var bestSide = null; + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + var rect = scrollCache.clientRect; + var leftDist = left - rect.left; + var rightDist = rect.right - left; + var topDist = top - rect.top; + var bottomDist = rect.bottom - top; + // completely within the rect? + if (leftDist >= 0 && rightDist >= 0 && topDist >= 0 && bottomDist >= 0) { + if (topDist <= edgeThreshold && this.everMovedUp && scrollCache.canScrollUp() && + (!bestSide || bestSide.distance > topDist)) { + bestSide = { scrollCache: scrollCache, name: 'top', distance: topDist }; + } + if (bottomDist <= edgeThreshold && this.everMovedDown && scrollCache.canScrollDown() && + (!bestSide || bestSide.distance > bottomDist)) { + bestSide = { scrollCache: scrollCache, name: 'bottom', distance: bottomDist }; + } + if (leftDist <= edgeThreshold && this.everMovedLeft && scrollCache.canScrollLeft() && + (!bestSide || bestSide.distance > leftDist)) { + bestSide = { scrollCache: scrollCache, name: 'left', distance: leftDist }; + } + if (rightDist <= edgeThreshold && this.everMovedRight && scrollCache.canScrollRight() && + (!bestSide || bestSide.distance > rightDist)) { + bestSide = { scrollCache: scrollCache, name: 'right', distance: rightDist }; + } + } + } + return bestSide; + }; + AutoScroller.prototype.buildCaches = function () { + return this.queryScrollEls().map(function (el) { + if (el === window) { + return new WindowScrollGeomCache(false); // false = don't listen to user-generated scrolls + } + else { + return new ElementScrollGeomCache(el, false); // false = don't listen to user-generated scrolls + } + }); + }; + AutoScroller.prototype.queryScrollEls = function () { + var els = []; + for (var _i = 0, _a = this.scrollQuery; _i < _a.length; _i++) { + var query = _a[_i]; + if (typeof query === 'object') { + els.push(query); + } + else { + els.push.apply(els, Array.prototype.slice.call(document.querySelectorAll(query))); + } + } + return els; + }; + return AutoScroller; + }()); + + /* + Monitors dragging on an element. Has a number of high-level features: + - minimum distance required before dragging + - minimum wait time ("delay") before dragging + - a mirror element that follows the pointer + */ + var FeaturefulElementDragging = /** @class */ (function (_super) { + __extends(FeaturefulElementDragging, _super); + function FeaturefulElementDragging(containerEl) { + var _this = _super.call(this, containerEl) || this; + // options that can be directly set by caller + // the caller can also set the PointerDragging's options as well + _this.delay = null; + _this.minDistance = 0; + _this.touchScrollAllowed = true; // prevents drag from starting and blocks scrolling during drag + _this.mirrorNeedsRevert = false; + _this.isInteracting = false; // is the user validly moving the pointer? lasts until pointerup + _this.isDragging = false; // is it INTENTFULLY dragging? lasts until after revert animation + _this.isDelayEnded = false; + _this.isDistanceSurpassed = false; + _this.delayTimeoutId = null; + _this.onPointerDown = function (ev) { + if (!_this.isDragging) { // so new drag doesn't happen while revert animation is going + _this.isInteracting = true; + _this.isDelayEnded = false; + _this.isDistanceSurpassed = false; + core.preventSelection(document.body); + core.preventContextMenu(document.body); + // prevent links from being visited if there's an eventual drag. + // also prevents selection in older browsers (maybe?). + // not necessary for touch, besides, browser would complain about passiveness. + if (!ev.isTouch) { + ev.origEvent.preventDefault(); + } + _this.emitter.trigger('pointerdown', ev); + if (!_this.pointer.shouldIgnoreMove) { + // actions related to initiating dragstart+dragmove+dragend... + _this.mirror.setIsVisible(false); // reset. caller must set-visible + _this.mirror.start(ev.subjectEl, ev.pageX, ev.pageY); // must happen on first pointer down + _this.startDelay(ev); + if (!_this.minDistance) { + _this.handleDistanceSurpassed(ev); + } + } + } + }; + _this.onPointerMove = function (ev) { + if (_this.isInteracting) { // if false, still waiting for previous drag's revert + _this.emitter.trigger('pointermove', ev); + if (!_this.isDistanceSurpassed) { + var minDistance = _this.minDistance; + var distanceSq = void 0; // current distance from the origin, squared + var deltaX = ev.deltaX, deltaY = ev.deltaY; + distanceSq = deltaX * deltaX + deltaY * deltaY; + if (distanceSq >= minDistance * minDistance) { // use pythagorean theorem + _this.handleDistanceSurpassed(ev); + } + } + if (_this.isDragging) { + // a real pointer move? (not one simulated by scrolling) + if (ev.origEvent.type !== 'scroll') { + _this.mirror.handleMove(ev.pageX, ev.pageY); + _this.autoScroller.handleMove(ev.pageX, ev.pageY); + } + _this.emitter.trigger('dragmove', ev); + } + } + }; + _this.onPointerUp = function (ev) { + if (_this.isInteracting) { // if false, still waiting for previous drag's revert + _this.isInteracting = false; + core.allowSelection(document.body); + core.allowContextMenu(document.body); + _this.emitter.trigger('pointerup', ev); // can potentially set mirrorNeedsRevert + if (_this.isDragging) { + _this.autoScroller.stop(); + _this.tryStopDrag(ev); // which will stop the mirror + } + if (_this.delayTimeoutId) { + clearTimeout(_this.delayTimeoutId); + _this.delayTimeoutId = null; + } + } + }; + var pointer = _this.pointer = new PointerDragging(containerEl); + pointer.emitter.on('pointerdown', _this.onPointerDown); + pointer.emitter.on('pointermove', _this.onPointerMove); + pointer.emitter.on('pointerup', _this.onPointerUp); + _this.mirror = new ElementMirror(); + _this.autoScroller = new AutoScroller(); + return _this; + } + FeaturefulElementDragging.prototype.destroy = function () { + this.pointer.destroy(); + }; + FeaturefulElementDragging.prototype.startDelay = function (ev) { + var _this = this; + if (typeof this.delay === 'number') { + this.delayTimeoutId = setTimeout(function () { + _this.delayTimeoutId = null; + _this.handleDelayEnd(ev); + }, this.delay); + } + else { + this.handleDelayEnd(ev); + } + }; + FeaturefulElementDragging.prototype.handleDelayEnd = function (ev) { + this.isDelayEnded = true; + this.tryStartDrag(ev); + }; + FeaturefulElementDragging.prototype.handleDistanceSurpassed = function (ev) { + this.isDistanceSurpassed = true; + this.tryStartDrag(ev); + }; + FeaturefulElementDragging.prototype.tryStartDrag = function (ev) { + if (this.isDelayEnded && this.isDistanceSurpassed) { + if (!this.pointer.wasTouchScroll || this.touchScrollAllowed) { + this.isDragging = true; + this.mirrorNeedsRevert = false; + this.autoScroller.start(ev.pageX, ev.pageY); + this.emitter.trigger('dragstart', ev); + if (this.touchScrollAllowed === false) { + this.pointer.cancelTouchScroll(); + } + } + } + }; + FeaturefulElementDragging.prototype.tryStopDrag = function (ev) { + // .stop() is ALWAYS asynchronous, which we NEED because we want all pointerup events + // that come from the document to fire beforehand. much more convenient this way. + this.mirror.stop(this.mirrorNeedsRevert, this.stopDrag.bind(this, ev) // bound with args + ); + }; + FeaturefulElementDragging.prototype.stopDrag = function (ev) { + this.isDragging = false; + this.emitter.trigger('dragend', ev); + }; + // fill in the implementations... + FeaturefulElementDragging.prototype.setIgnoreMove = function (bool) { + this.pointer.shouldIgnoreMove = bool; + }; + FeaturefulElementDragging.prototype.setMirrorIsVisible = function (bool) { + this.mirror.setIsVisible(bool); + }; + FeaturefulElementDragging.prototype.setMirrorNeedsRevert = function (bool) { + this.mirrorNeedsRevert = bool; + }; + FeaturefulElementDragging.prototype.setAutoScrollEnabled = function (bool) { + this.autoScroller.isEnabled = bool; + }; + return FeaturefulElementDragging; + }(core.ElementDragging)); + + /* + When this class is instantiated, it records the offset of an element (relative to the document topleft), + and continues to monitor scrolling, updating the cached coordinates if it needs to. + Does not access the DOM after instantiation, so highly performant. + + Also keeps track of all scrolling/overflow:hidden containers that are parents of the given element + and an determine if a given point is inside the combined clipping rectangle. + */ + var OffsetTracker = /** @class */ (function () { + function OffsetTracker(el) { + this.origRect = core.computeRect(el); + // will work fine for divs that have overflow:hidden + this.scrollCaches = core.getClippingParents(el).map(function (el) { + return new ElementScrollGeomCache(el, true); // listen=true + }); + } + OffsetTracker.prototype.destroy = function () { + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + scrollCache.destroy(); + } + }; + OffsetTracker.prototype.computeLeft = function () { + var left = this.origRect.left; + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + left += scrollCache.origScrollLeft - scrollCache.getScrollLeft(); + } + return left; + }; + OffsetTracker.prototype.computeTop = function () { + var top = this.origRect.top; + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + top += scrollCache.origScrollTop - scrollCache.getScrollTop(); + } + return top; + }; + OffsetTracker.prototype.isWithinClipping = function (pageX, pageY) { + var point = { left: pageX, top: pageY }; + for (var _i = 0, _a = this.scrollCaches; _i < _a.length; _i++) { + var scrollCache = _a[_i]; + if (!isIgnoredClipping(scrollCache.getEventTarget()) && + !core.pointInsideRect(point, scrollCache.clientRect)) { + return false; + } + } + return true; + }; + return OffsetTracker; + }()); + // certain clipping containers should never constrain interactions, like and + // https://github.com/fullcalendar/fullcalendar/issues/3615 + function isIgnoredClipping(node) { + var tagName = node.tagName; + return tagName === 'HTML' || tagName === 'BODY'; + } + + /* + Tracks movement over multiple droppable areas (aka "hits") + that exist in one or more DateComponents. + Relies on an existing draggable. + + emits: + - pointerdown + - dragstart + - hitchange - fires initially, even if not over a hit + - pointerup + - (hitchange - again, to null, if ended over a hit) + - dragend + */ + var HitDragging = /** @class */ (function () { + function HitDragging(dragging, droppableStore) { + var _this = this; + // options that can be set by caller + this.useSubjectCenter = false; + this.requireInitial = true; // if doesn't start out on a hit, won't emit any events + this.initialHit = null; + this.movingHit = null; + this.finalHit = null; // won't ever be populated if shouldIgnoreMove + this.handlePointerDown = function (ev) { + var dragging = _this.dragging; + _this.initialHit = null; + _this.movingHit = null; + _this.finalHit = null; + _this.prepareHits(); + _this.processFirstCoord(ev); + if (_this.initialHit || !_this.requireInitial) { + dragging.setIgnoreMove(false); + _this.emitter.trigger('pointerdown', ev); // TODO: fire this before computing processFirstCoord, so listeners can cancel. this gets fired by almost every handler :( + } + else { + dragging.setIgnoreMove(true); + } + }; + this.handleDragStart = function (ev) { + _this.emitter.trigger('dragstart', ev); + _this.handleMove(ev, true); // force = fire even if initially null + }; + this.handleDragMove = function (ev) { + _this.emitter.trigger('dragmove', ev); + _this.handleMove(ev); + }; + this.handlePointerUp = function (ev) { + _this.releaseHits(); + _this.emitter.trigger('pointerup', ev); + }; + this.handleDragEnd = function (ev) { + if (_this.movingHit) { + _this.emitter.trigger('hitupdate', null, true, ev); + } + _this.finalHit = _this.movingHit; + _this.movingHit = null; + _this.emitter.trigger('dragend', ev); + }; + this.droppableStore = droppableStore; + dragging.emitter.on('pointerdown', this.handlePointerDown); + dragging.emitter.on('dragstart', this.handleDragStart); + dragging.emitter.on('dragmove', this.handleDragMove); + dragging.emitter.on('pointerup', this.handlePointerUp); + dragging.emitter.on('dragend', this.handleDragEnd); + this.dragging = dragging; + this.emitter = new core.EmitterMixin(); + } + // sets initialHit + // sets coordAdjust + HitDragging.prototype.processFirstCoord = function (ev) { + var origPoint = { left: ev.pageX, top: ev.pageY }; + var adjustedPoint = origPoint; + var subjectEl = ev.subjectEl; + var subjectRect; + if (subjectEl !== document) { + subjectRect = core.computeRect(subjectEl); + adjustedPoint = core.constrainPoint(adjustedPoint, subjectRect); + } + var initialHit = this.initialHit = this.queryHitForOffset(adjustedPoint.left, adjustedPoint.top); + if (initialHit) { + if (this.useSubjectCenter && subjectRect) { + var slicedSubjectRect = core.intersectRects(subjectRect, initialHit.rect); + if (slicedSubjectRect) { + adjustedPoint = core.getRectCenter(slicedSubjectRect); + } + } + this.coordAdjust = core.diffPoints(adjustedPoint, origPoint); + } + else { + this.coordAdjust = { left: 0, top: 0 }; + } + }; + HitDragging.prototype.handleMove = function (ev, forceHandle) { + var hit = this.queryHitForOffset(ev.pageX + this.coordAdjust.left, ev.pageY + this.coordAdjust.top); + if (forceHandle || !isHitsEqual(this.movingHit, hit)) { + this.movingHit = hit; + this.emitter.trigger('hitupdate', hit, false, ev); + } + }; + HitDragging.prototype.prepareHits = function () { + this.offsetTrackers = core.mapHash(this.droppableStore, function (interactionSettings) { + return new OffsetTracker(interactionSettings.el); + }); + }; + HitDragging.prototype.releaseHits = function () { + var offsetTrackers = this.offsetTrackers; + for (var id in offsetTrackers) { + offsetTrackers[id].destroy(); + } + this.offsetTrackers = {}; + }; + HitDragging.prototype.queryHitForOffset = function (offsetLeft, offsetTop) { + var _a = this, droppableStore = _a.droppableStore, offsetTrackers = _a.offsetTrackers; + var bestHit = null; + for (var id in droppableStore) { + var component = droppableStore[id].component; + var offsetTracker = offsetTrackers[id]; + if (offsetTracker.isWithinClipping(offsetLeft, offsetTop)) { + var originLeft = offsetTracker.computeLeft(); + var originTop = offsetTracker.computeTop(); + var positionLeft = offsetLeft - originLeft; + var positionTop = offsetTop - originTop; + var origRect = offsetTracker.origRect; + var width = origRect.right - origRect.left; + var height = origRect.bottom - origRect.top; + if ( + // must be within the element's bounds + positionLeft >= 0 && positionLeft < width && + positionTop >= 0 && positionTop < height) { + var hit = component.queryHit(positionLeft, positionTop, width, height); + if (hit && + ( + // make sure the hit is within activeRange, meaning it's not a deal cell + !component.props.dateProfile || // hack for DayTile + core.rangeContainsRange(component.props.dateProfile.activeRange, hit.dateSpan.range)) && + (!bestHit || hit.layer > bestHit.layer)) { + // TODO: better way to re-orient rectangle + hit.rect.left += originLeft; + hit.rect.right += originLeft; + hit.rect.top += originTop; + hit.rect.bottom += originTop; + bestHit = hit; + } + } + } + } + return bestHit; + }; + return HitDragging; + }()); + function isHitsEqual(hit0, hit1) { + if (!hit0 && !hit1) { + return true; + } + if (Boolean(hit0) !== Boolean(hit1)) { + return false; + } + return core.isDateSpansEqual(hit0.dateSpan, hit1.dateSpan); + } + + /* + Monitors when the user clicks on a specific date/time of a component. + A pointerdown+pointerup on the same "hit" constitutes a click. + */ + var DateClicking = /** @class */ (function (_super) { + __extends(DateClicking, _super); + function DateClicking(settings) { + var _this = _super.call(this, settings) || this; + _this.handlePointerDown = function (ev) { + var dragging = _this.dragging; + // do this in pointerdown (not dragend) because DOM might be mutated by the time dragend is fired + dragging.setIgnoreMove(!_this.component.isValidDateDownEl(dragging.pointer.downEl)); + }; + // won't even fire if moving was ignored + _this.handleDragEnd = function (ev) { + var component = _this.component; + var pointer = _this.dragging.pointer; + if (!pointer.wasTouchScroll) { + var _a = _this.hitDragging, initialHit = _a.initialHit, finalHit = _a.finalHit; + if (initialHit && finalHit && isHitsEqual(initialHit, finalHit)) { + component.calendar.triggerDateClick(initialHit.dateSpan, initialHit.dayEl, component.view, ev.origEvent); + } + } + }; + var component = settings.component; + // we DO want to watch pointer moves because otherwise finalHit won't get populated + _this.dragging = new FeaturefulElementDragging(component.el); + _this.dragging.autoScroller.isEnabled = false; + var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, core.interactionSettingsToStore(settings)); + hitDragging.emitter.on('pointerdown', _this.handlePointerDown); + hitDragging.emitter.on('dragend', _this.handleDragEnd); + return _this; + } + DateClicking.prototype.destroy = function () { + this.dragging.destroy(); + }; + return DateClicking; + }(core.Interaction)); + + /* + Tracks when the user selects a portion of time of a component, + constituted by a drag over date cells, with a possible delay at the beginning of the drag. + */ + var DateSelecting = /** @class */ (function (_super) { + __extends(DateSelecting, _super); + function DateSelecting(settings) { + var _this = _super.call(this, settings) || this; + _this.dragSelection = null; + _this.handlePointerDown = function (ev) { + var _a = _this, component = _a.component, dragging = _a.dragging; + var canSelect = component.opt('selectable') && + component.isValidDateDownEl(ev.origEvent.target); + // don't bother to watch expensive moves if component won't do selection + dragging.setIgnoreMove(!canSelect); + // if touch, require user to hold down + dragging.delay = ev.isTouch ? getComponentTouchDelay(component) : null; + }; + _this.handleDragStart = function (ev) { + _this.component.calendar.unselect(ev); // unselect previous selections + }; + _this.handleHitUpdate = function (hit, isFinal) { + var calendar = _this.component.calendar; + var dragSelection = null; + var isInvalid = false; + if (hit) { + dragSelection = joinHitsIntoSelection(_this.hitDragging.initialHit, hit, calendar.pluginSystem.hooks.dateSelectionTransformers); + if (!dragSelection || !_this.component.isDateSelectionValid(dragSelection)) { + isInvalid = true; + dragSelection = null; + } + } + if (dragSelection) { + calendar.dispatch({ type: 'SELECT_DATES', selection: dragSelection }); + } + else if (!isFinal) { // only unselect if moved away while dragging + calendar.dispatch({ type: 'UNSELECT_DATES' }); + } + if (!isInvalid) { + core.enableCursor(); + } + else { + core.disableCursor(); + } + if (!isFinal) { + _this.dragSelection = dragSelection; // only clear if moved away from all hits while dragging + } + }; + _this.handlePointerUp = function (pev) { + if (_this.dragSelection) { + // selection is already rendered, so just need to report selection + _this.component.calendar.triggerDateSelect(_this.dragSelection, pev); + _this.dragSelection = null; + } + }; + var component = settings.component; + var dragging = _this.dragging = new FeaturefulElementDragging(component.el); + dragging.touchScrollAllowed = false; + dragging.minDistance = component.opt('selectMinDistance') || 0; + dragging.autoScroller.isEnabled = component.opt('dragScroll'); + var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, core.interactionSettingsToStore(settings)); + hitDragging.emitter.on('pointerdown', _this.handlePointerDown); + hitDragging.emitter.on('dragstart', _this.handleDragStart); + hitDragging.emitter.on('hitupdate', _this.handleHitUpdate); + hitDragging.emitter.on('pointerup', _this.handlePointerUp); + return _this; + } + DateSelecting.prototype.destroy = function () { + this.dragging.destroy(); + }; + return DateSelecting; + }(core.Interaction)); + function getComponentTouchDelay(component) { + var delay = component.opt('selectLongPressDelay'); + if (delay == null) { + delay = component.opt('longPressDelay'); + } + return delay; + } + function joinHitsIntoSelection(hit0, hit1, dateSelectionTransformers) { + var dateSpan0 = hit0.dateSpan; + var dateSpan1 = hit1.dateSpan; + var ms = [ + dateSpan0.range.start, + dateSpan0.range.end, + dateSpan1.range.start, + dateSpan1.range.end + ]; + ms.sort(core.compareNumbers); + var props = {}; + for (var _i = 0, dateSelectionTransformers_1 = dateSelectionTransformers; _i < dateSelectionTransformers_1.length; _i++) { + var transformer = dateSelectionTransformers_1[_i]; + var res = transformer(hit0, hit1); + if (res === false) { + return null; + } + else if (res) { + __assign(props, res); + } + } + props.range = { start: ms[0], end: ms[3] }; + props.allDay = dateSpan0.allDay; + return props; + } + + var EventDragging = /** @class */ (function (_super) { + __extends(EventDragging, _super); + function EventDragging(settings) { + var _this = _super.call(this, settings) || this; + // internal state + _this.subjectSeg = null; // the seg being selected/dragged + _this.isDragging = false; + _this.eventRange = null; + _this.relevantEvents = null; // the events being dragged + _this.receivingCalendar = null; + _this.validMutation = null; + _this.mutatedRelevantEvents = null; + _this.handlePointerDown = function (ev) { + var origTarget = ev.origEvent.target; + var _a = _this, component = _a.component, dragging = _a.dragging; + var mirror = dragging.mirror; + var initialCalendar = component.calendar; + var subjectSeg = _this.subjectSeg = core.getElSeg(ev.subjectEl); + var eventRange = _this.eventRange = subjectSeg.eventRange; + var eventInstanceId = eventRange.instance.instanceId; + _this.relevantEvents = core.getRelevantEvents(initialCalendar.state.eventStore, eventInstanceId); + dragging.minDistance = ev.isTouch ? 0 : component.opt('eventDragMinDistance'); + dragging.delay = + // only do a touch delay if touch and this event hasn't been selected yet + (ev.isTouch && eventInstanceId !== component.props.eventSelection) ? + getComponentTouchDelay$1(component) : + null; + mirror.parentNode = initialCalendar.el; + mirror.revertDuration = component.opt('dragRevertDuration'); + var isValid = component.isValidSegDownEl(origTarget) && + !core.elementClosest(origTarget, '.fc-resizer'); + dragging.setIgnoreMove(!isValid); + // disable dragging for elements that are resizable (ie, selectable) + // but are not draggable + _this.isDragging = isValid && + ev.subjectEl.classList.contains('fc-draggable'); + }; + _this.handleDragStart = function (ev) { + var initialCalendar = _this.component.calendar; + var eventRange = _this.eventRange; + var eventInstanceId = eventRange.instance.instanceId; + if (ev.isTouch) { + // need to select a different event? + if (eventInstanceId !== _this.component.props.eventSelection) { + initialCalendar.dispatch({ type: 'SELECT_EVENT', eventInstanceId: eventInstanceId }); + } + } + else { + // if now using mouse, but was previous touch interaction, clear selected event + initialCalendar.dispatch({ type: 'UNSELECT_EVENT' }); + } + if (_this.isDragging) { + initialCalendar.unselect(ev); // unselect *date* selection + initialCalendar.publiclyTrigger('eventDragStart', [ + { + el: _this.subjectSeg.el, + event: new core.EventApi(initialCalendar, eventRange.def, eventRange.instance), + jsEvent: ev.origEvent, + view: _this.component.view + } + ]); + } + }; + _this.handleHitUpdate = function (hit, isFinal) { + if (!_this.isDragging) { + return; + } + var relevantEvents = _this.relevantEvents; + var initialHit = _this.hitDragging.initialHit; + var initialCalendar = _this.component.calendar; + // states based on new hit + var receivingCalendar = null; + var mutation = null; + var mutatedRelevantEvents = null; + var isInvalid = false; + var interaction = { + affectedEvents: relevantEvents, + mutatedEvents: core.createEmptyEventStore(), + isEvent: true, + origSeg: _this.subjectSeg + }; + if (hit) { + var receivingComponent = hit.component; + receivingCalendar = receivingComponent.calendar; + if (initialCalendar === receivingCalendar || + receivingComponent.opt('editable') && receivingComponent.opt('droppable')) { + mutation = computeEventMutation(initialHit, hit, receivingCalendar.pluginSystem.hooks.eventDragMutationMassagers); + if (mutation) { + mutatedRelevantEvents = core.applyMutationToEventStore(relevantEvents, receivingCalendar.eventUiBases, mutation, receivingCalendar); + interaction.mutatedEvents = mutatedRelevantEvents; + if (!receivingComponent.isInteractionValid(interaction)) { + isInvalid = true; + mutation = null; + mutatedRelevantEvents = null; + interaction.mutatedEvents = core.createEmptyEventStore(); + } + } + } + else { + receivingCalendar = null; + } + } + _this.displayDrag(receivingCalendar, interaction); + if (!isInvalid) { + core.enableCursor(); + } + else { + core.disableCursor(); + } + if (!isFinal) { + if (initialCalendar === receivingCalendar && // TODO: write test for this + isHitsEqual(initialHit, hit)) { + mutation = null; + } + _this.dragging.setMirrorNeedsRevert(!mutation); + // render the mirror if no already-rendered mirror + // TODO: wish we could somehow wait for dispatch to guarantee render + _this.dragging.setMirrorIsVisible(!hit || !document.querySelector('.fc-mirror')); + // assign states based on new hit + _this.receivingCalendar = receivingCalendar; + _this.validMutation = mutation; + _this.mutatedRelevantEvents = mutatedRelevantEvents; + } + }; + _this.handlePointerUp = function () { + if (!_this.isDragging) { + _this.cleanup(); // because handleDragEnd won't fire + } + }; + _this.handleDragEnd = function (ev) { + if (_this.isDragging) { + var initialCalendar_1 = _this.component.calendar; + var initialView = _this.component.view; + var receivingCalendar = _this.receivingCalendar; + var eventDef = _this.eventRange.def; + var eventInstance = _this.eventRange.instance; + var eventApi = new core.EventApi(initialCalendar_1, eventDef, eventInstance); + var relevantEvents_1 = _this.relevantEvents; + var mutatedRelevantEvents = _this.mutatedRelevantEvents; + var finalHit = _this.hitDragging.finalHit; + _this.clearDrag(); // must happen after revert animation + initialCalendar_1.publiclyTrigger('eventDragStop', [ + { + el: _this.subjectSeg.el, + event: eventApi, + jsEvent: ev.origEvent, + view: initialView + } + ]); + if (_this.validMutation) { + // dropped within same calendar + if (receivingCalendar === initialCalendar_1) { + initialCalendar_1.dispatch({ + type: 'MERGE_EVENTS', + eventStore: mutatedRelevantEvents + }); + var eventDropArg = {}; + for (var _i = 0, _a = initialCalendar_1.pluginSystem.hooks.eventDropTransformers; _i < _a.length; _i++) { + var transformer = _a[_i]; + __assign(eventDropArg, transformer(_this.validMutation, initialCalendar_1)); + } + __assign(eventDropArg, { + el: ev.subjectEl, + delta: _this.validMutation.startDelta, + oldEvent: eventApi, + event: new core.EventApi(// the data AFTER the mutation + initialCalendar_1, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null), + revert: function () { + initialCalendar_1.dispatch({ + type: 'MERGE_EVENTS', + eventStore: relevantEvents_1 + }); + }, + jsEvent: ev.origEvent, + view: initialView + }); + initialCalendar_1.publiclyTrigger('eventDrop', [eventDropArg]); + // dropped in different calendar + } + else if (receivingCalendar) { + initialCalendar_1.publiclyTrigger('eventLeave', [ + { + draggedEl: ev.subjectEl, + event: eventApi, + view: initialView + } + ]); + initialCalendar_1.dispatch({ + type: 'REMOVE_EVENT_INSTANCES', + instances: _this.mutatedRelevantEvents.instances + }); + receivingCalendar.dispatch({ + type: 'MERGE_EVENTS', + eventStore: _this.mutatedRelevantEvents + }); + if (ev.isTouch) { + receivingCalendar.dispatch({ + type: 'SELECT_EVENT', + eventInstanceId: eventInstance.instanceId + }); + } + var dropArg = receivingCalendar.buildDatePointApi(finalHit.dateSpan); + dropArg.draggedEl = ev.subjectEl; + dropArg.jsEvent = ev.origEvent; + dropArg.view = finalHit.component; // ? + receivingCalendar.publiclyTrigger('drop', [dropArg]); + receivingCalendar.publiclyTrigger('eventReceive', [ + { + draggedEl: ev.subjectEl, + event: new core.EventApi(// the data AFTER the mutation + receivingCalendar, mutatedRelevantEvents.defs[eventDef.defId], mutatedRelevantEvents.instances[eventInstance.instanceId]), + view: finalHit.component + } + ]); + } + } + else { + initialCalendar_1.publiclyTrigger('_noEventDrop'); + } + } + _this.cleanup(); + }; + var component = _this.component; + var dragging = _this.dragging = new FeaturefulElementDragging(component.el); + dragging.pointer.selector = EventDragging.SELECTOR; + dragging.touchScrollAllowed = false; + dragging.autoScroller.isEnabled = component.opt('dragScroll'); + var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, core.interactionSettingsStore); + hitDragging.useSubjectCenter = settings.useEventCenter; + hitDragging.emitter.on('pointerdown', _this.handlePointerDown); + hitDragging.emitter.on('dragstart', _this.handleDragStart); + hitDragging.emitter.on('hitupdate', _this.handleHitUpdate); + hitDragging.emitter.on('pointerup', _this.handlePointerUp); + hitDragging.emitter.on('dragend', _this.handleDragEnd); + return _this; + } + EventDragging.prototype.destroy = function () { + this.dragging.destroy(); + }; + // render a drag state on the next receivingCalendar + EventDragging.prototype.displayDrag = function (nextCalendar, state) { + var initialCalendar = this.component.calendar; + var prevCalendar = this.receivingCalendar; + // does the previous calendar need to be cleared? + if (prevCalendar && prevCalendar !== nextCalendar) { + // does the initial calendar need to be cleared? + // if so, don't clear all the way. we still need to to hide the affectedEvents + if (prevCalendar === initialCalendar) { + prevCalendar.dispatch({ + type: 'SET_EVENT_DRAG', + state: { + affectedEvents: state.affectedEvents, + mutatedEvents: core.createEmptyEventStore(), + isEvent: true, + origSeg: state.origSeg + } + }); + // completely clear the old calendar if it wasn't the initial + } + else { + prevCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + } + if (nextCalendar) { + nextCalendar.dispatch({ type: 'SET_EVENT_DRAG', state: state }); + } + }; + EventDragging.prototype.clearDrag = function () { + var initialCalendar = this.component.calendar; + var receivingCalendar = this.receivingCalendar; + if (receivingCalendar) { + receivingCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + // the initial calendar might have an dummy drag state from displayDrag + if (initialCalendar !== receivingCalendar) { + initialCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + }; + EventDragging.prototype.cleanup = function () { + this.subjectSeg = null; + this.isDragging = false; + this.eventRange = null; + this.relevantEvents = null; + this.receivingCalendar = null; + this.validMutation = null; + this.mutatedRelevantEvents = null; + }; + EventDragging.SELECTOR = '.fc-draggable, .fc-resizable'; // TODO: test this in IE11 + return EventDragging; + }(core.Interaction)); + function computeEventMutation(hit0, hit1, massagers) { + var dateSpan0 = hit0.dateSpan; + var dateSpan1 = hit1.dateSpan; + var date0 = dateSpan0.range.start; + var date1 = dateSpan1.range.start; + var standardProps = {}; + if (dateSpan0.allDay !== dateSpan1.allDay) { + standardProps.allDay = dateSpan1.allDay; + standardProps.hasEnd = hit1.component.opt('allDayMaintainDuration'); + if (dateSpan1.allDay) { + // means date1 is already start-of-day, + // but date0 needs to be converted + date0 = core.startOfDay(date0); + } + } + var delta = core.diffDates(date0, date1, hit0.component.dateEnv, hit0.component === hit1.component ? + hit0.component.largeUnit : + null); + if (delta.milliseconds) { // has hours/minutes/seconds + standardProps.allDay = false; + } + var mutation = { + startDelta: delta, + endDelta: delta, + standardProps: standardProps + }; + for (var _i = 0, massagers_1 = massagers; _i < massagers_1.length; _i++) { + var massager = massagers_1[_i]; + massager(mutation, hit0, hit1); + } + return mutation; + } + function getComponentTouchDelay$1(component) { + var delay = component.opt('eventLongPressDelay'); + if (delay == null) { + delay = component.opt('longPressDelay'); + } + return delay; + } + + var EventDragging$1 = /** @class */ (function (_super) { + __extends(EventDragging, _super); + function EventDragging(settings) { + var _this = _super.call(this, settings) || this; + // internal state + _this.draggingSeg = null; // TODO: rename to resizingSeg? subjectSeg? + _this.eventRange = null; + _this.relevantEvents = null; + _this.validMutation = null; + _this.mutatedRelevantEvents = null; + _this.handlePointerDown = function (ev) { + var component = _this.component; + var seg = _this.querySeg(ev); + var eventRange = _this.eventRange = seg.eventRange; + _this.dragging.minDistance = component.opt('eventDragMinDistance'); + // if touch, need to be working with a selected event + _this.dragging.setIgnoreMove(!_this.component.isValidSegDownEl(ev.origEvent.target) || + (ev.isTouch && _this.component.props.eventSelection !== eventRange.instance.instanceId)); + }; + _this.handleDragStart = function (ev) { + var calendar = _this.component.calendar; + var eventRange = _this.eventRange; + _this.relevantEvents = core.getRelevantEvents(calendar.state.eventStore, _this.eventRange.instance.instanceId); + _this.draggingSeg = _this.querySeg(ev); + calendar.unselect(); + calendar.publiclyTrigger('eventResizeStart', [ + { + el: _this.draggingSeg.el, + event: new core.EventApi(calendar, eventRange.def, eventRange.instance), + jsEvent: ev.origEvent, + view: _this.component.view + } + ]); + }; + _this.handleHitUpdate = function (hit, isFinal, ev) { + var calendar = _this.component.calendar; + var relevantEvents = _this.relevantEvents; + var initialHit = _this.hitDragging.initialHit; + var eventInstance = _this.eventRange.instance; + var mutation = null; + var mutatedRelevantEvents = null; + var isInvalid = false; + var interaction = { + affectedEvents: relevantEvents, + mutatedEvents: core.createEmptyEventStore(), + isEvent: true, + origSeg: _this.draggingSeg + }; + if (hit) { + mutation = computeMutation(initialHit, hit, ev.subjectEl.classList.contains('fc-start-resizer'), eventInstance.range, calendar.pluginSystem.hooks.eventResizeJoinTransforms); + } + if (mutation) { + mutatedRelevantEvents = core.applyMutationToEventStore(relevantEvents, calendar.eventUiBases, mutation, calendar); + interaction.mutatedEvents = mutatedRelevantEvents; + if (!_this.component.isInteractionValid(interaction)) { + isInvalid = true; + mutation = null; + mutatedRelevantEvents = null; + interaction.mutatedEvents = null; + } + } + if (mutatedRelevantEvents) { + calendar.dispatch({ + type: 'SET_EVENT_RESIZE', + state: interaction + }); + } + else { + calendar.dispatch({ type: 'UNSET_EVENT_RESIZE' }); + } + if (!isInvalid) { + core.enableCursor(); + } + else { + core.disableCursor(); + } + if (!isFinal) { + if (mutation && isHitsEqual(initialHit, hit)) { + mutation = null; + } + _this.validMutation = mutation; + _this.mutatedRelevantEvents = mutatedRelevantEvents; + } + }; + _this.handleDragEnd = function (ev) { + var calendar = _this.component.calendar; + var view = _this.component.view; + var eventDef = _this.eventRange.def; + var eventInstance = _this.eventRange.instance; + var eventApi = new core.EventApi(calendar, eventDef, eventInstance); + var relevantEvents = _this.relevantEvents; + var mutatedRelevantEvents = _this.mutatedRelevantEvents; + calendar.publiclyTrigger('eventResizeStop', [ + { + el: _this.draggingSeg.el, + event: eventApi, + jsEvent: ev.origEvent, + view: view + } + ]); + if (_this.validMutation) { + calendar.dispatch({ + type: 'MERGE_EVENTS', + eventStore: mutatedRelevantEvents + }); + calendar.publiclyTrigger('eventResize', [ + { + el: _this.draggingSeg.el, + startDelta: _this.validMutation.startDelta || core.createDuration(0), + endDelta: _this.validMutation.endDelta || core.createDuration(0), + prevEvent: eventApi, + event: new core.EventApi(// the data AFTER the mutation + calendar, mutatedRelevantEvents.defs[eventDef.defId], eventInstance ? mutatedRelevantEvents.instances[eventInstance.instanceId] : null), + revert: function () { + calendar.dispatch({ + type: 'MERGE_EVENTS', + eventStore: relevantEvents + }); + }, + jsEvent: ev.origEvent, + view: view + } + ]); + } + else { + calendar.publiclyTrigger('_noEventResize'); + } + // reset all internal state + _this.draggingSeg = null; + _this.relevantEvents = null; + _this.validMutation = null; + // okay to keep eventInstance around. useful to set it in handlePointerDown + }; + var component = settings.component; + var dragging = _this.dragging = new FeaturefulElementDragging(component.el); + dragging.pointer.selector = '.fc-resizer'; + dragging.touchScrollAllowed = false; + dragging.autoScroller.isEnabled = component.opt('dragScroll'); + var hitDragging = _this.hitDragging = new HitDragging(_this.dragging, core.interactionSettingsToStore(settings)); + hitDragging.emitter.on('pointerdown', _this.handlePointerDown); + hitDragging.emitter.on('dragstart', _this.handleDragStart); + hitDragging.emitter.on('hitupdate', _this.handleHitUpdate); + hitDragging.emitter.on('dragend', _this.handleDragEnd); + return _this; + } + EventDragging.prototype.destroy = function () { + this.dragging.destroy(); + }; + EventDragging.prototype.querySeg = function (ev) { + return core.getElSeg(core.elementClosest(ev.subjectEl, this.component.fgSegSelector)); + }; + return EventDragging; + }(core.Interaction)); + function computeMutation(hit0, hit1, isFromStart, instanceRange, transforms) { + var dateEnv = hit0.component.dateEnv; + var date0 = hit0.dateSpan.range.start; + var date1 = hit1.dateSpan.range.start; + var delta = core.diffDates(date0, date1, dateEnv, hit0.component.largeUnit); + var props = {}; + for (var _i = 0, transforms_1 = transforms; _i < transforms_1.length; _i++) { + var transform = transforms_1[_i]; + var res = transform(hit0, hit1); + if (res === false) { + return null; + } + else if (res) { + __assign(props, res); + } + } + if (isFromStart) { + if (dateEnv.add(instanceRange.start, delta) < instanceRange.end) { + props.startDelta = delta; + return props; + } + } + else { + if (dateEnv.add(instanceRange.end, delta) > instanceRange.start) { + props.endDelta = delta; + return props; + } + } + return null; + } + + var UnselectAuto = /** @class */ (function () { + function UnselectAuto(calendar) { + var _this = this; + this.isRecentPointerDateSelect = false; // wish we could use a selector to detect date selection, but uses hit system + this.onSelect = function (selectInfo) { + if (selectInfo.jsEvent) { + _this.isRecentPointerDateSelect = true; + } + }; + this.onDocumentPointerUp = function (pev) { + var _a = _this, calendar = _a.calendar, documentPointer = _a.documentPointer; + var state = calendar.state; + // touch-scrolling should never unfocus any type of selection + if (!documentPointer.wasTouchScroll) { + if (state.dateSelection && // an existing date selection? + !_this.isRecentPointerDateSelect // a new pointer-initiated date selection since last onDocumentPointerUp? + ) { + var unselectAuto = calendar.viewOpt('unselectAuto'); + var unselectCancel = calendar.viewOpt('unselectCancel'); + if (unselectAuto && (!unselectAuto || !core.elementClosest(documentPointer.downEl, unselectCancel))) { + calendar.unselect(pev); + } + } + if (state.eventSelection && // an existing event selected? + !core.elementClosest(documentPointer.downEl, EventDragging.SELECTOR) // interaction DIDN'T start on an event + ) { + calendar.dispatch({ type: 'UNSELECT_EVENT' }); + } + } + _this.isRecentPointerDateSelect = false; + }; + this.calendar = calendar; + var documentPointer = this.documentPointer = new PointerDragging(document); + documentPointer.shouldIgnoreMove = true; + documentPointer.shouldWatchScroll = false; + documentPointer.emitter.on('pointerup', this.onDocumentPointerUp); + /* + TODO: better way to know about whether there was a selection with the pointer + */ + calendar.on('select', this.onSelect); + } + UnselectAuto.prototype.destroy = function () { + this.calendar.off('select', this.onSelect); + this.documentPointer.destroy(); + }; + return UnselectAuto; + }()); + + /* + Given an already instantiated draggable object for one-or-more elements, + Interprets any dragging as an attempt to drag an events that lives outside + of a calendar onto a calendar. + */ + var ExternalElementDragging = /** @class */ (function () { + function ExternalElementDragging(dragging, suppliedDragMeta) { + var _this = this; + this.receivingCalendar = null; + this.droppableEvent = null; // will exist for all drags, even if create:false + this.suppliedDragMeta = null; + this.dragMeta = null; + this.handleDragStart = function (ev) { + _this.dragMeta = _this.buildDragMeta(ev.subjectEl); + }; + this.handleHitUpdate = function (hit, isFinal, ev) { + var dragging = _this.hitDragging.dragging; + var receivingCalendar = null; + var droppableEvent = null; + var isInvalid = false; + var interaction = { + affectedEvents: core.createEmptyEventStore(), + mutatedEvents: core.createEmptyEventStore(), + isEvent: _this.dragMeta.create, + origSeg: null + }; + if (hit) { + receivingCalendar = hit.component.calendar; + if (_this.canDropElOnCalendar(ev.subjectEl, receivingCalendar)) { + droppableEvent = computeEventForDateSpan(hit.dateSpan, _this.dragMeta, receivingCalendar); + interaction.mutatedEvents = core.eventTupleToStore(droppableEvent); + isInvalid = !core.isInteractionValid(interaction, receivingCalendar); + if (isInvalid) { + interaction.mutatedEvents = core.createEmptyEventStore(); + droppableEvent = null; + } + } + } + _this.displayDrag(receivingCalendar, interaction); + // show mirror if no already-rendered mirror element OR if we are shutting down the mirror (?) + // TODO: wish we could somehow wait for dispatch to guarantee render + dragging.setMirrorIsVisible(isFinal || !droppableEvent || !document.querySelector('.fc-mirror')); + if (!isInvalid) { + core.enableCursor(); + } + else { + core.disableCursor(); + } + if (!isFinal) { + dragging.setMirrorNeedsRevert(!droppableEvent); + _this.receivingCalendar = receivingCalendar; + _this.droppableEvent = droppableEvent; + } + }; + this.handleDragEnd = function (pev) { + var _a = _this, receivingCalendar = _a.receivingCalendar, droppableEvent = _a.droppableEvent; + _this.clearDrag(); + if (receivingCalendar && droppableEvent) { + var finalHit = _this.hitDragging.finalHit; + var finalView = finalHit.component.view; + var dragMeta = _this.dragMeta; + var arg = receivingCalendar.buildDatePointApi(finalHit.dateSpan); + arg.draggedEl = pev.subjectEl; + arg.jsEvent = pev.origEvent; + arg.view = finalView; + receivingCalendar.publiclyTrigger('drop', [arg]); + if (dragMeta.create) { + receivingCalendar.dispatch({ + type: 'MERGE_EVENTS', + eventStore: core.eventTupleToStore(droppableEvent) + }); + if (pev.isTouch) { + receivingCalendar.dispatch({ + type: 'SELECT_EVENT', + eventInstanceId: droppableEvent.instance.instanceId + }); + } + // signal that an external event landed + receivingCalendar.publiclyTrigger('eventReceive', [ + { + draggedEl: pev.subjectEl, + event: new core.EventApi(receivingCalendar, droppableEvent.def, droppableEvent.instance), + view: finalView + } + ]); + } + } + _this.receivingCalendar = null; + _this.droppableEvent = null; + }; + var hitDragging = this.hitDragging = new HitDragging(dragging, core.interactionSettingsStore); + hitDragging.requireInitial = false; // will start outside of a component + hitDragging.emitter.on('dragstart', this.handleDragStart); + hitDragging.emitter.on('hitupdate', this.handleHitUpdate); + hitDragging.emitter.on('dragend', this.handleDragEnd); + this.suppliedDragMeta = suppliedDragMeta; + } + ExternalElementDragging.prototype.buildDragMeta = function (subjectEl) { + if (typeof this.suppliedDragMeta === 'object') { + return core.parseDragMeta(this.suppliedDragMeta); + } + else if (typeof this.suppliedDragMeta === 'function') { + return core.parseDragMeta(this.suppliedDragMeta(subjectEl)); + } + else { + return getDragMetaFromEl(subjectEl); + } + }; + ExternalElementDragging.prototype.displayDrag = function (nextCalendar, state) { + var prevCalendar = this.receivingCalendar; + if (prevCalendar && prevCalendar !== nextCalendar) { + prevCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + if (nextCalendar) { + nextCalendar.dispatch({ type: 'SET_EVENT_DRAG', state: state }); + } + }; + ExternalElementDragging.prototype.clearDrag = function () { + if (this.receivingCalendar) { + this.receivingCalendar.dispatch({ type: 'UNSET_EVENT_DRAG' }); + } + }; + ExternalElementDragging.prototype.canDropElOnCalendar = function (el, receivingCalendar) { + var dropAccept = receivingCalendar.opt('dropAccept'); + if (typeof dropAccept === 'function') { + return dropAccept(el); + } + else if (typeof dropAccept === 'string' && dropAccept) { + return Boolean(core.elementMatches(el, dropAccept)); + } + return true; + }; + return ExternalElementDragging; + }()); + // Utils for computing event store from the DragMeta + // ---------------------------------------------------------------------------------------------------- + function computeEventForDateSpan(dateSpan, dragMeta, calendar) { + var defProps = __assign({}, dragMeta.leftoverProps); + for (var _i = 0, _a = calendar.pluginSystem.hooks.externalDefTransforms; _i < _a.length; _i++) { + var transform = _a[_i]; + __assign(defProps, transform(dateSpan, dragMeta)); + } + var def = core.parseEventDef(defProps, dragMeta.sourceId, dateSpan.allDay, calendar.opt('forceEventDuration') || Boolean(dragMeta.duration), // hasEnd + calendar); + var start = dateSpan.range.start; + // only rely on time info if drop zone is all-day, + // otherwise, we already know the time + if (dateSpan.allDay && dragMeta.startTime) { + start = calendar.dateEnv.add(start, dragMeta.startTime); + } + var end = dragMeta.duration ? + calendar.dateEnv.add(start, dragMeta.duration) : + calendar.getDefaultEventEnd(dateSpan.allDay, start); + var instance = core.createEventInstance(def.defId, { start: start, end: end }); + return { def: def, instance: instance }; + } + // Utils for extracting data from element + // ---------------------------------------------------------------------------------------------------- + function getDragMetaFromEl(el) { + var str = getEmbeddedElData(el, 'event'); + var obj = str ? + JSON.parse(str) : + { create: false }; // if no embedded data, assume no event creation + return core.parseDragMeta(obj); + } + core.config.dataAttrPrefix = ''; + function getEmbeddedElData(el, name) { + var prefix = core.config.dataAttrPrefix; + var prefixedName = (prefix ? prefix + '-' : '') + name; + return el.getAttribute('data-' + prefixedName) || ''; + } + + /* + Makes an element (that is *external* to any calendar) draggable. + Can pass in data that determines how an event will be created when dropped onto a calendar. + Leverages FullCalendar's internal drag-n-drop functionality WITHOUT a third-party drag system. + */ + var ExternalDraggable = /** @class */ (function () { + function ExternalDraggable(el, settings) { + var _this = this; + if (settings === void 0) { settings = {}; } + this.handlePointerDown = function (ev) { + var dragging = _this.dragging; + var _a = _this.settings, minDistance = _a.minDistance, longPressDelay = _a.longPressDelay; + dragging.minDistance = + minDistance != null ? + minDistance : + (ev.isTouch ? 0 : core.globalDefaults.eventDragMinDistance); + dragging.delay = + ev.isTouch ? // TODO: eventually read eventLongPressDelay instead vvv + (longPressDelay != null ? longPressDelay : core.globalDefaults.longPressDelay) : + 0; + }; + this.handleDragStart = function (ev) { + if (ev.isTouch && + _this.dragging.delay && + ev.subjectEl.classList.contains('fc-event')) { + _this.dragging.mirror.getMirrorEl().classList.add('fc-selected'); + } + }; + this.settings = settings; + var dragging = this.dragging = new FeaturefulElementDragging(el); + dragging.touchScrollAllowed = false; + if (settings.itemSelector != null) { + dragging.pointer.selector = settings.itemSelector; + } + if (settings.appendTo != null) { + dragging.mirror.parentNode = settings.appendTo; // TODO: write tests + } + dragging.emitter.on('pointerdown', this.handlePointerDown); + dragging.emitter.on('dragstart', this.handleDragStart); + new ExternalElementDragging(dragging, settings.eventData); + } + ExternalDraggable.prototype.destroy = function () { + this.dragging.destroy(); + }; + return ExternalDraggable; + }()); + + /* + Detects when a *THIRD-PARTY* drag-n-drop system interacts with elements. + The third-party system is responsible for drawing the visuals effects of the drag. + This class simply monitors for pointer movements and fires events. + It also has the ability to hide the moving element (the "mirror") during the drag. + */ + var InferredElementDragging = /** @class */ (function (_super) { + __extends(InferredElementDragging, _super); + function InferredElementDragging(containerEl) { + var _this = _super.call(this, containerEl) || this; + _this.shouldIgnoreMove = false; + _this.mirrorSelector = ''; + _this.currentMirrorEl = null; + _this.handlePointerDown = function (ev) { + _this.emitter.trigger('pointerdown', ev); + if (!_this.shouldIgnoreMove) { + // fire dragstart right away. does not support delay or min-distance + _this.emitter.trigger('dragstart', ev); + } + }; + _this.handlePointerMove = function (ev) { + if (!_this.shouldIgnoreMove) { + _this.emitter.trigger('dragmove', ev); + } + }; + _this.handlePointerUp = function (ev) { + _this.emitter.trigger('pointerup', ev); + if (!_this.shouldIgnoreMove) { + // fire dragend right away. does not support a revert animation + _this.emitter.trigger('dragend', ev); + } + }; + var pointer = _this.pointer = new PointerDragging(containerEl); + pointer.emitter.on('pointerdown', _this.handlePointerDown); + pointer.emitter.on('pointermove', _this.handlePointerMove); + pointer.emitter.on('pointerup', _this.handlePointerUp); + return _this; + } + InferredElementDragging.prototype.destroy = function () { + this.pointer.destroy(); + }; + InferredElementDragging.prototype.setIgnoreMove = function (bool) { + this.shouldIgnoreMove = bool; + }; + InferredElementDragging.prototype.setMirrorIsVisible = function (bool) { + if (bool) { + // restore a previously hidden element. + // use the reference in case the selector class has already been removed. + if (this.currentMirrorEl) { + this.currentMirrorEl.style.visibility = ''; + this.currentMirrorEl = null; + } + } + else { + var mirrorEl = this.mirrorSelector ? + document.querySelector(this.mirrorSelector) : + null; + if (mirrorEl) { + this.currentMirrorEl = mirrorEl; + mirrorEl.style.visibility = 'hidden'; + } + } + }; + return InferredElementDragging; + }(core.ElementDragging)); + + /* + Bridges third-party drag-n-drop systems with FullCalendar. + Must be instantiated and destroyed by caller. + */ + var ThirdPartyDraggable = /** @class */ (function () { + function ThirdPartyDraggable(containerOrSettings, settings) { + var containerEl = document; + if ( + // wish we could just test instanceof EventTarget, but doesn't work in IE11 + containerOrSettings === document || + containerOrSettings instanceof Element) { + containerEl = containerOrSettings; + settings = settings || {}; + } + else { + settings = (containerOrSettings || {}); + } + var dragging = this.dragging = new InferredElementDragging(containerEl); + if (typeof settings.itemSelector === 'string') { + dragging.pointer.selector = settings.itemSelector; + } + else if (containerEl === document) { + dragging.pointer.selector = '[data-event]'; + } + if (typeof settings.mirrorSelector === 'string') { + dragging.mirrorSelector = settings.mirrorSelector; + } + new ExternalElementDragging(dragging, settings.eventData); + } + ThirdPartyDraggable.prototype.destroy = function () { + this.dragging.destroy(); + }; + return ThirdPartyDraggable; + }()); + + var main = core.createPlugin({ + componentInteractions: [DateClicking, DateSelecting, EventDragging, EventDragging$1], + calendarInteractions: [UnselectAuto], + elementDraggingImpl: FeaturefulElementDragging + }); + + exports.Draggable = ExternalDraggable; + exports.FeaturefulElementDragging = FeaturefulElementDragging; + exports.PointerDragging = PointerDragging; + exports.ThirdPartyDraggable = ThirdPartyDraggable; + exports.default = main; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/library/fullcalendar/packages/interaction/main.min.js b/library/fullcalendar/packages/interaction/main.min.js new file mode 100644 index 000000000..58c189c8a --- /dev/null +++ b/library/fullcalendar/packages/interaction/main.min.js @@ -0,0 +1,21 @@ +/*! +FullCalendar Interaction Plugin v4.0.2 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@fullcalendar/core")):"function"==typeof define&&define.amd?define(["exports","@fullcalendar/core"],t):(e=e||self,t(e.FullCalendarInteraction={},e.FullCalendar))}(this,function(e,t){"use strict";function n(e,t){function n(){this.constructor=e}m(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}function r(e){return 0===e.button&&!e.ctrlKey}function i(){y++,setTimeout(function(){y--},t.config.touchMouseIgnoreWait)}function o(){D++||window.addEventListener("touchmove",l,{passive:!1})}function a(){--D||window.removeEventListener("touchmove",l,{passive:!1})}function l(e){w&&e.preventDefault()}function s(e){var t=e.tagName;return"HTML"===t||"BODY"===t}function c(e,n){return!e&&!n||Boolean(e)===Boolean(n)&&t.isDateSpansEqual(e.dateSpan,n.dateSpan)}function d(e){var t=e.opt("selectLongPressDelay");return null==t&&(t=e.opt("longPressDelay")),t}function u(e,n,r){var i=e.dateSpan,o=n.dateSpan,a=[i.range.start,i.range.end,o.range.start,o.range.end];a.sort(t.compareNumbers);for(var l={},s=0,c=r;si.start)return d.endDelta=c,d;return null}function v(e,n,r){for(var i=S({},n.leftoverProps),o=0,a=r.pluginSystem.hooks.externalDefTransforms;o0&&(this.everMovedDown=!0),o<0?this.everMovedLeft=!0:o>0&&(this.everMovedRight=!0),this.pointerScreenX=n,this.pointerScreenY=r,this.isAnimating||(this.isAnimating=!0,this.requestAnimation(I()))}},e.prototype.stop=function(){if(this.isEnabled){this.isAnimating=!1;for(var e=0,t=this.scrollCaches;e=0&&c>=0&&d>=0&&u>=0&&(d<=n&&this.everMovedUp&&a.canScrollUp()&&(!r||r.distance>d)&&(r={scrollCache:a,name:"top",distance:d}),u<=n&&this.everMovedDown&&a.canScrollDown()&&(!r||r.distance>u)&&(r={scrollCache:a,name:"bottom",distance:u}),s<=n&&this.everMovedLeft&&a.canScrollLeft()&&(!r||r.distance>s)&&(r={scrollCache:a,name:"left",distance:s}),c<=n&&this.everMovedRight&&a.canScrollRight()&&(!r||r.distance>c)&&(r={scrollCache:a,name:"right",distance:c}))}return r},e.prototype.buildCaches=function(){return this.queryScrollEls().map(function(e){return e===window?new R(!1):new C(e,!1)})},e.prototype.queryScrollEls=function(){for(var e=[],t=0,n=this.scrollQuery;t=t*t&&r.handleDistanceSurpassed(e)}r.isDragging&&("scroll"!==e.origEvent.type&&(r.mirror.handleMove(e.pageX,e.pageY),r.autoScroller.handleMove(e.pageX,e.pageY)),r.emitter.trigger("dragmove",e))}},r.onPointerUp=function(e){r.isInteracting&&(r.isInteracting=!1,t.allowSelection(document.body),t.allowContextMenu(document.body),r.emitter.trigger("pointerup",e),r.isDragging&&(r.autoScroller.stop(),r.tryStopDrag(e)),r.delayTimeoutId&&(clearTimeout(r.delayTimeoutId),r.delayTimeoutId=null))};var i=r.pointer=new T(n);return i.emitter.on("pointerdown",r.onPointerDown),i.emitter.on("pointermove",r.onPointerMove),i.emitter.on("pointerup",r.onPointerUp),r.mirror=new M,r.autoScroller=new P,r}return n(r,e),r.prototype.destroy=function(){this.pointer.destroy()},r.prototype.startDelay=function(e){var t=this;"number"==typeof this.delay?this.delayTimeoutId=setTimeout(function(){t.delayTimeoutId=null,t.handleDelayEnd(e)},this.delay):this.handleDelayEnd(e)},r.prototype.handleDelayEnd=function(e){this.isDelayEnded=!0,this.tryStartDrag(e)},r.prototype.handleDistanceSurpassed=function(e){this.isDistanceSurpassed=!0,this.tryStartDrag(e)},r.prototype.tryStartDrag=function(e){this.isDelayEnded&&this.isDistanceSurpassed&&(this.pointer.wasTouchScroll&&!this.touchScrollAllowed||(this.isDragging=!0,this.mirrorNeedsRevert=!1,this.autoScroller.start(e.pageX,e.pageY),this.emitter.trigger("dragstart",e),!1===this.touchScrollAllowed&&this.pointer.cancelTouchScroll()))},r.prototype.tryStopDrag=function(e){this.mirror.stop(this.mirrorNeedsRevert,this.stopDrag.bind(this,e))},r.prototype.stopDrag=function(e){this.isDragging=!1,this.emitter.trigger("dragend",e)},r.prototype.setIgnoreMove=function(e){this.pointer.shouldIgnoreMove=e},r.prototype.setMirrorIsVisible=function(e){this.mirror.setIsVisible(e)},r.prototype.setMirrorNeedsRevert=function(e){this.mirrorNeedsRevert=e},r.prototype.setAutoScrollEnabled=function(e){this.autoScroller.isEnabled=e},r}(t.ElementDragging),j=function(){function e(e){this.origRect=t.computeRect(e),this.scrollCaches=t.getClippingParents(e).map(function(e){return new C(e,!0)})}return e.prototype.destroy=function(){for(var e=0,t=this.scrollCaches;e=0&&g=0&&ha.layer)||(E.rect.left+=d,E.rect.right+=d,E.rect.top+=u,E.rect.bottom+=u,a=E)}}}return a},e}(),H=function(e){function r(n){var r=e.call(this,n)||this;r.handlePointerDown=function(e){var t=r.dragging;t.setIgnoreMove(!r.component.isValidDateDownEl(t.pointer.downEl))},r.handleDragEnd=function(e){var t=r.component;if(!r.dragging.pointer.wasTouchScroll){var n=r.hitDragging,i=n.initialHit,o=n.finalHit;i&&o&&c(i,o)&&t.calendar.triggerDateClick(i.dateSpan,i.dayEl,t.view,e.origEvent)}};var i=n.component;r.dragging=new L(i.el),r.dragging.autoScroller.isEnabled=!1;var o=r.hitDragging=new A(r.dragging,t.interactionSettingsToStore(n));return o.emitter.on("pointerdown",r.handlePointerDown),o.emitter.on("dragend",r.handleDragEnd),r}return n(r,e),r.prototype.destroy=function(){this.dragging.destroy()},r}(t.Interaction),N=function(e){function r(n){var r=e.call(this,n)||this;r.dragSelection=null,r.handlePointerDown=function(e){var t=r,n=t.component,i=t.dragging,o=n.opt("selectable")&&n.isValidDateDownEl(e.origEvent.target);i.setIgnoreMove(!o),i.delay=e.isTouch?d(n):null},r.handleDragStart=function(e){r.component.calendar.unselect(e)},r.handleHitUpdate=function(e,n){var i=r.component.calendar,o=null,a=!1;e&&((o=u(r.hitDragging.initialHit,e,i.pluginSystem.hooks.dateSelectionTransformers))&&r.component.isDateSelectionValid(o)||(a=!0,o=null)),o?i.dispatch({type:"SELECT_DATES",selection:o}):n||i.dispatch({type:"UNSELECT_DATES"}),a?t.disableCursor():t.enableCursor(),n||(r.dragSelection=o)},r.handlePointerUp=function(e){r.dragSelection&&(r.component.calendar.triggerDateSelect(r.dragSelection,e),r.dragSelection=null)};var i=n.component,o=r.dragging=new L(i.el);o.touchScrollAllowed=!1,o.minDistance=i.opt("selectMinDistance")||0,o.autoScroller.isEnabled=i.opt("dragScroll");var a=r.hitDragging=new A(r.dragging,t.interactionSettingsToStore(n));return a.emitter.on("pointerdown",r.handlePointerDown),a.emitter.on("dragstart",r.handleDragStart),a.emitter.on("hitupdate",r.handleHitUpdate),a.emitter.on("pointerup",r.handlePointerUp),r}return n(r,e),r.prototype.destroy=function(){this.dragging.destroy()},r}(t.Interaction),V=function(e){function r(n){var i=e.call(this,n)||this;i.subjectSeg=null,i.isDragging=!1,i.eventRange=null,i.relevantEvents=null,i.receivingCalendar=null,i.validMutation=null,i.mutatedRelevantEvents=null,i.handlePointerDown=function(e){var n=e.origEvent.target,r=i,o=r.component,a=r.dragging,l=a.mirror,s=o.calendar,c=i.subjectSeg=t.getElSeg(e.subjectEl),d=i.eventRange=c.eventRange,u=d.instance.instanceId;i.relevantEvents=t.getRelevantEvents(s.state.eventStore,u),a.minDistance=e.isTouch?0:o.opt("eventDragMinDistance"),a.delay=e.isTouch&&u!==o.props.eventSelection?h(o):null,l.parentNode=s.el,l.revertDuration=o.opt("dragRevertDuration");var g=o.isValidSegDownEl(n)&&!t.elementClosest(n,".fc-resizer");a.setIgnoreMove(!g),i.isDragging=g&&e.subjectEl.classList.contains("fc-draggable")},i.handleDragStart=function(e){var n=i.component.calendar,r=i.eventRange,o=r.instance.instanceId;e.isTouch?o!==i.component.props.eventSelection&&n.dispatch({type:"SELECT_EVENT",eventInstanceId:o}):n.dispatch({type:"UNSELECT_EVENT"}),i.isDragging&&(n.unselect(e),n.publiclyTrigger("eventDragStart",[{el:i.subjectSeg.el,event:new t.EventApi(n,r.def,r.instance),jsEvent:e.origEvent,view:i.component.view}]))},i.handleHitUpdate=function(e,n){if(i.isDragging){var r=i.relevantEvents,o=i.hitDragging.initialHit,a=i.component.calendar,l=null,s=null,d=null,u=!1,h={affectedEvents:r,mutatedEvents:t.createEmptyEventStore(),isEvent:!0,origSeg:i.subjectSeg};if(e){var p=e.component;l=p.calendar,a===l||p.opt("editable")&&p.opt("droppable")?(s=g(o,e,l.pluginSystem.hooks.eventDragMutationMassagers))&&(d=t.applyMutationToEventStore(r,l.eventUiBases,s,l),h.mutatedEvents=d,p.isInteractionValid(h)||(u=!0,s=null,d=null,h.mutatedEvents=t.createEmptyEventStore())):l=null}i.displayDrag(l,h),u?t.disableCursor():t.enableCursor(),n||(a===l&&c(o,e)&&(s=null),i.dragging.setMirrorNeedsRevert(!s),i.dragging.setMirrorIsVisible(!e||!document.querySelector(".fc-mirror")),i.receivingCalendar=l,i.validMutation=s,i.mutatedRelevantEvents=d)}},i.handlePointerUp=function(){i.isDragging||i.cleanup()},i.handleDragEnd=function(e){if(i.isDragging){var n=i.component.calendar,r=i.component.view,o=i.receivingCalendar,a=i.eventRange.def,l=i.eventRange.instance,s=new t.EventApi(n,a,l),c=i.relevantEvents,d=i.mutatedRelevantEvents,u=i.hitDragging.finalHit;if(i.clearDrag(),n.publiclyTrigger("eventDragStop",[{el:i.subjectSeg.el,event:s,jsEvent:e.origEvent,view:r}]),i.validMutation){if(o===n){n.dispatch({type:"MERGE_EVENTS",eventStore:d});for(var g={},h=0,p=n.pluginSystem.hooks.eventDropTransformers;h tag */ + text-decoration: none; + color: inherit; } + +.fc-list-item-title a[href]:hover { + /* hover effect only on titles with hrefs */ + text-decoration: underline; } + +/* message when no events */ +.fc-list-empty-wrap2 { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; } + +.fc-list-empty-wrap1 { + width: 100%; + height: 100%; + display: table; } + +.fc-list-empty { + display: table-cell; + vertical-align: middle; + text-align: center; } + +.fc-unthemed .fc-list-empty { + /* theme will provide own background */ + background-color: #eee; } diff --git a/library/fullcalendar/packages/list/main.js b/library/fullcalendar/packages/list/main.js new file mode 100644 index 000000000..abd7c1192 --- /dev/null +++ b/library/fullcalendar/packages/list/main.js @@ -0,0 +1,341 @@ +/*! +FullCalendar List View Plugin v4.0.2 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('@fullcalendar/core')) : + typeof define === 'function' && define.amd ? define(['exports', '@fullcalendar/core'], factory) : + (global = global || self, factory(global.FullCalendarList = {}, global.FullCalendar)); +}(this, function (exports, core) { 'use strict'; + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. All rights reserved. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of the + License at http://www.apache.org/licenses/LICENSE-2.0 + + THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED + WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, + MERCHANTABLITY OR NON-INFRINGEMENT. + + See the Apache Version 2.0 License for specific language governing permissions + and limitations under the License. + ***************************************************************************** */ + /* global Reflect, Promise */ + + var extendStatics = function(d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + + function __extends(d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + } + + var ListEventRenderer = /** @class */ (function (_super) { + __extends(ListEventRenderer, _super); + function ListEventRenderer(listView) { + var _this = _super.call(this, listView.context) || this; + _this.listView = listView; + return _this; + } + ListEventRenderer.prototype.attachSegs = function (segs) { + if (!segs.length) { + this.listView.renderEmptyMessage(); + } + else { + this.listView.renderSegList(segs); + } + }; + ListEventRenderer.prototype.detachSegs = function () { + }; + // generates the HTML for a single event row + ListEventRenderer.prototype.renderSegHtml = function (seg) { + var _a = this.context, view = _a.view, theme = _a.theme; + var eventRange = seg.eventRange; + var eventDef = eventRange.def; + var eventInstance = eventRange.instance; + var eventUi = eventRange.ui; + var url = eventDef.url; + var classes = ['fc-list-item'].concat(eventUi.classNames); + var bgColor = eventUi.backgroundColor; + var timeHtml; + if (eventDef.allDay) { + timeHtml = core.getAllDayHtml(view); + } + else if (core.isMultiDayRange(eventRange.range)) { + if (seg.isStart) { + timeHtml = core.htmlEscape(this._getTimeText(eventInstance.range.start, seg.end, false // allDay + )); + } + else if (seg.isEnd) { + timeHtml = core.htmlEscape(this._getTimeText(seg.start, eventInstance.range.end, false // allDay + )); + } + else { // inner segment that lasts the whole day + timeHtml = core.getAllDayHtml(view); + } + } + else { + // Display the normal time text for the *event's* times + timeHtml = core.htmlEscape(this.getTimeText(eventRange)); + } + if (url) { + classes.push('fc-has-url'); + } + return '' + + (this.displayEventTime ? + '' + + (timeHtml || '') + + '' : + '') + + '' + + '' + + '' + + '' + + '' + + core.htmlEscape(eventDef.title || '') + + '' + + '' + + ''; + }; + // like "4:00am" + ListEventRenderer.prototype.computeEventTimeFormat = function () { + return { + hour: 'numeric', + minute: '2-digit', + meridiem: 'short' + }; + }; + return ListEventRenderer; + }(core.FgEventRenderer)); + + /* + Responsible for the scroller, and forwarding event-related actions into the "grid". + */ + var ListView = /** @class */ (function (_super) { + __extends(ListView, _super); + function ListView(context, viewSpec, dateProfileGenerator, parentEl) { + var _this = _super.call(this, context, viewSpec, dateProfileGenerator, parentEl) || this; + _this.computeDateVars = core.memoize(computeDateVars); + _this.eventStoreToSegs = core.memoize(_this._eventStoreToSegs); + var eventRenderer = _this.eventRenderer = new ListEventRenderer(_this); + _this.renderContent = core.memoizeRendering(eventRenderer.renderSegs.bind(eventRenderer), eventRenderer.unrender.bind(eventRenderer)); + _this.el.classList.add('fc-list-view'); + var listViewClassNames = (_this.theme.getClass('listView') || '').split(' '); // wish we didn't have to do this + for (var _i = 0, listViewClassNames_1 = listViewClassNames; _i < listViewClassNames_1.length; _i++) { + var listViewClassName = listViewClassNames_1[_i]; + if (listViewClassName) { // in case input was empty string + _this.el.classList.add(listViewClassName); + } + } + _this.scroller = new core.ScrollComponent('hidden', // overflow x + 'auto' // overflow y + ); + _this.el.appendChild(_this.scroller.el); + _this.contentEl = _this.scroller.el; // shortcut + context.calendar.registerInteractiveComponent(_this, { + el: _this.el + // TODO: make aware that it doesn't do Hits + }); + return _this; + } + ListView.prototype.render = function (props) { + var _a = this.computeDateVars(props.dateProfile), dayDates = _a.dayDates, dayRanges = _a.dayRanges; + this.dayDates = dayDates; + this.renderContent(this.eventStoreToSegs(props.eventStore, props.eventUiBases, dayRanges)); + }; + ListView.prototype.destroy = function () { + _super.prototype.destroy.call(this); + this.scroller.destroy(); // will remove the Grid too + this.calendar.unregisterInteractiveComponent(this); + }; + ListView.prototype.updateSize = function (isResize, viewHeight, isAuto) { + _super.prototype.updateSize.call(this, isResize, viewHeight, isAuto); + this.eventRenderer.computeSizes(isResize); + this.eventRenderer.assignSizes(isResize); + this.scroller.clear(); // sets height to 'auto' and clears overflow + if (!isAuto) { + this.scroller.setHeight(this.computeScrollerHeight(viewHeight)); + } + }; + ListView.prototype.computeScrollerHeight = function (viewHeight) { + return viewHeight - + core.subtractInnerElHeight(this.el, this.scroller.el); // everything that's NOT the scroller + }; + ListView.prototype._eventStoreToSegs = function (eventStore, eventUiBases, dayRanges) { + return this.eventRangesToSegs(core.sliceEventStore(eventStore, eventUiBases, this.props.dateProfile.activeRange, this.nextDayThreshold).fg, dayRanges); + }; + ListView.prototype.eventRangesToSegs = function (eventRanges, dayRanges) { + var segs = []; + for (var _i = 0, eventRanges_1 = eventRanges; _i < eventRanges_1.length; _i++) { + var eventRange = eventRanges_1[_i]; + segs.push.apply(segs, this.eventRangeToSegs(eventRange, dayRanges)); + } + return segs; + }; + ListView.prototype.eventRangeToSegs = function (eventRange, dayRanges) { + var _a = this, dateEnv = _a.dateEnv, nextDayThreshold = _a.nextDayThreshold; + var range = eventRange.range; + var allDay = eventRange.def.allDay; + var dayIndex; + var segRange; + var seg; + var segs = []; + for (dayIndex = 0; dayIndex < dayRanges.length; dayIndex++) { + segRange = core.intersectRanges(range, dayRanges[dayIndex]); + if (segRange) { + seg = { + component: this, + eventRange: eventRange, + start: segRange.start, + end: segRange.end, + isStart: eventRange.isStart && segRange.start.valueOf() === range.start.valueOf(), + isEnd: eventRange.isEnd && segRange.end.valueOf() === range.end.valueOf(), + dayIndex: dayIndex + }; + segs.push(seg); + // detect when range won't go fully into the next day, + // and mutate the latest seg to the be the end. + if (!seg.isEnd && !allDay && + dayIndex + 1 < dayRanges.length && + range.end < + dateEnv.add(dayRanges[dayIndex + 1].start, nextDayThreshold)) { + seg.end = range.end; + seg.isEnd = true; + break; + } + } + } + return segs; + }; + ListView.prototype.renderEmptyMessage = function () { + this.contentEl.innerHTML = + '
' + // TODO: try less wraps + '
' + + '
' + + core.htmlEscape(this.opt('noEventsMessage')) + + '
' + + '
' + + '
'; + }; + // called by ListEventRenderer + ListView.prototype.renderSegList = function (allSegs) { + var segsByDay = this.groupSegsByDay(allSegs); // sparse array + var dayIndex; + var daySegs; + var i; + var tableEl = core.htmlToElement('
'); + var tbodyEl = tableEl.querySelector('tbody'); + for (dayIndex = 0; dayIndex < segsByDay.length; dayIndex++) { + daySegs = segsByDay[dayIndex]; + if (daySegs) { // sparse array, so might be undefined + // append a day header + tbodyEl.appendChild(this.buildDayHeaderRow(this.dayDates[dayIndex])); + daySegs = this.eventRenderer.sortEventSegs(daySegs); + for (i = 0; i < daySegs.length; i++) { + tbodyEl.appendChild(daySegs[i].el); // append event row + } + } + } + this.contentEl.innerHTML = ''; + this.contentEl.appendChild(tableEl); + }; + // Returns a sparse array of arrays, segs grouped by their dayIndex + ListView.prototype.groupSegsByDay = function (segs) { + var segsByDay = []; // sparse array + var i; + var seg; + for (i = 0; i < segs.length; i++) { + seg = segs[i]; + (segsByDay[seg.dayIndex] || (segsByDay[seg.dayIndex] = [])) + .push(seg); + } + return segsByDay; + }; + // generates the HTML for the day headers that live amongst the event rows + ListView.prototype.buildDayHeaderRow = function (dayDate) { + var dateEnv = this.dateEnv; + var mainFormat = core.createFormatter(this.opt('listDayFormat')); // TODO: cache + var altFormat = core.createFormatter(this.opt('listDayAltFormat')); // TODO: cache + return core.createElement('tr', { + className: 'fc-list-heading', + 'data-date': dateEnv.formatIso(dayDate, { omitTime: true }) + }, '' + + (mainFormat ? + core.buildGotoAnchorHtml(this, dayDate, { 'class': 'fc-list-heading-main' }, core.htmlEscape(dateEnv.format(dayDate, mainFormat)) // inner HTML + ) : + '') + + (altFormat ? + core.buildGotoAnchorHtml(this, dayDate, { 'class': 'fc-list-heading-alt' }, core.htmlEscape(dateEnv.format(dayDate, altFormat)) // inner HTML + ) : + '') + + ''); + }; + return ListView; + }(core.View)); + ListView.prototype.fgSegSelector = '.fc-list-item'; // which elements accept event actions + function computeDateVars(dateProfile) { + var dayStart = core.startOfDay(dateProfile.renderRange.start); + var viewEnd = dateProfile.renderRange.end; + var dayDates = []; + var dayRanges = []; + while (dayStart < viewEnd) { + dayDates.push(dayStart); + dayRanges.push({ + start: dayStart, + end: core.addDays(dayStart, 1) + }); + dayStart = core.addDays(dayStart, 1); + } + return { dayDates: dayDates, dayRanges: dayRanges }; + } + + var main = core.createPlugin({ + views: { + list: { + class: ListView, + buttonTextKey: 'list', + listDayFormat: { month: 'long', day: 'numeric', year: 'numeric' } // like "January 1, 2016" + }, + listDay: { + type: 'list', + duration: { days: 1 }, + listDayFormat: { weekday: 'long' } // day-of-week is all we need. full date is probably in header + }, + listWeek: { + type: 'list', + duration: { weeks: 1 }, + listDayFormat: { weekday: 'long' }, + listDayAltFormat: { month: 'long', day: 'numeric', year: 'numeric' } + }, + listMonth: { + type: 'list', + duration: { month: 1 }, + listDayAltFormat: { weekday: 'long' } // day-of-week is nice-to-have + }, + listYear: { + type: 'list', + duration: { year: 1 }, + listDayAltFormat: { weekday: 'long' } // day-of-week is nice-to-have + } + } + }); + + exports.ListView = ListView; + exports.default = main; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/library/fullcalendar/packages/list/main.min.css b/library/fullcalendar/packages/list/main.min.css new file mode 100644 index 000000000..6a9c9101d --- /dev/null +++ b/library/fullcalendar/packages/list/main.min.css @@ -0,0 +1,5 @@ +/*! +FullCalendar List View Plugin v4.0.2 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/.fc-event-dot{display:inline-block;width:10px;height:10px;border-radius:5px}.fc-rtl .fc-list-view{direction:rtl}.fc-list-view{border-width:1px;border-style:solid}.fc .fc-list-table{table-layout:auto}.fc-list-table td{border-width:1px 0 0;padding:8px 14px}.fc-list-table tr:first-child td{border-top-width:0}.fc-list-heading{border-bottom-width:1px}.fc-list-heading td{font-weight:700}.fc-ltr .fc-list-heading-main{float:left}.fc-ltr .fc-list-heading-alt,.fc-rtl .fc-list-heading-main{float:right}.fc-rtl .fc-list-heading-alt{float:left}.fc-list-item.fc-has-url{cursor:pointer}.fc-list-item-marker,.fc-list-item-time{white-space:nowrap;width:1px}.fc-ltr .fc-list-item-marker{padding-right:0}.fc-rtl .fc-list-item-marker{padding-left:0}.fc-list-item-title a{text-decoration:none;color:inherit}.fc-list-item-title a[href]:hover{text-decoration:underline}.fc-list-empty-wrap2{position:absolute;top:0;left:0;right:0;bottom:0}.fc-list-empty-wrap1{width:100%;height:100%;display:table}.fc-list-empty{display:table-cell;vertical-align:middle;text-align:center}.fc-unthemed .fc-list-empty{background-color:#eee} \ No newline at end of file diff --git a/library/fullcalendar/packages/list/main.min.js b/library/fullcalendar/packages/list/main.min.js new file mode 100644 index 000000000..3310229fa --- /dev/null +++ b/library/fullcalendar/packages/list/main.min.js @@ -0,0 +1,20 @@ +/*! +FullCalendar List View Plugin v4.0.2 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("@fullcalendar/core")):"function"==typeof define&&define.amd?define(["exports","@fullcalendar/core"],t):(e=e||self,t(e.FullCalendarList={},e.FullCalendar))}(this,function(e,t){"use strict";function n(e,t){function n(){this.constructor=e}s(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}function r(e){for(var n=t.startOfDay(e.renderRange.start),r=e.renderRange.end,s=[],a=[];n'+(this.displayEventTime?''+(n||"")+"":"")+'"+t.htmlEscape(o.title||"")+""},r.prototype.computeEventTimeFormat=function(){return{hour:"numeric",minute:"2-digit",meridiem:"short"}},r}(t.FgEventRenderer),i=function(e){function s(n,s,i,o){var l=e.call(this,n,s,i,o)||this;l.computeDateVars=t.memoize(r),l.eventStoreToSegs=t.memoize(l._eventStoreToSegs);var d=l.eventRenderer=new a(l);l.renderContent=t.memoizeRendering(d.renderSegs.bind(d),d.unrender.bind(d)),l.el.classList.add("fc-list-view");for(var c=(l.theme.getClass("listView")||"").split(" "),p=0,h=c;p
'+t.htmlEscape(this.opt("noEventsMessage"))+"
"},s.prototype.renderSegList=function(e){var n,r,s,a=this.groupSegsByDay(e),i=t.htmlToElement('
'),o=i.querySelector("tbody");for(n=0;n'+(r?t.buildGotoAnchorHtml(this,e,{class:"fc-list-heading-main"},t.htmlEscape(n.format(e,r))):"")+(s?t.buildGotoAnchorHtml(this,e,{class:"fc-list-heading-alt"},t.htmlEscape(n.format(e,s))):"")+"")},s}(t.View);i.prototype.fgSegSelector=".fc-list-item";var o=t.createPlugin({views:{list:{class:i,buttonTextKey:"list",listDayFormat:{month:"long",day:"numeric",year:"numeric"}},listDay:{type:"list",duration:{days:1},listDayFormat:{weekday:"long"}},listWeek:{type:"list",duration:{weeks:1},listDayFormat:{weekday:"long"},listDayAltFormat:{month:"long",day:"numeric",year:"numeric"}},listMonth:{type:"list",duration:{month:1},listDayAltFormat:{weekday:"long"}},listYear:{type:"list",duration:{year:1},listDayAltFormat:{weekday:"long"}}}});e.ListView=i,e.default=o,Object.defineProperty(e,"__esModule",{value:!0})}); \ No newline at end of file diff --git a/library/fullcalendar/packages/luxon/main.js b/library/fullcalendar/packages/luxon/main.js new file mode 100644 index 000000000..ff5fc19a1 --- /dev/null +++ b/library/fullcalendar/packages/luxon/main.js @@ -0,0 +1,162 @@ +/*! +FullCalendar Luxon Plugin v4.0.2 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('luxon'), require('@fullcalendar/core')) : + typeof define === 'function' && define.amd ? define(['exports', 'luxon', '@fullcalendar/core'], factory) : + (global = global || self, factory(global.FullCalendarLuxon = {}, global.luxon, global.FullCalendar)); +}(this, function (exports, luxon, core) { 'use strict'; + + /*! ***************************************************************************** + Copyright (c) Microsoft Corporation. All rights reserved. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of the + License at http://www.apache.org/licenses/LICENSE-2.0 + + THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED + WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, + MERCHANTABLITY OR NON-INFRINGEMENT. + + See the Apache Version 2.0 License for specific language governing permissions + and limitations under the License. + ***************************************************************************** */ + /* global Reflect, Promise */ + + var extendStatics = function(d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + + function __extends(d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + } + + var __assign = function() { + __assign = Object.assign || function __assign(t) { + for (var s, i = 1, n = arguments.length; i < n; i++) { + s = arguments[i]; + for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; + } + return t; + }; + return __assign.apply(this, arguments); + }; + + function toDateTime(date, calendar) { + if (!(calendar instanceof core.Calendar)) { + throw new Error('must supply a Calendar instance'); + } + return luxon.DateTime.fromJSDate(date, { + zone: calendar.dateEnv.timeZone, + locale: calendar.dateEnv.locale.codes[0] + }); + } + function toDuration(duration, calendar) { + if (!(calendar instanceof core.Calendar)) { + throw new Error('must supply a Calendar instance'); + } + return luxon.Duration.fromObject(__assign({}, duration, { locale: calendar.dateEnv.locale.codes[0] })); + } + var LuxonNamedTimeZone = /** @class */ (function (_super) { + __extends(LuxonNamedTimeZone, _super); + function LuxonNamedTimeZone() { + return _super !== null && _super.apply(this, arguments) || this; + } + LuxonNamedTimeZone.prototype.offsetForArray = function (a) { + return arrayToLuxon(a, this.timeZoneName).offset; + }; + LuxonNamedTimeZone.prototype.timestampToArray = function (ms) { + return luxonToArray(luxon.DateTime.fromMillis(ms, { + zone: this.timeZoneName + })); + }; + return LuxonNamedTimeZone; + }(core.NamedTimeZoneImpl)); + function formatWithCmdStr(cmdStr, arg) { + var cmd = parseCmdStr(cmdStr); + if (arg.end) { + var start = arrayToLuxon(arg.start.array, arg.timeZone, arg.localeCodes[0]); + var end = arrayToLuxon(arg.end.array, arg.timeZone, arg.localeCodes[0]); + return formatRange(cmd, start.toFormat.bind(start), end.toFormat.bind(end), arg.separator); + } + return arrayToLuxon(arg.date.array, arg.timeZone, arg.localeCodes[0]).toFormat(cmd.whole); + } + var main = core.createPlugin({ + cmdFormatter: formatWithCmdStr, + namedTimeZonedImpl: LuxonNamedTimeZone + }); + function luxonToArray(datetime) { + return [ + datetime.year, + datetime.month - 1, + datetime.day, + datetime.hour, + datetime.minute, + datetime.second, + datetime.millisecond + ]; + } + function arrayToLuxon(arr, timeZone, locale) { + return luxon.DateTime.fromObject({ + zone: timeZone, + locale: locale, + year: arr[0], + month: arr[1] + 1, + day: arr[2], + hour: arr[3], + minute: arr[4], + second: arr[5], + millisecond: arr[6] + }); + } + function parseCmdStr(cmdStr) { + var parts = cmdStr.match(/^(.*?)\{(.*)\}(.*)$/); // TODO: lookbehinds for escape characters + if (parts) { + var middle = parseCmdStr(parts[2]); + return { + head: parts[1], + middle: middle, + tail: parts[3], + whole: parts[1] + middle.whole + parts[3] + }; + } + else { + return { + head: null, + middle: null, + tail: null, + whole: cmdStr + }; + } + } + function formatRange(cmd, formatStart, formatEnd, separator) { + if (cmd.middle) { + var startHead = formatStart(cmd.head); + var startMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator); + var startTail = formatStart(cmd.tail); + var endHead = formatEnd(cmd.head); + var endMiddle = formatRange(cmd.middle, formatStart, formatEnd, separator); + var endTail = formatEnd(cmd.tail); + if (startHead === endHead && startTail === endTail) { + return startHead + + (startMiddle === endMiddle ? startMiddle : startMiddle + separator + endMiddle) + + startTail; + } + } + return formatStart(cmd.whole) + separator + formatEnd(cmd.whole); + } + + exports.default = main; + exports.toDateTime = toDateTime; + exports.toDuration = toDuration; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/library/fullcalendar/packages/luxon/main.min.js b/library/fullcalendar/packages/luxon/main.min.js new file mode 100644 index 000000000..6267f8bbe --- /dev/null +++ b/library/fullcalendar/packages/luxon/main.min.js @@ -0,0 +1,20 @@ +/*! +FullCalendar Luxon Plugin v4.0.2 +Docs & License: https://fullcalendar.io/ +(c) 2019 Adam Shaw +*/ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("luxon"),require("@fullcalendar/core")):"function"==typeof define&&define.amd?define(["exports","luxon","@fullcalendar/core"],t):(e=e||self,t(e.FullCalendarLuxon={},e.luxon,e.FullCalendar))}(this,function(e,t,n){"use strict";function o(e,t){function n(){this.constructor=e}f(e,t),e.prototype=null===t?Object.create(t):(n.prototype=t.prototype,new n)}function r(e,o){if(!(o instanceof n.Calendar))throw new Error("must supply a Calendar instance");return t.DateTime.fromJSDate(e,{zone:o.dateEnv.timeZone,locale:o.dateEnv.locale.codes[0]})}function a(e,o){if(!(o instanceof n.Calendar))throw new Error("must supply a Calendar instance");return t.Duration.fromObject(m({},e,{locale:o.dateEnv.locale.codes[0]}))}function i(e,t){var n=c(e);if(t.end){var o=u(t.start.array,t.timeZone,t.localeCodes[0]),r=u(t.end.array,t.timeZone,t.localeCodes[0]);return d(n,o.toFormat.bind(o),r.toFormat.bind(r),t.separator)}return u(t.date.array,t.timeZone,t.localeCodes[0]).toFormat(n.whole)}function l(e){return[e.year,e.month-1,e.day,e.hour,e.minute,e.second,e.millisecond]}function u(e,n,o){return t.DateTime.fromObject({zone:n,locale:o,year:e[0],month:e[1]+1,day:e[2],hour:e[3],minute:e[4],second:e[5],millisecond:e[6]})}function c(e){var t=e.match(/^(.*?)\{(.*)\}(.*)$/);if(t){var n=c(t[2]);return{head:t[1],middle:n,tail:t[3],whole:t[1]+n.whole+t[3]}}return{head:null,middle:null,tail:null,whole:e}}function d(e,t,n,o){if(e.middle){var r=t(e.head),a=d(e.middle,t,n,o),i=t(e.tail),l=n(e.head),u=d(e.middle,t,n,o),c=n(e.tail);if(r===l&&i===c)return r+(a===u?a:a+o+u)+i}return t(e.whole)+o+n(e.whole)}/*! ***************************************************************************** + Copyright (c) Microsoft Corporation. All rights reserved. + Licensed under the Apache License, Version 2.0 (the "License"); you may not use + this file except in compliance with the License. You may obtain a copy of the + License at http://www.apache.org/licenses/LICENSE-2.0 + + THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED + WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, + MERCHANTABLITY OR NON-INFRINGEMENT. + + See the Apache Version 2.0 License for specific language governing permissions + and limitations under the License. + ***************************************************************************** */ +var f=function(e,t){return(f=Object.setPrototypeOf||{__proto__:[]}instanceof Array&&function(e,t){e.__proto__=t}||function(e,t){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])})(e,t)},m=function(){return m=Object.assign||function(e){for(var t,n=1,o=arguments.length;n